6 releases

0.2.0 Mar 11, 2024
0.1.4 Feb 9, 2024
0.1.3 Dec 21, 2023

#151 in Procedural macros

Unlicense

57KB
1K SLoC

restructed


A quick and easy way to create derivative models of your existing types without repeating yourself all the damn time.

  • Reduce number of structs you need to write
  • Allows deriving on generated structs
  • Allows generating multiple structs from one derive
  • Automatically generates From<T> traits for original <> generated structs

New features planned are available here on github. See below for examples of current usage & features.

Usage

Add restructed to your projects Cargo.toml:

restructed = "0.1"

alternatively run this in the project directory

cargo add restructed

Add the import and derive it on the target struct

#[derive(restructed::Models)]
struct User {
    id: i32,
    username: String
}

And then add attributes for each model you want to create.

#[derive(restructed::Models)]
#[view(UserId, fields(id))]        // <-- Simple subset of the deriving structs field
#[patch(UserUpdatables, omit(id))] // <-- Wraps all fields with a Option type inside a new struct
struct User {
    id: i32,
    username: String,
}

Continue reading for the available models and their breakdown and arguments in general.

Models

These are arguments that can be applied there respective Attributes (i.e. #[attr(args...)])).

#[view]

A model with generates a subset of the original/parent deriving Model. Useful for creating things like RESTful API or Database views.

Argument Name description Required? Type/Enum Example
name Name of the struct the generate True+First Identifier MyStruct
fields or Field names in the original structure to include False List(Ident) fields(field1, field2, ...)
omit Field names in the original structure to exclude False List(Ident) omit(field1, field2, ...)
derive Things to derive on the newly generated struct False List(Path) derive(Debug, thiserror::Error)
preset Behaviours and/or defaults to apply False none/write/read preset = "all"
attributes_with Attributes to inherit at both struct & field level False none/oai/deriveless/all attributes_with = "all"
#[derive(Clone, restructed::Models)]
#[view(UserProfile, omit(id, password))]
struct User {
    id: i32, // Not in `UserProfile`
    display_name: String,
    bio: String,
    extra: Option<String>,
    password: String, // Not in `UserProfile`
}

#[patch]

A model which creates subsets of your data except each field's type is wrapped in a Option<t> or a alternative type of Option implementation if specified. Useful for creating RESTful API patch method types or Database Table Patches where you only want to update fields if they were explictly given (even to delete).

Argument Name description Required? Type/Enum Example
name Name of the struct the generate True+First Identifier MyStruct
fields or Field names in the original structure to include False List(Ident) fields(field1, field2, ...)
omit Field names in the original structure to exclude False List(Ident) omit(field1, field2, ...)
derive Things to derive on the newly generated struct False List(Path) derive(Debug, thiserror::Error)
preset Behaviours and/or defaults to apply False none/write/read preset = "all"
attributes_with Attributes to inherit at both struct & field level False none/oai/deriveless/all attributes_with = "all"
option A alternative to Option<T> to wrap fields with False Option/MaybeUndefined option = MaybeUndefined
#[derive(Clone, restructed::Models)]
#[patch(UserUpdate, fields(display_name, bio, extra, password))]
struct User {
    id: i32, // Not in `UserUpdate`
    display_name: String, // Option<String> in `UserUpdate`
    bio: String, // Option<String> in `UserUpdate`
    extra: Option<String>, // Option<Option<String>> in `UserUpdate` (If this isn't desired, see *option* arg and the *openapi* crate feature)
    password: String, // Not in `UserProfile`
}

#[model]

Not a model, used to define a base or default set of arguments to be apply to all models. Acts as a interface for taking arguments to apply more broad and doesn't generate any models itself.

There are two arguments possible

base

A list of non-overridable arguments that are applied to all generated arguments that you cab build on-top off. It doesn't prevent you from using the in individual models later but it also won't allow you to undo the effect individually.

e.g. #[model(base(...)]

Argument Name description Required? Type/Enum Example
fields or Field names in the original structure to include False List(Ident) fields(field1, field2, ...)
omit Field names in the original structure to exclude False List(Ident) omit(field1, field2, ...)
derive Things to derive on the newly generated struct False List(Path) derive(Debug, thiserror::Error)

defaults

Arguments given in this list are applied to all models where the argument isn't given. Meaning, if writen #[model(defaults(fields(a, b)))] and then later #[view(omit(b))] is written, the fields(a, b) earlier will not be applied because the two args are mutally exclusive unlike with base arguments.

e.g. #[model(defaults(...))]

Argument Name description Required? Type/Enum Example
fields or Field names in the original structure to include False List(Ident) fields(field1, field2, ...)
omit Field names in the original structure to exclude False List(Ident) omit(field1, field2, ...)
derive Things to derive on the newly generated struct False List(Path) derive(Debug, thiserror::Error)
preset Behaviours and/or defaults to apply False none/write/read preset = "all"
attributes_with Attributes to inherit at both struct & field level False none/oai/deriveless/all attributes_with = "all"

Example

#[derive(Clone, restructed::Models)]
#[model(base(derive(Debug)))] // All models now *MUST* derive Debug (despite parent)
#[view(UserView)]
#[patch(UserPatch)]
struct User {
    id: i32,
    display_name: String,
    bio: String,
    extra: Option<String>,
    password: String,
}

fn debug_models() {
  let user = User {
    id: 1,
    display_name: "Dude".to_string(),
    bio: "Too long didn't read".to_string(),
    extra: None,
    password: "ezpz".to_string(),
  };

  let view: UserView = user.clone().into(); // Automatically gen from model
  print!("A view of a user {:?}", view);

  let patch: UserPatch = user.clone().into(); // Automatically gen from model
  print!("A patch of a user {:?}", patch);
}

Argument Behaviours

preset

A string literal of the preset to use, presets are a set of defaults to apply to a model. Below is a list of what arguments are composed in a preset. [e.g. preset = "none"]

  • none - Does nothing and is the default behaviour [Default]

  • write ['openapi' Feature Flag] - Designed to only show properties that can be written to.

    • omit - Applied as a base, any fields with #[oai(read_only)] attribute are removed, your fields/omit is applied after
    • option patch only - Arg defaults to MaybeUndefined
  • read ['openapi' Feature Flag] - Designed to only show properties that can always be read.

    • omit - Applied as a base, any fields with #[oai(write_only)] attribute are removed, your fields/omit is applied after
    • option patch only - arg defaults to MaybeUndefined

attributes_with

A string literal of the attributes to inherit at both struct & field level. Below is a list of values. [e.g. attributes_with = "none"]

  • none - Does not Includes any attributes [Default]
  • oai ['openapi' Feature Flag] - Includes all Poem's OpenAPI attributes
  • deriveless - Includes all attributes but omits the derive attributes
  • all - Includes all attributes

Known Limitations

  • Generic Structs & Enums - At the moment, this crate doesn't support deriving models on Structs that need to be generic (e.g. deriving on a Struct<T>). I just don't need the feature, contributions are welcome however!

Crate Features

Links are to other crates GitHub page that are related to the features.

Poem OpenAPI

Enables wrapping Option<T> from the source struct with MaybeUndefined<T> from the poem-openapi crate in patch models. All oai(...) attributes can also be explictly copied over to the generated struct meaning you keep all validators, etc..

use restructed::Models;

#[derive(poem_openapi::Object, Models)]
#[oai(skip_serializing_if_is_none, rename_all = "camelCase")]
#[model(base(derive(poem_openapi::Object, Debug)), defaults(preset = "read"))]
#[patch(UserUpdate, preset = "write")]
#[view(UserProfile)]
#[view(UserNames, fields(username, name, surname))]
pub struct User {
    #[oai(read_only)]
    pub id: u32,
    // profile
    #[oai(validator(min_length = 3, max_length = 16, pattern = r"^[a-zA-Z0-9_]*$"))] // oai attributes carry over with `preset = write/write` or attributes_with="oai"
    pub username: String,
    #[oai(validator(min_length = 5, max_length = 1024), write_only)]
    pub password: String,
    #[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))]
    pub name: Option<String>,
    #[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))]
    pub surname: Option<String>, // in patch modeels, this is `MaybeUndefined` type with default with preset `read` or `write` (or option = MaybeUndefined)
    #[oai(read_only)]
    pub joined: u64,
}

Dependencies

~330–780KB
~18K SLoC