From da8d4416c5364f68f0ec959b14eefd341f7127b5 Mon Sep 17 00:00:00 2001 From: Callum Cunha Date: Sun, 10 Mar 2024 15:50:17 +0000 Subject: [PATCH 1/5] majority rewrite with new arguments --- Cargo.toml | 22 +- readme.md | 180 +++++++++------- src/clone.rs | 105 ---------- src/lib.rs | 268 +----------------------- src/logic/args.rs | 283 +++++++++++++++++++++++++ src/logic/mod.rs | 268 ++++++++++++++++++++++++ src/logic/tests.rs | 48 +++++ src/patch.rs | 344 +++++++++++-------------------- src/view.rs | 278 ++++++++++++------------- tests/clone.rs | 22 +- tests/feature_testing/builder.rs | 38 ---- tests/feature_testing/openapi.rs | 30 ++- tests/feature_testing/welds.rs | 48 ----- tests/mod.rs | 2 +- tests/patch.rs | 45 +++- tests/view.rs | 39 ++++ 16 files changed, 1081 insertions(+), 939 deletions(-) delete mode 100644 src/clone.rs create mode 100644 src/logic/args.rs create mode 100644 src/logic/mod.rs create mode 100644 src/logic/tests.rs delete mode 100644 tests/feature_testing/builder.rs delete mode 100644 tests/feature_testing/welds.rs diff --git a/Cargo.toml b/Cargo.toml index b2d4184..15f19d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,26 +14,26 @@ license-file = "license.txt" proc-macro = true [features] -default = ["builder", "openapi"] -openapi = ["dep:poem-openapi"] -builder = ["dep:typed-builder"] -welds = ["dep:welds"] +default = ["openapi"] +openapi = [] +# builder = ["dep:typed-builder"] +# welds = ["dep:welds"] [dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["parsing"] } +proc-macro-error = "1.0.4" -# Supported Crates -poem-openapi = { version = "3.0", features = [ +[dev-dependencies] +# # Supported Crates +poem-openapi = { version = "4.0", features = [ "uuid", "chrono", -], optional = true } - -typed-builder = { version = "0.18.0", optional = true } -welds = { version = "0.2.1", default-features = false, optional = true} -proc-macro-error = "1.0.4" +] } +# typed-builder = { version = "0.18.0" } +# welds = { version = "0.3", default-features = false} [package.metadata.docs.rs] all-features = true diff --git a/readme.md b/readme.md index baa869d..610bd28 100644 --- a/readme.md +++ b/readme.md @@ -34,8 +34,6 @@ New features planned are available [here](https://github.com/NexRX/restructed/is # Usage -This library requires the `nightly` channel. - Add `restructed` to your projects `Cargo.toml`: ```toml @@ -51,8 +49,6 @@ cargo add restructed Add the import and derive it on the target struct ```rust -use restructed::Models; - #[derive(restructed::Models)] struct User { id: i32, @@ -65,112 +61,142 @@ And then add attributes for each model you want to create. ```rust #[derive(restructed::Models)] -#[patch(UserUpdatables, omit(id))] // <-- Wraps all fields in a new struct with Option -#[view(UserId, fields(id))] // <-- Selectively includes fields in a new struct +#[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. +Continue reading for the available models and their breakdown and arguments in general. + -Now anywhere that requires a `T: OrderStoreFilter` will also accept an `&T` or `Arc` where `T` implements `OrderStoreFilter`. +## Common Arguments +These are arguments that can be applied to all models. -## Models +- `name` - The name of the struct the generate [**Required**, **Must be first** e.g. `MyStruct`] +- `fields` [**Only One**] + - `fields` - A _list_ of field names in the original structure to carry over [e.g. `fields(field1, field2, ...)`] + - `omit` - A _list_ of field names in the original structure to omit [e.g. `omit(field1, field2, ...)`] +- `derive` - A _list_ of derivables to derive on the generated struct just like you would normally [e.g. `derive(Clone, Debug, thiserror::Error)`] +- `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** - **Default**, does nothing and is the default behaviour + - **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` -Each model is defined using an attribute after deriving `Models` and multiple models (of the same kind) can be had with multiple attributes. + - **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` -### view +- `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 consider any attributes [**Default**] + - **oai** *['openapi' Feature Flag]* - Composes all Poem's OpenAPI attributes + - **deriveless** - Composes all attributes but omits the derive attributes + - **all** - Composes all attributes -A selective subset of fields from the original model of the same types. Useful for generating views of a database table, etc. Supports both struct and enums +
-**Arguements:** +# Models +Derivable models via the struct attributes. -- `name` - The name of the struct the generate (**Required**, **Must be first** e.g. `MyStruct`) -- `fields` - A _list_ of field names in the original structure to carry over (**Required**, e.g. `fields(field1, field2, ...)`) -- `derive` - A _list_ of derivables (in scope) to derive on the generated struct (e.g. `derive(Clone, Debug, thiserror::Error)`) -- `default_derives` - A _bool_, if `true` _(default)_ then the a list of derives will be additionally derived. Otherwise, `false` to avoid this (e.g. `default_derives = false`) +## **Patch** +A subset model where every field's type is an option in some way. It's called patch because it reflect a REST / DB patch of the original struct. -**Example:** +#### **Unique Args** +- `option` - A _Identifer_ that allows a different option implementation from supported crates [e.g. `option = MaybeUndefined` (from poem-openapi)] + - **Option** - The standard implementation of Option [**Default**] + - **MaybeUndefined** *['openapi' Feature Flag]* - Use Poem's OpenAPI crate and it's Option implmentation +#### **Example** ```rust - // Original - #[derive(restructed::Models)] - #[view(UserProfile, fields(display_name, bio), derive(Clone), default_derives = false)] - struct User { - id: i32, - display_name: String, - bio: String, - password: String, - } +#[derive(restructed::Models)] +#[patch(UserUpdate, omit(id), option = Option)] +struct User { + id: i32, + display_name: String, + bio: String, + extra: Option, + password: String, +} ``` -Generates: - +will expand into something like this: ```rust - #[derive(Clone)] - struct UserProfile { - display_name: String, - bio: String, - } +struct UserUpdate { + display_name: Option, + bio: Option, + extra: Option>, + password: Option, +} ``` -### patch - -A complete subset of fields of the original model wrapped in `Option` with the ability to omit instead select fields.
-I want to note that patch currently doesn't support enums as I don't see the use-case for it. If someone can, feel free to submit a feature request +## **View** +A simple subset of the deriving model/struct. -**Arguements:** - -- `name` - The name of the struct the generate (**Required**, **Must be first** e.g. `MyStruct`) -- `omit` - A _list_ of field names in the original structure to omit (e.g. `fields(field1, field2, ...)`) -- `derive` - A _list_ of derivables (in scope) to derive on the generated struct (e.g. `derive(Clone, Debug, thiserror::Error)`) -- `default_derives` - A _bool_, if `true` _(default)_ then the a list of derives will be additionally derived. Otherwise, `false` to avoid this (e.g. `default_derives = false`) - -**Example:** +#### **Unique Args** +- N/A +#### **Example** ```rust - // Original - #[derive(restructed::Models)] - #[patch(UserUpdate, omit(id))] - struct User { - id: i32, - display_name: String, - bio: String, - password: String, - } +#[derive(restructed::Models)] +#[view(UserPublic, fields(display_name, bio))] +struct User { + id: i32, + display_name: String, + bio: String, + extra: Option, + password: String, +} ``` -Generates: - +will expand into something like this: ```rust - #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] // <-- Default derives (when *not* disabled) - struct UserUpdate { - display_name: Option, - bio: Option, // MaybeUndefined with feature 'openapi' - password: Option, - } +struct UserPublic { + display_name: String, + bio: String, +} ``` -## Crate Features - -Links are to other crates GitHub page that are related to the features.
-Only `builder` is enabled by default. - -### `openapi` +
-Wraps `Option` from the source struct with `MaybeUndefined` from the [poem-openapi](https://github.com/poem-web/poem/tree/master/poem-openapi) crate in `patch` models. All `oai(...)` attributes are also copied over to the generated struct meaning you keep all validators, etc.. +# Complex Example +Just to demonstrate the versitility of this crate, here is an example using all the possible arguments at once using all features. -### `builder` +## Poem OpenAPI +Each attribute is copied over so all your validations are kept -Uses the [typed-builder](https://github.com/idanarye/rust-typed-builder) crate to derive a builder for add a type safe builder for all generated models. +```rust +use restructed::Models; -### `welds` +#[cfg(test)] // For rust_doc test purposes +#[derive(poem::Object, Models)] +#[oai(skip_serializing_if_is_none, rename_all = "camelCase")] +#[patch(UserUpdate, preset = "write", derive(Object))] +#[view(UserProfile, preset = "view", derive(Object))] +#[view(UserNames, preset = "view", derive(Object))] +pub struct User { + #[oai(read_only)] + pub id: u32, + // profile + #[oai(validator(min_length = 3, max_length = 16, pattern = r"^[a-zA-Z0-9_]*$"))] + 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, + #[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))] + pub surname: Option, + #[oai(read_only)] + pub joined: u64, +} +``` -Generates a function to merge changes for returning a `DbState` from the [welds](https://github.com/weldsorm/welds) crate. +
-## Contributions & Bugs +# Crate Features +Links are to other crates GitHub page that are related to the features.
-This is my first self publish proc macro so any feedback and feature request, changes, pull requests are all welcome!
-If you find any bugs do submit a github issue with any relavent information and I'll try to fix it. +## Poem OpenAPI +Enables wrapping `Option` from the source struct with `MaybeUndefined` from the [poem-openapi](https://github.com/poem-web/poem/tree/master/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.. \ No newline at end of file diff --git a/src/clone.rs b/src/clone.rs deleted file mode 100644 index 8564fef..0000000 --- a/src/clone.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::*; -use proc_macro2::{Ident, TokenStream, TokenTree}; -use quote::quote; -use syn::{self, Attribute, DeriveInput}; - -struct CloneModelArgs { - name: Ident, - derives: Vec, -} - -pub fn impl_clone_model(ast: &DeriveInput, attr: &Attribute) -> TokenStream { - let CloneModelArgs { name, derives } = parse_clone_attributes(attr); - - let attrs: Vec<_> = ast - .attrs - .iter() - .filter(|v| is_attribute(v, "view")) - .filter(|v| is_attribute(v, "patch")) - .filter(|v| is_attribute(v, "clone")) - .collect(); - let vis = &ast.vis; - #[allow(unused_assignments)] - let mut data_type = TokenStream::default(); - let data = match &ast.data { - syn::Data::Struct(v) => { - data_type = quote!(struct); - let fields = &v.fields; - quote!(#fields) - } - syn::Data::Enum(v) => { - data_type = quote!(enum); - let fields = &v.variants; - quote!(#fields) - } - syn::Data::Union(v) => { - data_type = quote!(v.union_token.clone()); - let fields = &v.fields; - quote!(#fields) - } - }; - - quote! { - #[derive(#(#derives),*)] - #( #attrs )* - #vis #data_type #name - #data - } -} - -// TODO: Impl this for struct, union, and enum -fn impl_from_trait( - original_name: &Ident, - name: &Ident, - field_from_mapping: Vec, - is_struct: bool, -) -> proc_macro2::TokenStream { - if is_struct { - quote! { - impl ::core::convert::From<#original_name> for #name { - fn from(value: #original_name) -> Self { - Self { - #(#field_from_mapping),* - } - } - } - } - } else { - quote! { - impl ::core::convert::From<#name> for #original_name { - fn from(value: #name) -> Self { - match value { - #(#field_from_mapping),* - } - } - } - } - } -} - -fn parse_clone_attributes(attr: &Attribute) -> CloneModelArgs { - let tks: Vec = attr - .meta - .require_list() - .unwrap() - .to_owned() - .tokens - .into_iter() - .collect::>(); - - let name = match &tks[0] { - TokenTree::Ident(v) => v.clone(), - x => { - abort!( - x, - "First argument must be an identifier (name) of the struct for the clone" - ) - } - }; - - let mut args_slice = tks[2..].to_vec(); - let derives = parse_derives(&mut args_slice); - abort_unexpected_args(vec!["derive"], &args_slice); - - CloneModelArgs { name, derives } -} diff --git a/src/lib.rs b/src/lib.rs index 28e8d08..48990d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,293 +1,37 @@ #![doc = include_str!("../readme.md")] +mod logic; + mod patch; mod view; -mod clone; +use crate::logic::is_attribute; use proc_macro::TokenStream; -use proc_macro2::{Group, Ident, TokenTree}; -use proc_macro_error::{abort, proc_macro_error}; -use quote::quote; -use syn::Attribute; +use proc_macro_error::proc_macro_error; -/// Derives any number of models that are a subset of the struct deriving this macro. There are two types of models possible.
-/// # view -///A selective subset of fields from the original model of the same types. -/// -///**Arguements:** -///- `name` - The name of the struct the generate (**Required**, **Must be first** e.g. `MyStruct`) -///- `fields` - A *list* of field names in the original structure to carry over (**Required**, e.g. `fields(field1, field2, ...)`) -///- `derive` - A *list* of derivables (in scope) to derive on the generated struct (e.g. `derive(Clone, Debug, thiserror::Error)`) -///- `default_derives` - A *bool*, if `true` *(default)* then the a list of derives will be additionally derived. Otherwise, `false` to avoid this (e.g. `default_derives = false`) -/// -///**Example:** -///```rust -/// // Original -/// #[derive(restructed::Models)] -/// #[view(UserProfile, fields(display_name, bio), derive(Clone), default_derives = false)] -/// struct User { -/// id: i32, -/// display_name: String, -/// bio: String, -/// password: String, -/// } -///``` -///Generates: -///```rust -/// #[derive(Clone)] -/// struct UserProfile { -/// display_name: String, -/// bio: String, -/// } -///``` -/// -///# patch -///A complete subset of fields of the original model wrapped in `Option` with the ability to omit instead select fields. -/// -///**Arguements:** -///- `name` - The name of the struct the generate (**Required**, **Must be first** e.g. `MyStruct`) -///- `omit` - A *list* of field names in the original structure to omit (**Required**, e.g. `fields(field1, field2, ...)`) -///- `derive` - A *list* of derivables (in scope) to derive on the generated struct (e.g. `derive(Clone, Debug, thiserror::Error)`) -///- `default_derives` - A *bool*, if `true` *(default)* then the a list of derives will be additionally derived. Otherwise, `false` to avoid this (e.g. `default_derives = false`) -/// -///**Example:** -///```rust -/// // Original -/// #[derive(restructed::Models)] -/// #[patch(UserUpdate, omit(id))] -/// struct User { -/// id: i32, -/// display_name: String, -/// bio: String, -/// password: String, -/// } -///``` -/// -///Generates: -///```rust -/// #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] // <-- Default derives (when *not* disabled) -/// struct UserUpdate { -/// display_name: Option, -/// bio: Option, // MaybeUndefined with feature 'openapi' -/// password: Option, -/// } -///``` -/// -/// For more information, read the crate level documentation. #[proc_macro_error] #[proc_macro_derive(Models, attributes(view, patch, clone))] pub fn models(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as syn::DeriveInput); - let oai_attr = get_oai_attributes(&ast.attrs); - let views: Vec = ast .attrs .iter() .filter(|v| is_attribute(v, "view")) - .map(|a| view::impl_view_model(&ast, a, &oai_attr)) + .map(|a| view::impl_view_model(&ast, a)) .collect(); let patches: Vec = ast .attrs .iter() .filter(|v| is_attribute(v, "patch")) - .map(|a| patch::impl_patch_model(&ast, a, &oai_attr)) + .map(|a| patch::impl_patch_model(&ast, a)) .collect(); - // let clones: Vec = ast - // .attrs - // .iter() - // .filter(|v| is_attribute(v, "clone")) - // .map(|a| clone::impl_clone_model(&ast, a)) - // .collect(); - let gen = quote::quote!( #(#views)* #(#patches)* - // #(#clones)* ); gen.into() } - -// TODO: this stuff slaps. i should make it public - -// Supporting Functions - -fn get_oai_attributes(attrs: &[Attribute]) -> Vec<&Attribute> { - attrs - .iter() - .filter(|attr| is_attribute(attr, "oai")) - .collect() -} - -/// Check the first segment of an attribute to see if it matches the given name -fn is_attribute(attr: &Attribute, name: &str) -> bool { - get_attribute_name(attr).map_or(false, |v| v == name) -} - -/// Gets the first segment of the attribute which should typically the name of the attribute -fn get_attribute_name(attr: &Attribute) -> Option<&Ident> { - attr.meta.path().segments.first().map(|v| &v.ident) -} - -// "Invalid or missing `{name}` argument, expected a group of args, e.g. `{name}(...)`" - -/// Extract a group for a given identifier, e.g. `name(...)`. The `(...)` part is returned. -fn take_ident_group(name: &str, args: &mut Vec) -> Option { - for (i, tk) in args.iter().enumerate() { - match tk { - TokenTree::Ident(v) if *v == name => match args.get(i + 1) { - Some(TokenTree::Group(g)) => { - let g = g.to_owned(); - args.remove(i); // Remove Ident - args.remove(i); // Remove Group - if matches!(args.get(i), Some(TokenTree::Punct(v)) if v.as_char() == ',') { - args.remove(i); // Remove leading Comma - } - return Some(g); - }, - e => abort!(e, "Invalid or missing `{name}` argument, expected a group of args, e.g. `{name}(...)`"), - }, - _ => {} - } - } - None -} - -/// Extract a literal for a given identifier, e.g. `name = "..."`. The `"..."` part is returned. -fn take_ident_bool(name: &str, args: &mut Vec) -> Option { - for (i, tk) in args.iter().enumerate() { - match tk { - TokenTree::Ident(v) if *v == name => match args.get(i + 1) { - Some(TokenTree::Punct(p)) if p.as_char() == '=' => { - let value = args - .get(i + 2) - .map(|v| v.to_string()) - .unwrap_or("Nothing".to_string()); - let b = match value == "true" || value == "false" { - true => value == "true", - false => abort!( - tk, "Invalid or missing `{name}` argument, expected a bool, e.g. `{name} = true`" - ) - }; - - args.remove(i); // Remove Ident - args.remove(i); // Remove Punct - args.remove(i); // Remove Literal (Bool) - if matches!(args.get(i), Some(TokenTree::Punct(v)) if v.as_char() == ',') { - args.remove(i); // Remove leading Comma - } - return Some(b); - } - _ => abort!( - tk, - "Invalid or missing `{name}` argument, expected a bool, e.g. `{name} = true`" - ), - }, - _ => {} - } - } - None -} - -/// Parse the fields argument into a TokenStream, skipping checking for commas coz lazy -fn extract_idents(group: Group) -> Vec { - group - .stream() - .into_iter() - .filter_map(|tt| match tt { - TokenTree::Ident(ident) => Some(ident), - TokenTree::Punct(v) if v.as_char() == ',' => None, - tt => abort!(tt, "Invalid syntax, expected a field identifier, got {tt}`"), - }) - .collect() -} - -fn extract_oai_f_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> { - attrs - .iter() - .filter(|attr| { - attr.meta - .path() - .segments - .first() - .map_or(false, |seg| seg.ident == "oai") - }) - .collect() -} - -fn is_doc(v: &&Attribute) -> bool { - v.meta.require_name_value().map_or(false, |v| { - v.path.segments.first().map_or(false, |v| v.ident == "doc") - }) -} - -fn extract_docs(attrs: &[Attribute]) -> proc_macro2::TokenStream { - let docs: Vec<_> = attrs.iter().filter(is_doc).collect(); - quote!(#(#docs)*) -} - -/// Aborts on unexpected args to show that they arent valid -fn abort_unexpected_args(names: Vec<&str>, args: &[TokenTree]) { - for tk in args.iter() { - match tk { - TokenTree::Ident(v) if names.contains(&v.to_string().as_str()) => {} - TokenTree::Ident(v) => { - abort!( - v, - "Unknown argument `{}`, all known arguments are {:?}", - v, - names - ) - } - _ => {} - } - } -} -/// Parse a list of identifiers we want to derive. Will be empty if none are found. -fn parse_derives(args: &mut Vec) -> Vec { - // Extract the fields args and ensuring it is a key-value pair of Ident and Group - let fields: Group = match take_ident_group("derive", args) { - Some(g) => g, - None => return vec![], - }; - - extract_idents(fields) -} - -fn parse_default_derives(args: &mut Vec) -> bool { - // Extract the fields args and ensuring it is a key-value pair of Ident and Group - take_ident_bool("default_derives", args).unwrap_or(true) -} - -fn get_derive( - defaults: bool, - from_args: Vec<&Ident>, - for_struct: bool, -) -> proc_macro2::TokenStream { - let mut derives: Vec = vec![]; - - if defaults { - derives.push(quote!(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)); - if for_struct { - derives.push(quote!(Default)); - #[cfg(feature = "openapi")] - { - derives.push(quote!(::poem_openapi::Object)); - } - #[cfg(feature = "builder")] - { - derives.push(quote!(::typed_builder::TypedBuilder)); - } - } - } - if !from_args.is_empty() { - derives.push(quote!(#(#from_args),*)); - } - - quote!( - #[derive(#(#derives),*)] - ) -} diff --git a/src/logic/args.rs b/src/logic/args.rs new file mode 100644 index 0000000..ce1241f --- /dev/null +++ b/src/logic/args.rs @@ -0,0 +1,283 @@ +use super::{ + abort_unexpected_args, extract_idents, has_oai_attribute, take_ident_group, take_ident_ident, take_ident_literal, take_path_group +}; +use proc_macro2::{Ident, TokenTree}; +use proc_macro_error::abort; +use syn::{Attribute, Field}; + +#[derive(Clone)] +pub(crate) struct AttrArgs { + pub name: Ident, + pub fields: FieldsArg, + pub derive: Option>, + pub preset: Preset, + pub attributes_with: AttributesWith, +} + +impl AttrArgs { + /// Conditional aborts on unexpected args to show that they arent valid + pub(crate) fn abort_unexpected(args: &[TokenTree], ignore: &[&str]) { + const EXPECTED: &[&str; 5] = &["fields", "omit", "derive", "attributes_with", "preset"]; + let mut expect = EXPECTED.to_vec(); + expect.extend(ignore); + abort_unexpected_args(expect, args) + } + + /// Parses the attribute and returns the parsed arguments (0) and any remaining (1) + pub(crate) fn parse(attr: &Attribute, abort_unexpected: bool) -> (Self, Vec) { + let tks: Vec = attr + .meta + .require_list() + .unwrap() + .to_owned() + .tokens + .into_iter() + .collect::>(); + + let name = match &tks[0] { + TokenTree::Ident(v) => v.clone(), + x => { + abort!( + x, + "First argument must be an identifier (name) of the struct for the view" + ) + } + }; + + if tks.len() < 3 { + abort!(attr, "Invalid syntax, expected at least one argument"); + } + + let mut args = tks[2..].to_vec(); + let args_mr = &mut args; + + // Parse Expected Macro Args + let fields = FieldsArg::parse(args_mr, attr); + let derive = take_path_group("derive", args_mr); + let preset = Preset::parse(args_mr).unwrap_or_default(); + let attributes_with = AttributesWith::parse(args_mr).unwrap_or_else(|| preset.attr_with()); + + if abort_unexpected { + Self::abort_unexpected(&args, &[]) + } + + ( + Self { + name, + fields, + derive, + preset, + attributes_with + }, + args, + ) + } +} + +#[derive(Debug, Clone)] +pub(crate) enum FieldsArg { + /// Fields to be included in the model (A whitelist) + Fields(Vec), + /// Fields to be omitted from the model (A blacklist) + Omit(Vec), +} + +impl FieldsArg { + pub(crate) fn parse(args: &mut Vec, attr_spanned: &Attribute) -> Self { + // Extract the fields args and ensuring it is a key-value pair of Ident and Group + + let field_arg = take_ident_group("fields", args); + let omit_args = take_ident_group("omit", args); + + if field_arg.is_some() && omit_args.is_some() { + abort!( + attr_spanned, + "Cannot have both `fields` and `omit` arguments" + ) + } + + // Parse the fields argument into a TokenStream, skip checking for commas coz lazy + match (field_arg, omit_args) { + (Some(g), None) => Self::Fields(extract_idents(g)), + (None, Some(g)) => Self::Omit(extract_idents(g)), + (None, None) => Self::Omit(vec![]), + (Some(_), Some(_)) => abort!( + attr_spanned, + "Cannot have both `fields` and `omit` arguments" + ), + } + } + + pub(crate) fn predicate(&self, field: &Ident) -> bool { + match self { + Self::Fields(fields) => fields.contains(field), + Self::Omit(fields) => !fields.contains(field), + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub(crate) enum AttributesWith { + #[default] + None, + #[cfg(feature = "openapi")] + Oai, + Deriveless, + All, +} + +impl AttributesWith { + pub(crate) fn parse(args: &mut Vec) -> Option { + let ident = take_ident_literal("attributes_with", args)?; + + let value = ident.to_string(); + Some(match &value[1..value.chars().count() - 1] { + "none" => Self::None, + #[cfg(feature = "openapi")] "oai" => Self::Oai, + "deriveless" => Self::Deriveless, + "all" => Self::All, + #[cfg(feature = "openapi")] + v => abort!( + ident, + "Invalid value, expected `none`, `oai` (from poem_openapi crate), `deriveless`, or `all` but got `{}`", v + ), + #[cfg(not(feature = "openapi"))] + v => abort!( + ident, + "Invalid value, expected `none`, `deriveless`, or `all` but got `{}`", v + ), + }) + } + + pub(crate) fn gen_top_attributes<'a>(&self, ast: &'a syn::DeriveInput) -> Vec<&'a Attribute> { + match self { + Self::All => ast + .attrs + .iter() + .filter(|attr| { + attr.path().segments.first().map_or(true, |seg| { + !matches!(seg.ident.to_string().as_str(), "view" | "patch") + }) // update if we add more + }) + .collect(), + Self::Deriveless => ast + .attrs + .iter() + .filter(|attr| { + attr.path().segments.first().map_or(false, |seg| { + !matches!(seg.ident.to_string().as_str(), "view" | "patch" | "derive") + }) // update if we add more + }) + .collect(), + #[cfg(feature = "openapi")] + Self::Oai => ast + .attrs + .iter() + .filter(|attr| attr.meta.path().is_ident("oai")) + .collect(), + Self::None => vec![], + } + } + + pub(crate) fn gen_field_attributes(&self, attrs: Vec) -> Vec { + match self { + #[cfg(feature = "openapi")] + Self::Oai => attrs + .into_iter() + .filter(|attr| { + attr.meta + .path() + .segments + .first() + .map_or(false, |seg| seg.ident == "oai") + }) + .collect::>(), + Self::All | AttributesWith::Deriveless => attrs.into_iter().collect::>(), // change if we ever add field level attributes to this crate + Self::None => vec![], + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub(crate) enum OptionType { + #[default] + Option, + MaybeUndefined, +} + +impl OptionType { + pub(crate) fn parse(args: &mut Vec) -> Option { + let ident = take_ident_ident("option", args)?; + + Some(match ident.to_string().as_str() { + "Option" => OptionType::Option, + "MaybeUndefined" => OptionType::MaybeUndefined, + _ => abort!( + ident, + "Invalid type, expected `Option` or `MaybeUndefined` (from poem_openapi crate)" + ), + }) + } + +} + +#[derive(Debug, Clone, Copy, Default)] +pub(crate) enum Preset { + #[default] + None, + #[cfg(feature = "openapi")] + Read, + #[cfg(feature = "openapi")] + Write, +} + +impl Preset { + pub(crate) fn parse(args: &mut Vec) -> Option { + let ident = take_ident_literal("preset", args)?; + + let value = ident.to_string(); + Some(match &value[1..value.chars().count() - 1] { + "none" => Self::None, + #[cfg(feature = "openapi")] + "read" => Self::Read, + #[cfg(feature = "openapi")] + "write" => Self::Write, + #[cfg(feature = "openapi")] + v => abort!( + ident, + "Invalid value, expected `none` or `read`/`write` (with `openapi` feature) but got `{}`", v + ), + #[cfg(not(feature = "openapi"))] + v => abort!( + ident, + "Invalid value, expected `none` but got `{}`", v + ), + }) + } + + pub(crate) fn predicate(&self, field: &Field) -> bool { + match self { + Self::None => true, + #[cfg(feature = "openapi")] + Self::Read => !has_oai_attribute(&field.attrs, Some("write_only")), + #[cfg(feature = "openapi")] + Self::Write => !has_oai_attribute(&field.attrs, Some("read_only")), + } + } + + pub(crate) fn option(&self) -> OptionType { + match self { + Self::None => OptionType::Option, + #[cfg(feature = "openapi")] + Self::Read | Self::Write => OptionType::MaybeUndefined, + } + } + + pub(crate) fn attr_with(&self) -> AttributesWith { + match self { + Self::None => AttributesWith::None, + #[cfg(feature = "openapi")] + Self::Read | Self::Write => AttributesWith::Oai, + } + } +} diff --git a/src/logic/mod.rs b/src/logic/mod.rs new file mode 100644 index 0000000..cf42517 --- /dev/null +++ b/src/logic/mod.rs @@ -0,0 +1,268 @@ +use proc_macro2::{Group, Ident, Literal, TokenTree}; +use proc_macro_error::abort; +use quote::quote; +use syn::{parse2, Attribute}; + +#[cfg(test)] +mod tests; + +pub(crate) mod args; + +/// Check the first segment of an attribute to see if it matches the given name +pub(crate) fn is_attribute(attr: &Attribute, name: &str) -> bool { + get_attribute_name(attr).map_or(false, |v| v == name) +} + +/// Gets the first segment of the attribute which should typically the name of the attribute +pub(crate) fn get_attribute_name(attr: &Attribute) -> Option<&Ident> { + attr.meta.path().segments.first().map(|v| &v.ident) +} + +// "Invalid or missing `{name}` argument, expected a group of args, e.g. `{name}(...)`" + +/// Extract a group for a given identifier, e.g. `name(...)`. The `(...)` part is returned. +pub(crate) fn take_ident_group(name: &str, args: &mut Vec) -> Option { + for (i, tk) in args.iter().enumerate() { + match tk { + TokenTree::Ident(v) if *v == name => match args.get(i + 1) { + Some(TokenTree::Group(g)) => { + let g = g.to_owned(); + args.remove(i); // Remove Ident + args.remove(i); // Remove Group + if matches!(args.get(i), Some(TokenTree::Punct(v)) if v.as_char() == ',') { + args.remove(i); // Remove leading Comma + } + return Some(g); + }, + e => abort!(e, "Invalid or missing `{name}` argument, expected a group of args, e.g. `{name}(...)`"), + }, + _ => {} + } + } + None +} + +/// Extract a literal for a given identifier, e.g. `name = "..."`. The `"..."` part is returned. +#[allow(dead_code)] +pub(crate) fn take_ident_bool(name: &str, args: &mut Vec) -> Option { + for (i, tk) in args.iter().enumerate() { + match tk { + TokenTree::Ident(v) if *v == name => match args.get(i + 1) { + Some(TokenTree::Punct(p)) if p.as_char() == '=' => { + let value = args + .get(i + 2) + .map(|v| v.to_string()) + .unwrap_or("Nothing".to_string()); + let b = match value == "true" || value == "false" { + true => value == "true", + false => abort!( + tk, "Invalid or missing `{name}` argument, expected a bool, e.g. `{name} = true`" + ) + }; + + args.remove(i); // Remove Ident + args.remove(i); // Remove Punct + args.remove(i); // Remove Literal (Bool) + if matches!(args.get(i), Some(TokenTree::Punct(v)) if v.as_char() == ',') { + args.remove(i); // Remove leading Comma + } + return Some(b); + } + _ => abort!( + tk, + "Invalid or missing `{name}` argument, expected a bool, e.g. `{name} = true`" + ), + }, + _ => {} + } + } + None +} + +pub(crate) fn take_ident_ident(name: &str, args: &mut Vec) -> Option { + for (i, tk) in args.iter().enumerate() { + match tk { + TokenTree::Ident(v) if *v == name => match args.get(i + 1) { + Some(TokenTree::Punct(p)) if p.as_char() == '=' => { + let value = match args + .get(i + 2) { + Some(proc_macro2::TokenTree::Ident(v)) => v.to_owned(), + Some(v) => abort!(v, "Invalid arguement value, expected a identifier, e.g. `{} = *StructIdentifer*` but got {}", name, v), + None => abort!(v, "Missing `{name}` argument, expected an identifier, e.g. `{} = MyStruct`", name), + }; + + args.remove(i); // Remove Ident {name} + args.remove(i); // Remove Punct = + args.remove(i); // Remove Ident {value} + if matches!(args.get(i), Some(TokenTree::Punct(v)) if v.as_char() == ',') { + args.remove(i); // Remove leading Comma + } + return Some(value); + } + _ => abort!( + tk, + "Invalid or missing `{name}` argument, expected an identifier, e.g. `{name} = MyStruct`" + ), + }, + _ => {} + } + } + None +} + +pub(crate) fn take_ident_literal(name: &str, args: &mut Vec) -> Option { + for (i, tk) in args.iter().enumerate() { + match tk { + TokenTree::Ident(v) if *v == name => match args.get(i + 1) { + Some(TokenTree::Punct(p)) if p.as_char() == '=' => { + let value = match args + .get(i + 2) { + Some(proc_macro2::TokenTree::Literal(v)) => v.to_owned(), + Some(v) => abort!(v, "Invalid arguement value, expected a identifier, e.g. `{} = *StructIdentifer*` but got {}", name, v), + None => abort!(v, "Missing `{name}` argument, expected an identifier, e.g. `{} = MyStruct`", name), + }; + + args.remove(i); // Remove Ident {name} + args.remove(i); // Remove Punct = + args.remove(i); // Remove Literal {value} + if matches!(args.get(i), Some(TokenTree::Punct(v)) if v.as_char() == ',') { + args.remove(i); // Remove leading Comma + } + return Some(value); + } + _ => abort!( + tk, + "Invalid or missing `{name}` argument, expected an identifier, e.g. `{name} = MyStruct`" + ), + }, + _ => {} + } + } + None +} + +/// Extract a group for a given identifier, e.g. `name(...)`. The `(...)` part is returned. (Returns a group of syn::Path) +/// This function creates a stream/iter does 3 main things +/// 1. Filter for indices of all commas +/// 2. Create a range between indices (this will be the range of all paths) +/// 3. Parse the ranges into `syn::Path` +pub(crate) fn take_path_group(name: &str, args: &mut Vec) -> Option> { + let g = take_ident_group(name, args)?; + let paths: Vec = g + .stream() + .into_iter() + .enumerate() // Step 1 (Collect all comma indices) + .filter_map(|(i, v)| match v { + TokenTree::Punct(p) if p.as_char() == ',' => Some(i), + _ => None, + }) // Step 2 (Collect all ranges) + .chain(std::iter::once(g.stream().into_iter().count())) + .scan(0, |state, next| { + let start = match state { + 0 => 0, + _ => *state + 1, + }; + + let range = start..next; + *state = next; + + match start == next { + true => None, + false => Some(range), + } + }) + .map(|range| { + // Step 3 (Parse all paths) + let path: proc_macro2::TokenStream = g + .stream() + .into_iter() + .skip(range.start) + .take(range.end - range.start) + .collect(); + match parse2(path) { + Ok(v) => v, + Err(_) => abort!(g, "Invalid, not a valid derive Path (e.g. `core::fmt::Debug`) or Identifier (e.g. `Debug`)") + } + }) + .collect(); + match paths.is_empty() { + true => None, + false => Some(paths), + } +} + +/// Parse the fields argument into a TokenStream, skipping checking for commas coz lazy +pub(crate) fn extract_idents(group: Group) -> Vec { + group + .stream() + .into_iter() + .filter_map(|tt| match tt { + TokenTree::Ident(ident) => Some(ident), + TokenTree::Punct(v) if v.as_char() == ',' => None, + tt => abort!(tt, "Invalid syntax, expected a field identifier, got {tt}`"), + }) + .collect() +} + + +pub(crate) fn is_doc(v: &&Attribute) -> bool { + v.meta.require_name_value().map_or(false, |v| { + v.path.segments.first().map_or(false, |v| v.ident == "doc") + }) +} + +pub(crate) fn extract_docs(attrs: &[Attribute]) -> proc_macro2::TokenStream { + let docs: Vec<_> = attrs.iter().filter(is_doc).collect(); + quote!(#(#docs)*) +} + +/// Aborts on unexpected args to show that they arent valid +pub(crate) fn abort_unexpected_args(names: Vec<&str>, args: &[TokenTree]) { + for tk in args.iter() { + match tk { + TokenTree::Ident(v) if names.contains(&v.to_string().as_str()) => {} + TokenTree::Ident(v) => { + abort!( + v, + "Unknown argument `{}`, all known arguments are {:?}", + v, + names + ) + } + _ => {} + } + } +} + +pub(crate) fn gen_derive( + from_args: Option<&Vec> +) -> proc_macro2::TokenStream { + let mut derives: Vec = vec![]; + + match from_args { + Some(from_args) if !from_args.is_empty() => derives.push(quote!(#(#from_args),*)), + _ => {} + } + + quote!( + #[derive(#(#derives),*)] + ) +} + +#[cfg(feature = "openapi")] +pub(crate) fn has_oai_attribute(attrs: &[Attribute], containing: Option<&str>) -> bool { + attrs.iter() + .filter(|a| a.path().is_ident("oai")) + .filter(|a| + match containing { + Some(name) => + a.meta.require_list() + .expect("oai attribute usually has a list") + .tokens + .to_string() + .contains(name), + None => true + } + ) + .count() > 0 +} \ No newline at end of file diff --git a/src/logic/tests.rs b/src/logic/tests.rs new file mode 100644 index 0000000..a1b1bb1 --- /dev/null +++ b/src/logic/tests.rs @@ -0,0 +1,48 @@ +use super::*; +use quote::{quote, ToTokens}; +use syn::{parse2, DeriveInput}; + +fn gen_test_case_2() -> DeriveInput { + parse2(quote! { + #[derive(Models, Clone)] + #[view(UserUpdatables, omit(id), derives(Clone, core::fmt::Debug))] + struct User { + id: i32, + display_name: String, + bio: String, + password: String, + } + }) + .unwrap() +} + +fn get_view_args(attrs: Vec, index: usize) -> Vec { + let attr: Attribute = attrs + .iter() + .filter(|v| is_attribute(v, "view")) + .collect::>()[index] + .clone(); + + attr.meta + .require_list() + .unwrap() + .to_owned() + .tokens + .into_iter() + .collect::>()[2..] + .to_vec() +} + +#[test] +pub fn should_extract_derives() { + let ast = gen_test_case_2(); + let mut attr_args = get_view_args(ast.attrs, 0); + + let derives = take_path_group("derives", &mut attr_args).expect("Should of found derives"); + assert_eq!(derives[0].to_token_stream().to_string(), "Clone"); + assert_eq!( + derives[1].to_token_stream().to_string(), + "core :: fmt :: Debug" + ); + assert_eq!(derives.len(), 2); +} diff --git a/src/patch.rs b/src/patch.rs index 7b1d47b..e51d3bd 100644 --- a/src/patch.rs +++ b/src/patch.rs @@ -1,28 +1,26 @@ -use crate::*; - -use proc_macro2::{Group, Ident, TokenStream, TokenTree}; +use crate::logic::{ + args::{AttrArgs, OptionType}, + *, +}; +use proc_macro2::{Ident, TokenStream}; use proc_macro_error::abort; use quote::quote; use syn::{Attribute, DeriveInput, Type}; -struct PatchModelArgs { - name: Ident, - omit: Vec, - derives: Vec, - default_derives: bool, -} - -pub fn impl_patch_model( - ast: &DeriveInput, - attr: &Attribute, - oai_attr: &Vec<&Attribute>, -) -> TokenStream { - let PatchModelArgs { +pub fn impl_patch_model(ast: &DeriveInput, attr: &Attribute) -> TokenStream { + // Argument and Variable Initialization and Prep + let (args, mut remainder) = AttrArgs::parse(attr, false); + let AttrArgs { name, - omit, - derives, - default_derives, - } = parse_patch_arg(attr); + fields: _, + derive, + preset, + attributes_with, + } = args.clone(); + + let option = OptionType::parse(&mut remainder).unwrap_or_else(|| preset.option()); + + AttrArgs::abort_unexpected(&remainder, &["option"]); let original_name = &ast.ident; @@ -30,53 +28,42 @@ pub fn impl_patch_model( let mut fields_and_is_option: Vec<(&Ident, bool)> = vec![]; let mut fields: Vec<_> = vec![]; match &ast.data { - syn::Data::Struct(data) => data.fields.iter().for_each(|field| { - let field_attributes: &Vec = &field.attrs; - let field_name = &field.ident.as_ref().unwrap(); - - // Omit - if omit.contains(field_name) || has_attribute(field_attributes, "oai", "read_only") { - return; - } - - // Add - let docs = extract_docs(&field.attrs); - let oai_f_attributes: Vec<_> = field_attributes - .iter() - .filter(|attr| { - attr.meta - .path() - .segments - .first() - .map_or(false, |seg| seg.ident == "oai") - }) - .collect(); - let field_ty = &field.ty; - let option_ty = extract_type_from_option(field_ty); - - fields_and_is_option.push((field_name, option_ty.is_some())); - fields.push(impl_struct_fields( - field_name, - field_ty, - option_ty, - &docs, - &oai_f_attributes, - )); - }), + syn::Data::Struct(data) => data + .fields + .iter() + .filter(|f| { + preset.predicate(f) + && args + .fields + .predicate(f.ident.as_ref().expect("Field must be named")) + }) + .for_each(|field| { + let field_name = &field.ident.as_ref().unwrap(); + + // Add + let docs = extract_docs(&field.attrs); + let field_ty = &field.ty; + let option_ty = extract_type_from_option(field_ty); + + fields_and_is_option.push((field_name, option_ty.is_some())); + fields.push(impl_struct_fields( + field_name, field_ty, option_ty, &docs, option, + )); + }), _ => abort!(attr, "Patch Models can only be derived for structs"), }; - let derives = get_derive(default_derives, derives.iter().collect(), true); - let impl_from_derived = impl_from_derived(&fields_and_is_option); - let impl_merge = impl_merge(&fields_and_is_option); - let impl_weld_merge = impl_weld_merge(&impl_merge, original_name); + let attributes = attributes_with.gen_top_attributes(ast); + let derives = gen_derive(derive.as_ref()); + let impl_from_derived = impl_from_derived(&fields_and_is_option, option); + let impl_merge = impl_merge(&fields_and_is_option, option); // Generate the implementation of the PatchModel trait quote! { /// Generated patch model of [`#original_name`] #derives - #(#oai_attr)* + #(#attributes)* pub struct #name { #(#fields),* } @@ -101,9 +88,6 @@ pub fn impl_patch_model( pub fn merge_mut(self, mut value: &mut #original_name) { #impl_merge } - - #impl_weld_merge - } @@ -115,103 +99,79 @@ pub fn impl_patch_model( } } -fn impl_merge(fields: &[(&Ident, bool)]) -> TokenStream { - #[cfg(feature = "openapi")] - { - let option_field: Vec<_> = fields - .iter() - .filter_map(|(ident, is_option)| is_option.then_some(ident)) - .collect(); +fn impl_merge(fields: &[(&Ident, bool)], option: OptionType) -> TokenStream { + match option { + OptionType::MaybeUndefined => { + let option_field: Vec<_> = fields + .iter() + .filter_map(|(ident, is_option)| is_option.then_some(ident)) + .collect(); - let required_field: Vec<_> = fields - .iter() - .filter_map(|(ident, is_option)| (!is_option).then_some(ident)) - .collect(); + let required_field: Vec<_> = fields + .iter() + .filter_map(|(ident, is_option)| (!is_option).then_some(ident)) + .collect(); - quote! { - #( - match self.#required_field { - ::core::option::Option::Some(v) => value.#required_field = v, - ::core::option::Option::None => {}, - } - )* - #( - match self.#option_field { - ::poem_openapi::types::MaybeUndefined::Value(v) => value.#option_field = ::core::option::Option::Some(v), - ::poem_openapi::types::MaybeUndefined::Null => value.#option_field = ::core::option::Option::None, - ::poem_openapi::types::MaybeUndefined::Undefined => {}, - } - )* + quote! { + #( + match self.#required_field { + ::core::option::Option::Some(v) => value.#required_field = v, + ::core::option::Option::None => {}, + } + )* + #( + match self.#option_field { + ::poem_openapi::types::MaybeUndefined::Value(v) => value.#option_field = ::core::option::Option::Some(v), + ::poem_openapi::types::MaybeUndefined::Null => value.#option_field = ::core::option::Option::None, + ::poem_openapi::types::MaybeUndefined::Undefined => {}, + } + )* + } } - } - #[cfg(not(feature = "openapi"))] - { - let field: Vec<_> = fields.iter().map(|v| v.0).collect(); - quote! { - #( - match self.#field { - ::core::option::Option::Some(v) => value.#field = v, - ::core::option::Option::None => {}, - } - )* + OptionType::Option => { + let field: Vec<_> = fields.iter().map(|v| v.0).collect(); + quote! { + #( + match self.#field { + ::core::option::Option::Some(v) => value.#field = v, + ::core::option::Option::None => {}, + } + )* + } } } } -fn impl_from_derived(fields: &[(&Ident, bool)]) -> TokenStream { - #[cfg(feature = "openapi")] - { - let option_field: Vec<_> = fields - .iter() - .filter_map(|(ident, is_option)| is_option.then_some(ident)) - .collect(); - - let required_field: Vec<_> = fields - .iter() - .filter_map(|(ident, is_option)| (!is_option).then_some(ident)) - .collect(); - - quote! { - #( - #option_field: ::poem_openapi::types::MaybeUndefined::from_opt_undefined(value.#option_field), - )* - #( - #required_field: ::core::option::Option::Some(value.#required_field), - )* - } - } - #[cfg(not(feature = "openapi"))] - { - let field: Vec<_> = fields.iter().map(|v| v.0).collect(); - quote! { - #( - #field: ::core::option::Option::Some(value.#field), - )* - } - } -} +fn impl_from_derived(fields: &[(&Ident, bool)], option: OptionType) -> TokenStream { + match option { + OptionType::MaybeUndefined => { + let option_field: Vec<_> = fields + .iter() + .filter_map(|(ident, is_option)| is_option.then_some(ident)) + .collect(); -#[allow(unused_variables)] -fn impl_weld_merge(impl_merge: &TokenStream, original_name: &Ident) -> TokenStream { - #[cfg(feature = "welds")] - { - quote! { - /// Creates a new [`DbState`] from the given value, merging the updates into the given value, returning the updated value - pub fn merge_weld(self, mut value: ::welds::state::DbState<#original_name>) -> ::welds::state::DbState<#original_name> { - self.merge_weld_mut(&mut value); - value + let required_field: Vec<_> = fields + .iter() + .filter_map(|(ident, is_option)| (!is_option).then_some(ident)) + .collect(); + quote! { + #( + #option_field: ::poem_openapi::types::MaybeUndefined::from_opt_undefined(value.#option_field), + )* + #( + #required_field: ::core::option::Option::Some(value.#required_field), + )* } - - /// Mutable reference version of [`Self::merge_weld`] - pub fn merge_weld_mut(self, mut value: &mut ::welds::state::DbState<#original_name>) { - #impl_merge + } + OptionType::Option => { + let field: Vec<_> = fields.iter().map(|v| v.0).collect(); + quote! { + #( + #field: ::core::option::Option::Some(value.#field), + )* } } } - #[cfg(not(feature = "welds"))] - { - quote!() - } } fn impl_struct_fields( @@ -219,90 +179,30 @@ fn impl_struct_fields( field_ty: &Type, #[allow(unused_variables)] option_ty: Option<&Type>, docs: &TokenStream, - oai_f_attr: &Vec<&Attribute>, + option: OptionType, ) -> TokenStream { - #[cfg(feature = "openapi")] - { - match option_ty { + match option { + OptionType::MaybeUndefined => match option_ty { Some(t) => { quote! { #docs - #(#oai_f_attr)* pub #field_name: ::poem_openapi::types::MaybeUndefined<#t> } } _ => { quote! { #docs - #(#oai_f_attr)* pub #field_name: core::option::Option<#field_ty> } } + }, + OptionType::Option => { + quote! { + #docs + pub #field_name: core::option::Option<#field_ty> + } } } - #[cfg(not(feature = "openapi"))] - { - quote! { - #docs - #(#oai_f_attr)* - pub #field_name: core::option::Option<#field_ty> - } - } -} - -fn parse_patch_arg(attr: &Attribute) -> PatchModelArgs { - let tks = attr - .meta - .require_list() - .unwrap() - .to_owned() - .tokens - .into_iter() - .collect::>(); - - let name = match &tks[0] { - TokenTree::Ident(v) => v.clone(), - x => { - abort!( - x, - "First argument must be an identifier (name) of the struct for the view" - ) - } - }; - - if tks.len() < 3 { - return PatchModelArgs { - name, - omit: vec![], - derives: vec![], - default_derives: true, - }; - } - - let mut args_slice = tks[2..].to_vec(); - - let omit = parse_omit(&mut args_slice); - let derives = parse_derives(&mut args_slice); - let default_derives = parse_default_derives(&mut args_slice); - abort_unexpected_args(vec!["fields", "derive", "default_derives"], &args_slice); - - PatchModelArgs { - name, - omit, - derives, - default_derives, - } -} - -fn parse_omit(args: &mut Vec) -> Vec { - // Extract the fields args and ensuring it is a key-value pair of Ident and Group - let fields: Group = match take_ident_group("omit", args) { - Some(g) => g, - None => return vec![], - }; - - // Parse the fields argument into a TokenStream, skip checking for commas coz lazy - extract_idents(fields) } fn extract_type_from_option(ty: &Type) -> Option<&Type> { @@ -342,17 +242,3 @@ fn extract_type_from_option(ty: &Type) -> Option<&Type> { _ => None, }) } - -fn has_attribute(attrs: &[syn::Attribute], name: &str, value: &str) -> bool { - attrs.iter().any(|attr| { - let path = attr.path(); - let segments = &path.segments; - - segments.len() == 1 - && segments[0].ident == name - && attr - .meta - .require_list() - .map_or(false, |v| v.tokens.to_string() == value) - }) -} diff --git a/src/view.rs b/src/view.rs index 40a61ba..65be667 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,129 +1,51 @@ -use crate::*; -use proc_macro2::{Ident, TokenStream, TokenTree}; +use crate::logic::{args::AttrArgs, *}; +use proc_macro2::{Ident, TokenStream}; +use proc_macro_error::abort; use quote::quote; -use syn::{self, Attribute, DeriveInput}; - -struct ViewModelArgs { - name: Ident, - fields: Vec, - derives: Vec, - default_derives: bool, -} +use syn::{self, Attribute, DataEnum, DataStruct, DeriveInput}; pub fn impl_view_model( ast: &DeriveInput, - attr: &Attribute, - oai_attr: &Vec<&Attribute>, + attr: &Attribute ) -> TokenStream { - let ViewModelArgs { + // Argument and Variable Initialization and Prep + let (args, _) = AttrArgs::parse(attr, true); + let AttrArgs { name, - fields, - derives, - default_derives, - } = parse_view_attributes(attr); + fields: _, + derive, + preset: _, + attributes_with + } = args.clone(); - // // Filter attr to only copy the 'oai' ones over let original_name = &ast.ident; - let mut is_struct = true; - let mut field_from_mapping: Vec = vec![]; + let is_struct = matches!(&ast.data, syn::Data::Struct(_)); + let mut field_mapping: Vec = vec![]; // Will contain each fields `From` trait impl + // Generate Implementation let field_tokens: Vec<_> = match &ast.data { - syn::Data::Struct(data) => data - .fields - .iter() - .filter(|f| fields.contains(f.ident.as_ref().expect("Field(s) must be named"))) - .map(|field| { - let field_attr: &Vec = &field.attrs; - - let vis = &field.vis; - let docs = extract_docs(&field.attrs); - let oai_f_attributes: Vec<_> = extract_oai_f_attributes(field_attr); - let field_name = &field.ident.as_ref().unwrap(); - let field_ty = &field.ty; - - field_from_mapping.push(quote!(#field_name: value.#field_name)); - quote! { - #docs - #(#oai_f_attributes)* - #vis #field_name: #field_ty - } - }) - .collect(), - syn::Data::Enum(data) => { - is_struct = false; - data.variants - .iter() - .filter(|v| fields.contains(&v.ident)) - .map(|field| { - let oai_f_attr = get_oai_attributes(&field.attrs); - - let mut field_without_attrs = field.clone(); - field_without_attrs.attrs = vec![]; - - let docs = extract_docs(&field.attrs); - let field_name = &field.ident; - - match &field.fields { - syn::Fields::Unit => { - field_from_mapping.push(quote!{ - #name::#field_name => #original_name::#field_name - }); - }, - syn::Fields::Unnamed(_) => { - let variant_args: Vec<_> = field - .fields - .iter() - .enumerate() - .map(|(i, _)| { - let c = (i as u8 + b'a') as char; - // convert c to ident so it wont be quoted - let c= syn::Ident::new(&c.to_string(), proc_macro2::Span::call_site()); - - quote!(#c) - }) - .collect(); - field_from_mapping.push(quote!{ - #name::#field_name(#(#variant_args),*) => #original_name::#field_name(#(#variant_args),*) - }); - } - syn::Fields::Named(n) => { - let variant_args: Vec<_> = n - .named - .iter() - .map(|f| { - let arg = f.ident.as_ref(); - quote!(#arg) - }) - .collect(); - field_from_mapping.push(quote!{ - #name::#field_name{#(#variant_args),*} => #original_name::#field_name{#(#variant_args),*} - }); - }, - }; - - quote! { - #docs - #(#oai_f_attr)* - #field_without_attrs - } - }) - .collect() - } - _ => abort!(attr, "Patch Model can only be derived for structs & enums"), + syn::Data::Struct(data) => impl_for_struct(data, &mut field_mapping, &args), + syn::Data::Enum(data) => impl_for_enum(data, &mut field_mapping, &args, original_name), + syn::Data::Union(_) => abort!(attr, "Patch Model can only be derived for `struct` & `enum`, NOT `union`"), }; - let data_type = match is_struct { + let structure = match is_struct { true => quote!(struct), false => quote!(enum), }; - let derives = get_derive(default_derives, derives.iter().collect(), is_struct); - let impl_from = impl_from_trait(original_name, &name, field_from_mapping, is_struct); + + let attributes = attributes_with.gen_top_attributes(ast); + let derives = gen_derive(derive.as_ref()); + + let impl_from = impl_from_trait(original_name, &name, field_mapping, is_struct); + + let doc_string = format!("This is a restructured (View) model of ['{original_name}']. Refer to the original model for more structual documentation."); quote! { - /// A generated view of the original struct with only the specified fields + #[doc= #doc_string] #derives - #(#oai_attr)* - pub #data_type #name { + #(#attributes)* + pub #structure #name { #(#field_tokens),* } @@ -160,51 +82,105 @@ fn impl_from_trait( } } -fn parse_view_attributes(attr: &Attribute) -> ViewModelArgs { - let tks: Vec = attr - .meta - .require_list() - .unwrap() - .to_owned() - .tokens - .into_iter() - .collect::>(); - - let name = match &tks[0] { - TokenTree::Ident(v) => v.clone(), - x => { - abort!( - x, - "First argument must be an identifier (name) of the struct for the view" - ) - } - }; - if tks.len() < 3 { - abort!(attr, "Invalid syntax, expected at least one argument"); - } - let mut args_slice = tks[2..].to_vec(); - - let fields = parse_fields(&mut args_slice, attr); - let derives = parse_derives(&mut args_slice); - let default_derives = parse_default_derives(&mut args_slice); - abort_unexpected_args(vec!["fields", "derive", "default_derives"], &args_slice); - ViewModelArgs { - name, +fn impl_for_struct(data: &DataStruct, field_mapping: &mut Vec, args: &AttrArgs) -> Vec { + let AttrArgs { + name: _, fields, - derives, - default_derives, - } -} + derive: _, + preset, + attributes_with + } = args; -/// Parse a list of identifiers equal to fields we want in the model. Aborts if none are found. -fn parse_fields(args: &mut Vec, attr_spanned: &Attribute) -> Vec { - // Extract the fields args and ensuring it is a key-value pair of Ident and Group - let fields: Group = match take_ident_group("fields", args) { - Some(g) => g, - None => abort!(attr_spanned, "Missing args, expected `fields(...)"), - }; - // Parse the fields argument into a TokenStream, skip checking for commas coz lazy - extract_idents(fields) + data + .fields + .iter() + .filter(|f| { + preset.predicate(f) && fields.predicate(f.ident.as_ref().expect("Field must be named")) + }) + .map(|field| { + let vis = &field.vis; + let docs = extract_docs(&field.attrs); + let field_name = &field.ident.as_ref().unwrap(); + let field_ty = &field.ty; + + let field_attr = attributes_with.gen_field_attributes(field.attrs.clone()); + + field_mapping.push(quote!(#field_name: value.#field_name)); + quote! { + #docs + #(#field_attr)* + #vis #field_name: #field_ty + } + }) + .collect() } + +fn impl_for_enum(data: &DataEnum, field_mapping: &mut Vec, args: &AttrArgs, original_name: &Ident) -> Vec { + let AttrArgs { + name, + fields, + derive: _, + preset, + attributes_with + } = args; + + data.variants + .iter() + .filter(|v| fields.predicate(&v.ident)) + .map(|field| { + let mut field_impl = field.clone(); + field_impl.attrs = attributes_with.gen_field_attributes(field_impl.attrs); + + let docs = extract_docs(&field.attrs); + let field_name = &field.ident; + + match &field.fields { + syn::Fields::Unit => { + field_mapping.push(quote!{ + #name::#field_name => #original_name::#field_name + }); + }, + syn::Fields::Unnamed(_) => { + let variant_args: Vec<_> = field + .fields + .iter() + .filter(|f| preset.predicate(f)) + .enumerate() + .map(|(i, _)| { + let c = (i as u8 + b'a') as char; + let c= syn::Ident::new(&c.to_string(), proc_macro2::Span::call_site()); // convert char to ident so it wont be "quoted" + + quote!(#c) + }) + .collect(); + field_mapping.push(quote!{ + #name::#field_name(#(#variant_args),*) => #original_name::#field_name(#(#variant_args),*) + }); + } + syn::Fields::Named(n) => { + let variant_args: Vec<_> = n + .named + .iter() + .filter(|f| preset.predicate(f)) + .map(|f| { + + let arg = f.ident.as_ref(); + quote!(#arg) + }) + .collect(); + field_mapping.push(quote!{ + #name::#field_name{#(#variant_args),*} => #original_name::#field_name{#(#variant_args),*} + }); + }, + }; + + quote! { + #docs + #field_impl + } + }) + .collect() + +} \ No newline at end of file diff --git a/tests/clone.rs b/tests/clone.rs index 75cb847..ba0b549 100644 --- a/tests/clone.rs +++ b/tests/clone.rs @@ -1,12 +1,12 @@ -use restructed::Models; +// use restructed::Models; -#[derive(Models)] -#[clone(UserClone, derive(Clone))] -struct User { - /// This should be omitted - id: i32, - /// This shouldn't be omitted - display_name: String, - bio: String, - password: String, -} +// #[derive(Models)] +// #[clone(UserClone, derive(Clone))] +// struct User { +// /// This should be omitted +// id: i32, +// /// This shouldn't be omitted +// display_name: String, +// bio: String, +// password: String, +// } diff --git a/tests/feature_testing/builder.rs b/tests/feature_testing/builder.rs deleted file mode 100644 index 8a433e3..0000000 --- a/tests/feature_testing/builder.rs +++ /dev/null @@ -1,38 +0,0 @@ -use restructed::Models; - -#[derive(Models, Clone)] -#[patch(UserUpdatables, omit(id), default_derives = true)] -#[view(UserId, fields(id))] -struct User { - id: i32, - display_name: String, - bio: String, - password: String, -} - -impl User { - pub fn new() -> Self { - User { - id: 123, - display_name: "Cool Doode".to_string(), - bio: "I'm a cool doode, what can I say?".to_string(), - password: "Pls don't hack me".to_string(), - } - } -} - -#[test] -fn can_build() { - let user = User::new(); - - let id = UserId::builder() - .id(user.id) - .build(); - assert_eq!(&id.id, &user.id); - - UserUpdatables::builder() - .display_name(Some("Cooler doode".to_string())) - .bio(None) - .password(Some("Can't hack 'dis".to_string())) - .build(); -} \ No newline at end of file diff --git a/tests/feature_testing/openapi.rs b/tests/feature_testing/openapi.rs index 50f9165..62280f0 100644 --- a/tests/feature_testing/openapi.rs +++ b/tests/feature_testing/openapi.rs @@ -1,11 +1,14 @@ -use poem_openapi::{payload::Json, types::MaybeUndefined, ApiResponse}; +#![allow(dead_code)] + +use poem_openapi::{payload::Json, types::MaybeUndefined, ApiResponse, Object}; use restructed::Models; //---------- Structs // Should makes it impl poem's Object for structs -#[derive(Clone, Models)] -#[patch(UserUpdates, omit(id))] +#[derive(Clone, Object, Models)] +#[oai(skip_serializing_if_is_none, rename_all = "camelCase")] +#[patch(UserUpdates, omit(id), attributes_with = "oai", option = MaybeUndefined, derive(Object))] struct User { id: i32, display_name: Option, @@ -48,6 +51,25 @@ fn mapping_struct() { assert_eq!(updated_user.password, user.password); } +//---------- Structs - Preset Read/Write + +// Should makes it impl poem's Object for structs +#[derive(Clone, Debug, Object, Models, PartialEq, Eq)] +#[oai(skip_serializing_if_is_none, rename_all = "camelCase")] +#[patch(UserUpdatable, preset = "write", derive(Object))] +#[patch(UserViewables, preset = "read", derive(Object))] +#[view(UserUpdated, preset = "write", derive(Object))] +#[view(UserRecord, preset = "read", derive(Object))] +struct UserPre { + #[oai(read_only)] + id: i32, + display_name: Option, + bio: String, + extra: Option, + #[oai(write_only)] + password: Option, +} + //---------- Enums // There are many derives for enums in poem_openapi. @@ -56,7 +78,7 @@ fn mapping_struct() { #[view( ApiErrorReads, fields(NotFound, InternalServerError), - default_derives = false, // poem_openapi types don't implement stuff like PartialEq, this avoids errors + attributes_with = "oai", derive(ApiResponse, Clone) )] pub enum ApiError { diff --git a/tests/feature_testing/welds.rs b/tests/feature_testing/welds.rs deleted file mode 100644 index 7535fcc..0000000 --- a/tests/feature_testing/welds.rs +++ /dev/null @@ -1,48 +0,0 @@ -use restructed::Models; -use welds::state::DbState; - -#[derive(Models, Clone, PartialEq, Eq, Debug)] -#[patch(UserUpdatables, omit(id), default_derives = true)] -#[view(UserId, fields(id))] -struct User { - id: i32, - display_name: String, - bio: String, - password: String, -} - -impl User { - pub fn new() -> Self { - User { - id: 123, - display_name: "Cool Doode".to_string(), - bio: "I'm a cool doode, what can I say?".to_string(), - password: "Pls don't hack me".to_string(), - } - } -} - -#[test] -fn can_build() { - let user = User::new(); - - let id = UserId::builder() - .id(user.id) - .build(); - assert_eq!(&id.id, &user.id); - - let patch = UserUpdatables::builder() - .display_name(Some("Cooler doode".to_string())) - .bio(None) - .password(Some("Can't hack 'dis".to_string())) - .build(); - - let mut state = DbState::new_uncreated(user.clone()); - patch.merge_weld_mut(&mut state); - - - assert_ne!(*state, user); - assert_eq!(*state.display_name, "Cooler doode".to_string()); - assert_eq!(*state.bio, "I'm a cool doode, what can I say?".to_string()); - assert_eq!(*state.password, "Can't hack 'dis".to_string()); -} \ No newline at end of file diff --git a/tests/mod.rs b/tests/mod.rs index e246d1b..0cbf280 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,3 +1,3 @@ //! Entry point for integration tests. -mod feature_testing; +mod feature_testing; \ No newline at end of file diff --git a/tests/patch.rs b/tests/patch.rs index 11796d7..ed9d9e0 100644 --- a/tests/patch.rs +++ b/tests/patch.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] extern crate restructed; +use poem_openapi::types::MaybeUndefined; use restructed::Models; #[derive(Models, Clone)] @@ -8,7 +9,7 @@ use restructed::Models; struct User { id: i32, display_name: String, - bio: String, + bio: Option, password: String, } @@ -17,7 +18,7 @@ impl User { User { id: 123, display_name: "Cool Doode".to_string(), - bio: "I'm a cool doode, what can I say?".to_string(), + bio: Some("I'm a cool doode, what can I say?".to_string()), password: "Pls don't hack me".to_string(), } } @@ -39,3 +40,43 @@ fn omitted_only() { assert_eq!(user.bio, updated_user.bio); assert_ne!(user.password, updated_user.password); } + +//------------------ Structs - MaybeUndefined + + +#[derive(Models, Clone)] +#[patch(UserMaybes, omit(id), option = MaybeUndefined)] +struct UserAlt { + id: i32, + display_name: String, + bio: Option, + password: String, +} + +impl UserAlt { + pub fn new() -> Self { + UserAlt { + id: 123, + display_name: "Cool Doode".to_string(), + bio: Some("I'm a cool doode, what can I say?".to_string()), + password: "Pls don't hack me".to_string(), + } + } +} + +#[test] +fn alt_omitted_only() { + let user = UserAlt::new(); + + let maybes = UserMaybes { + display_name: Some("Cooler doode".to_string()), + bio: MaybeUndefined::Null, + password: Some("Can't hack 'dis".to_string()), + }; + + let updated_user = maybes.merge(user.clone()); + + assert_ne!(user.display_name, updated_user.display_name); + assert_ne!(user.bio, updated_user.bio); + assert_ne!(user.password, updated_user.password); +} diff --git a/tests/view.rs b/tests/view.rs index 1c10d5f..f10485e 100644 --- a/tests/view.rs +++ b/tests/view.rs @@ -50,6 +50,45 @@ fn only_fields() { assert_eq!(profile.bio, profile.bio); } +//------------------ Structs -- attributes_with = "all" + +#[derive(Models)] +#[view(UserProfileAll, fields(display_name, bio), attributes_with = "all")] +struct UserAttrAll { + /// This should be omitted + id: i32, + /// This shouldn't be omitted + display_name: String, + bio: String, + password: String, +} + +//------------------ Structs -- attributes_with = "deriveless" + +#[derive(Models)] +#[view(UserProfileDeriveless, fields(display_name, bio), attributes_with = "deriveless")] +struct UserAttrDeriveless { + /// This should be omitted + id: i32, + /// This shouldn't be omitted + display_name: String, + bio: String, + password: String, +} + +//------------------ Structs -- attributes_with = "none" + +#[derive(Models)] +#[view(UserProfileNone, fields(display_name, bio), attributes_with = "none")] +struct UserAttrNone{ + /// This should be omitted + id: i32, + /// This shouldn't be omitted + display_name: String, + bio: String, + password: String, +} + //------------------ Enums #[derive(Debug, Clone, Models)] From 5f507924bf6c1cbf8b68963a80f21264fc807a6b Mon Sep 17 00:00:00 2001 From: Callum Cunha Date: Mon, 11 Mar 2024 10:24:14 +0000 Subject: [PATCH 2/5] feat: added away to apply defaults --- src/lib.rs | 13 +++-- src/logic/args.rs | 119 +++++++++++++++++++++++++++++++++++++++------- src/patch.rs | 10 ++-- src/view.rs | 7 +-- tests/patch.rs | 13 +++++ tests/view.rs | 12 +++++ 6 files changed, 149 insertions(+), 25 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 48990d0..404bc85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,26 +6,33 @@ mod patch; mod view; use crate::logic::is_attribute; +use logic::args::AttrArgsDefaults; use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; #[proc_macro_error] -#[proc_macro_derive(Models, attributes(view, patch, clone))] +#[proc_macro_derive(Models, attributes(model, view, patch))] pub fn models(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as syn::DeriveInput); + let model_attr = ast.attrs + .iter() + .filter(|v| is_attribute(v, "model")) + .collect::>(); + let defaults = AttrArgsDefaults::parse(model_attr); + let views: Vec = ast .attrs .iter() .filter(|v| is_attribute(v, "view")) - .map(|a| view::impl_view_model(&ast, a)) + .map(|a| view::impl_view_model(&ast, a, &defaults)) .collect(); let patches: Vec = ast .attrs .iter() .filter(|v| is_attribute(v, "patch")) - .map(|a| patch::impl_patch_model(&ast, a)) + .map(|a| patch::impl_patch_model(&ast, a, &defaults)) .collect(); let gen = quote::quote!( diff --git a/src/logic/args.rs b/src/logic/args.rs index ce1241f..96d918f 100644 --- a/src/logic/args.rs +++ b/src/logic/args.rs @@ -1,10 +1,71 @@ use super::{ - abort_unexpected_args, extract_idents, has_oai_attribute, take_ident_group, take_ident_ident, take_ident_literal, take_path_group + abort_unexpected_args, extract_idents, has_oai_attribute, take_ident_group, take_ident_ident, + take_ident_literal, take_path_group, }; use proc_macro2::{Ident, TokenTree}; use proc_macro_error::abort; use syn::{Attribute, Field}; +#[derive(Clone)] +pub(crate) struct AttrArgsDefaults { + pub fields: Option, + pub derive: Option>, + pub preset: Option, + pub attributes_with: AttributesWith, // Has it's own None +} + +impl AttrArgsDefaults { + /// Parses the attribute and returns the parsed arguments (0) and any remaining (1) + pub(crate) fn parse(attr: Vec<&Attribute>) -> Self { + if attr.is_empty() { + return Self { + fields: None, + derive: None, + preset: None, + attributes_with: AttributesWith::None, + }; + } else if attr.len() > 1 { + abort!( + attr[1], + "Expected only one `model` attribute to derive defaults from." + ) + } + + let attr = attr.first().unwrap(); + + let mut tks: Vec = attr + .meta + .require_list() + .unwrap() + .to_owned() + .tokens + .into_iter() + .collect::>(); + let args = &mut tks; + + let fields = { + let fields = FieldsArg::parse(args, attr); + match fields.is_default() { + true => None, + false => Some(fields), + } + }; + let derive = take_path_group("derive", args); + let preset = Preset::parse(args); + let attributes_with = AttributesWith::parse(args).unwrap_or_else(|| match preset { + Some(p) => p.attr_with(), + None => AttributesWith::None, + }); + + Self { + fields, + derive, + preset, + attributes_with, + } + } +} + #[derive(Clone)] pub(crate) struct AttrArgs { pub name: Ident, @@ -24,7 +85,11 @@ impl AttrArgs { } /// Parses the attribute and returns the parsed arguments (0) and any remaining (1) - pub(crate) fn parse(attr: &Attribute, abort_unexpected: bool) -> (Self, Vec) { + pub(crate) fn parse( + attr: &Attribute, + defaults: &AttrArgsDefaults, + abort_unexpected: bool, + ) -> (Self, Vec) { let tks: Vec = attr .meta .require_list() @@ -44,19 +109,28 @@ impl AttrArgs { } }; - if tks.len() < 3 { - abort!(attr, "Invalid syntax, expected at least one argument"); - } - - let mut args = tks[2..].to_vec(); + let mut args = match tks.len() < 3 { + true => vec![], + false => tks[2..].to_vec(), + }; let args_mr = &mut args; // Parse Expected Macro Args - let fields = FieldsArg::parse(args_mr, attr); - let derive = take_path_group("derive", args_mr); - let preset = Preset::parse(args_mr).unwrap_or_default(); - let attributes_with = AttributesWith::parse(args_mr).unwrap_or_else(|| preset.attr_with()); - + let fields = { + let fields = FieldsArg::parse(args_mr, attr); + match &defaults.fields { + Some(f) if fields.is_default() => f.clone(), + _ => fields, + } + }; + + let derive = take_path_group("derive", args_mr).or(defaults.derive.clone()); + let preset = Preset::parse(args_mr).or(defaults.preset); + let attributes_with = AttributesWith::parse(args_mr).unwrap_or_else(|| match &preset { + Some(preset) => preset.attr_with(), + _ => defaults.attributes_with, + }); + if abort_unexpected { Self::abort_unexpected(&args, &[]) } @@ -66,8 +140,8 @@ impl AttrArgs { name, fields, derive, - preset, - attributes_with + preset: preset.unwrap_or_default(), + attributes_with, }, args, ) @@ -100,7 +174,7 @@ impl FieldsArg { match (field_arg, omit_args) { (Some(g), None) => Self::Fields(extract_idents(g)), (None, Some(g)) => Self::Omit(extract_idents(g)), - (None, None) => Self::Omit(vec![]), + (None, None) => Self::default(), (Some(_), Some(_)) => abort!( attr_spanned, "Cannot have both `fields` and `omit` arguments" @@ -108,6 +182,14 @@ impl FieldsArg { } } + /// Similar to an is_empty function but only checks if omit is empty as thats the default case + pub(crate) fn is_default(&self) -> bool { + match self { + Self::Omit(fields) => fields.is_empty(), + _ => false, + } + } + pub(crate) fn predicate(&self, field: &Ident) -> bool { match self { Self::Fields(fields) => fields.contains(field), @@ -116,6 +198,12 @@ impl FieldsArg { } } +impl Default for FieldsArg { + fn default() -> Self { + Self::Omit(vec![]) + } +} + #[derive(Debug, Clone, Copy, Default)] pub(crate) enum AttributesWith { #[default] @@ -218,7 +306,6 @@ impl OptionType { ), }) } - } #[derive(Debug, Clone, Copy, Default)] diff --git a/src/patch.rs b/src/patch.rs index e51d3bd..d2b22ff 100644 --- a/src/patch.rs +++ b/src/patch.rs @@ -1,5 +1,5 @@ use crate::logic::{ - args::{AttrArgs, OptionType}, + args::{AttrArgs, AttrArgsDefaults, OptionType}, *, }; use proc_macro2::{Ident, TokenStream}; @@ -7,9 +7,13 @@ use proc_macro_error::abort; use quote::quote; use syn::{Attribute, DeriveInput, Type}; -pub fn impl_patch_model(ast: &DeriveInput, attr: &Attribute) -> TokenStream { +pub fn impl_patch_model( + ast: &DeriveInput, + attr: &Attribute, + defaults: &AttrArgsDefaults, +) -> TokenStream { // Argument and Variable Initialization and Prep - let (args, mut remainder) = AttrArgs::parse(attr, false); + let (args, mut remainder) = AttrArgs::parse(attr, defaults, false); let AttrArgs { name, fields: _, diff --git a/src/view.rs b/src/view.rs index 65be667..87c7eb3 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,4 +1,4 @@ -use crate::logic::{args::AttrArgs, *}; +use crate::logic::{args::{AttrArgs, AttrArgsDefaults}, *}; use proc_macro2::{Ident, TokenStream}; use proc_macro_error::abort; use quote::quote; @@ -6,10 +6,11 @@ use syn::{self, Attribute, DataEnum, DataStruct, DeriveInput}; pub fn impl_view_model( ast: &DeriveInput, - attr: &Attribute + attr: &Attribute, + defaults: &AttrArgsDefaults ) -> TokenStream { // Argument and Variable Initialization and Prep - let (args, _) = AttrArgs::parse(attr, true); + let (args, _) = AttrArgs::parse(attr, defaults, true); let AttrArgs { name, fields: _, diff --git a/tests/patch.rs b/tests/patch.rs index ed9d9e0..b36d8d4 100644 --- a/tests/patch.rs +++ b/tests/patch.rs @@ -64,6 +64,19 @@ impl UserAlt { } } +//------------------ Structs -- defaults + +#[derive(Models)] +#[model(fields(display_name, bio), attributes_with = "none")] +#[patch(UserProfileDefaults)] +struct UserDefaults{ + id: i32, + display_name: String, + bio: String, + password: String, +} + + #[test] fn alt_omitted_only() { let user = UserAlt::new(); diff --git a/tests/view.rs b/tests/view.rs index f10485e..87e6f22 100644 --- a/tests/view.rs +++ b/tests/view.rs @@ -89,6 +89,18 @@ struct UserAttrNone{ password: String, } +//------------------ Structs -- defaults + +#[derive(Models)] +#[model(fields(display_name, bio), attributes_with = "none")] +#[view(UserProfileDefaults)] +struct UserDefaults{ + id: i32, + display_name: String, + bio: String, + password: String, +} + //------------------ Enums #[derive(Debug, Clone, Models)] From fa040a371262a902df55e2246a8ff6a047fb4065 Mon Sep 17 00:00:00 2001 From: Callum Cunha Date: Mon, 11 Mar 2024 14:33:35 +0000 Subject: [PATCH 3/5] chore: refactored arg parsing to be more encapsulated --- src/lib.rs | 9 ++- src/logic/args.rs | 200 +++++++++++++++++++++++++++++++++++----------- src/patch.rs | 4 +- src/view.rs | 6 +- tests/patch.rs | 24 +++++- tests/view.rs | 2 +- 6 files changed, 188 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 404bc85..5c0ee94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ mod patch; mod view; use crate::logic::is_attribute; -use logic::args::AttrArgsDefaults; +use logic::args::ModelAttrArgs; use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; @@ -18,21 +18,22 @@ pub fn models(input: TokenStream) -> TokenStream { let model_attr = ast.attrs .iter() .filter(|v| is_attribute(v, "model")) + .cloned() .collect::>(); - let defaults = AttrArgsDefaults::parse(model_attr); + let model_args = ModelAttrArgs::parse(model_attr); let views: Vec = ast .attrs .iter() .filter(|v| is_attribute(v, "view")) - .map(|a| view::impl_view_model(&ast, a, &defaults)) + .map(|a| view::impl_view_model(&ast, a, model_args.clone())) .collect(); let patches: Vec = ast .attrs .iter() .filter(|v| is_attribute(v, "patch")) - .map(|a| patch::impl_patch_model(&ast, a, &defaults)) + .map(|a| patch::impl_patch_model(&ast, a, model_args.clone())) .collect(); let gen = quote::quote!( diff --git a/src/logic/args.rs b/src/logic/args.rs index 96d918f..8dc5f4c 100644 --- a/src/logic/args.rs +++ b/src/logic/args.rs @@ -7,42 +7,89 @@ use proc_macro_error::abort; use syn::{Attribute, Field}; #[derive(Clone)] -pub(crate) struct AttrArgsDefaults { - pub fields: Option, - pub derive: Option>, - pub preset: Option, - pub attributes_with: AttributesWith, // Has it's own None +pub(crate) struct ModelAttrArgs { + pub base: Option, + pub defaults: Option, } -impl AttrArgsDefaults { - /// Parses the attribute and returns the parsed arguments (0) and any remaining (1) - pub(crate) fn parse(attr: Vec<&Attribute>) -> Self { +impl ModelAttrArgs { + /// Conditional aborts on unexpected args to show that they arent valid + pub(crate) fn abort_unexpected(args: &[TokenTree]) { + const EXPECTED: &[&str; 2] = &["base", "defaults"]; + abort_unexpected_args(EXPECTED.to_vec(), args); + } + + pub(crate) fn parse(attr: Vec) -> Self { if attr.is_empty() { return Self { - fields: None, - derive: None, - preset: None, - attributes_with: AttributesWith::None, + base: None, + defaults: None, }; } else if attr.len() > 1 { abort!( attr[1], - "Expected only one `model` attribute to derive defaults from." + "Invalid attribute, expected only one `model` attribute but got `{}`", + attr.len() ) } let attr = attr.first().unwrap(); - let mut tks: Vec = attr + let mut args: Vec = attr .meta .require_list() - .unwrap() + .expect("This attribute must be in a list format") .to_owned() .tokens .into_iter() .collect::>(); - let args = &mut tks; + let args_mr = &mut args; + + let base = take_ident_group("base", args_mr) + .map(|g| BaseAttrArgs::parse(&mut g.stream().into_iter().collect(), attr)); + + let defaults = take_ident_group("defaults", args_mr) + .map(|g| DefaultAttrArgs::parse(&mut g.stream().into_iter().collect(), attr)); + + Self::abort_unexpected(&args); + + Self { base, defaults } + } +} + +#[derive(Clone)] +pub(crate) struct BaseAttrArgs { + pub fields: Option, + pub derive: Option>, +} + +impl BaseAttrArgs { + /// Parses the attribute and returns the parsed arguments (0) and any remaining (1) + pub(crate) fn parse(args: &mut Vec, attr: &Attribute) -> Self { + let fields = { + let fields = FieldsArg::parse(args, attr); + match fields.is_default() { + true => None, + false => Some(fields), + } + }; + let derive = take_path_group("derive", args); + + Self { fields, derive } + } +} + +#[derive(Clone)] +pub(crate) struct DefaultAttrArgs { + pub fields: Option, + pub derive: Option>, + pub preset: Option, + pub attributes_with: AttributesWith, // Has it's own None +} +impl DefaultAttrArgs { + /// Parses the attribute and returns the parsed arguments (0) and any remaining (1) + pub(crate) fn parse(args: &mut Vec, attr: &Attribute) -> Self { let fields = { let fields = FieldsArg::parse(args, attr); match fields.is_default() { @@ -84,16 +131,16 @@ impl AttrArgs { abort_unexpected_args(expect, args) } - /// Parses the attribute and returns the parsed arguments (0) and any remaining (1) + /// Parses the attribute and returns the parsed arguments as `Self` (0) and any arguments remaining unparsed (1) pub(crate) fn parse( attr: &Attribute, - defaults: &AttrArgsDefaults, + model_args: ModelAttrArgs, abort_unexpected: bool, ) -> (Self, Vec) { let tks: Vec = attr .meta .require_list() - .unwrap() + .expect("This attribute must be in a list format") .to_owned() .tokens .into_iter() @@ -113,23 +160,13 @@ impl AttrArgs { true => vec![], false => tks[2..].to_vec(), }; - let args_mr = &mut args; - - // Parse Expected Macro Args - let fields = { - let fields = FieldsArg::parse(args_mr, attr); - match &defaults.fields { - Some(f) if fields.is_default() => f.clone(), - _ => fields, - } - }; - let derive = take_path_group("derive", args_mr).or(defaults.derive.clone()); - let preset = Preset::parse(args_mr).or(defaults.preset); - let attributes_with = AttributesWith::parse(args_mr).unwrap_or_else(|| match &preset { - Some(preset) => preset.attr_with(), - _ => defaults.attributes_with, - }); + let fields = FieldsArg::parse_with_args(&mut args, &model_args, attr); + let derive = parse_derives_wtih_args(&mut args, &model_args); + let preset = Preset::parse_with_args(&mut args, &model_args); + let attributes_with = + AttributesWith::parse_with_args(&mut args, &model_args, preset.as_ref()) + .unwrap_or_default(); if abort_unexpected { Self::abort_unexpected(&args, &[]) @@ -182,6 +219,38 @@ impl FieldsArg { } } + pub(crate) fn parse_with_args( + args: &mut Vec, + model_args: &ModelAttrArgs, + attr: &Attribute, // Just for its span and error highlighting purposes + ) -> Self { + use FieldsArg::*; + let fields = FieldsArg::parse(args, attr); + + let default_fields = model_args.defaults.as_ref().and_then(|v| v.fields.clone()); + let fields = match &default_fields { + Some(f) if fields.is_default() => f.clone(), + _ => fields, + }; + + let base_fields = model_args.base.as_ref().and_then(|v| v.fields.clone()); + if let Some(base) = base_fields { + let final_fields: Vec<_> = match (fields, base) { + (Fields(f), Fields(b)) => f.into_iter().filter(|v| b.contains(v)).collect(), + (Fields(f), Omit(b)) => f.into_iter().filter(|v| !b.contains(v)).collect(), + (Omit(f), Fields(b)) => b.into_iter().filter(|v| !f.contains(v)).collect(), + (Omit(f), Omit(mut b)) => { + let not_in_base = f.into_iter().filter(|v| !b.contains(v)).collect::>(); + b.extend(not_in_base); + b + } + }; + Fields(final_fields) + } else { + fields + } + } + /// Similar to an is_empty function but only checks if omit is empty as thats the default case pub(crate) fn is_default(&self) -> bool { match self { @@ -226,14 +295,25 @@ impl AttributesWith { "all" => Self::All, #[cfg(feature = "openapi")] v => abort!( - ident, - "Invalid value, expected `none`, `oai` (from poem_openapi crate), `deriveless`, or `all` but got `{}`", v - ), + ident, + "Invalid value, expected `none`, `oai` (from poem_openapi crate), `deriveless`, or `all` but got `{}`", v + ), #[cfg(not(feature = "openapi"))] v => abort!( - ident, - "Invalid value, expected `none`, `deriveless`, or `all` but got `{}`", v - ), + ident, + "Invalid value, expected `none`, `deriveless`, or `all` but got `{}`", v + ), + }) + } + + pub(crate) fn parse_with_args( + args: &mut Vec, + model_args: &ModelAttrArgs, + preset: Option<&Preset>, + ) -> Option { + AttributesWith::parse(args).or_else(|| match preset { + Some(preset) => Some(preset.attr_with()), + None => model_args.defaults.as_ref().map(|f| f.attributes_with), }) } @@ -331,16 +411,22 @@ impl Preset { "write" => Self::Write, #[cfg(feature = "openapi")] v => abort!( - ident, - "Invalid value, expected `none` or `read`/`write` (with `openapi` feature) but got `{}`", v - ), + ident, + "Invalid value, expected `none` or `read`/`write` (with `openapi` feature) but got `{}`", v + ), #[cfg(not(feature = "openapi"))] v => abort!( - ident, - "Invalid value, expected `none` but got `{}`", v - ), + ident, + "Invalid value, expected `none` but got `{}`", v + ), }) } + pub(crate) fn parse_with_args( + args: &mut Vec, + model_args: &ModelAttrArgs, + ) -> Option { + Preset::parse(args).or(model_args.defaults.as_ref().and_then(|f| f.preset)) + } pub(crate) fn predicate(&self, field: &Field) -> bool { match self { @@ -368,3 +454,23 @@ impl Preset { } } } + +/// Parses the `derive` attribute and returns the parsed arguments as a Vec of `syn::Path` if the argument was given +fn parse_derives_wtih_args( + args: &mut Vec, + model_args: &ModelAttrArgs, +) -> Option> { + let base_derives = model_args.base.as_ref().and_then(|v| v.derive.clone()); + let default_derives = model_args.defaults.as_ref().and_then(|v| v.derive.clone()); + + let derives = take_path_group("derive", args).or(default_derives.clone()); + match (derives, base_derives) { + (Some(d), Some(mut b)) => { + b.extend(d); + Some(b) + } + (Some(g), None) => Some(g), + (None, Some(b)) => Some(b), + (None, None) => None, + } +} diff --git a/src/patch.rs b/src/patch.rs index d2b22ff..2f12eec 100644 --- a/src/patch.rs +++ b/src/patch.rs @@ -1,5 +1,5 @@ use crate::logic::{ - args::{AttrArgs, AttrArgsDefaults, OptionType}, + args::{AttrArgs, ModelAttrArgs, OptionType}, *, }; use proc_macro2::{Ident, TokenStream}; @@ -10,7 +10,7 @@ use syn::{Attribute, DeriveInput, Type}; pub fn impl_patch_model( ast: &DeriveInput, attr: &Attribute, - defaults: &AttrArgsDefaults, + defaults: ModelAttrArgs, ) -> TokenStream { // Argument and Variable Initialization and Prep let (args, mut remainder) = AttrArgs::parse(attr, defaults, false); diff --git a/src/view.rs b/src/view.rs index 87c7eb3..f25a7f6 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,13 +1,15 @@ -use crate::logic::{args::{AttrArgs, AttrArgsDefaults}, *}; +use crate::logic::{args::AttrArgs, *}; use proc_macro2::{Ident, TokenStream}; use proc_macro_error::abort; use quote::quote; use syn::{self, Attribute, DataEnum, DataStruct, DeriveInput}; +use self::args::ModelAttrArgs; + pub fn impl_view_model( ast: &DeriveInput, attr: &Attribute, - defaults: &AttrArgsDefaults + defaults: ModelAttrArgs ) -> TokenStream { // Argument and Variable Initialization and Prep let (args, _) = AttrArgs::parse(attr, defaults, true); diff --git a/tests/patch.rs b/tests/patch.rs index b36d8d4..6353e1b 100644 --- a/tests/patch.rs +++ b/tests/patch.rs @@ -67,7 +67,7 @@ impl UserAlt { //------------------ Structs -- defaults #[derive(Models)] -#[model(fields(display_name, bio), attributes_with = "none")] +#[model(defaults(fields(display_name, bio), attributes_with = "none"))] #[patch(UserProfileDefaults)] struct UserDefaults{ id: i32, @@ -76,6 +76,28 @@ struct UserDefaults{ password: String, } +//------------------ Structs -- base +#[derive(Models)] +#[model(base(fields(display_name, bio), attributes_with = "none"))] +#[patch(UserProfileBase)] +struct UserBase { + id: i32, + display_name: String, + bio: String, + password: String, +} + +//------------------ Structs -- base & defaults mix +#[derive(Models)] +#[model(base(fields(bio, display_name), attributes_with = "none"), defaults(omit(display_name)))] +#[patch(UserProfileMix)] +struct UserMix { + id: i32, + display_name: String, + bio: String, + password: String, +} + #[test] fn alt_omitted_only() { diff --git a/tests/view.rs b/tests/view.rs index 87e6f22..a113e4a 100644 --- a/tests/view.rs +++ b/tests/view.rs @@ -92,7 +92,7 @@ struct UserAttrNone{ //------------------ Structs -- defaults #[derive(Models)] -#[model(fields(display_name, bio), attributes_with = "none")] +#[model(defaults(fields(display_name, bio), attributes_with = "none"))] #[view(UserProfileDefaults)] struct UserDefaults{ id: i32, From 7963438202ba1dbbfa63ef391fcbea2250769447 Mon Sep 17 00:00:00 2001 From: Callum Cunha Date: Mon, 11 Mar 2024 16:51:41 +0000 Subject: [PATCH 4/5] Updated docs --- readme.md | 191 +++++++++++++++++++++++++++++++-------------------- tests/mod.rs | 4 +- 2 files changed, 119 insertions(+), 76 deletions(-) diff --git a/readme.md b/readme.md index 610bd28..6c7f5d9 100644 --- a/readme.md +++ b/readme.md @@ -72,47 +72,44 @@ struct User { Continue reading for the available models and their breakdown and arguments in general. -## Common Arguments -These are arguments that can be applied to all models. - -- `name` - The name of the struct the generate [**Required**, **Must be first** e.g. `MyStruct`] -- `fields` [**Only One**] - - `fields` - A _list_ of field names in the original structure to carry over [e.g. `fields(field1, field2, ...)`] - - `omit` - A _list_ of field names in the original structure to omit [e.g. `omit(field1, field2, ...)`] -- `derive` - A _list_ of derivables to derive on the generated struct just like you would normally [e.g. `derive(Clone, Debug, thiserror::Error)`] -- `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** - **Default**, does nothing and is the default behaviour - - **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` +# Models +These are arguments that can be applied there respective Attributes (i.e. `#[attr(args...)])`). - - **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` +## `#[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 on itself. -- `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 consider any attributes [**Default**] - - **oai** *['openapi' Feature Flag]* - Composes all Poem's OpenAPI attributes - - **deriveless** - Composes all attributes but omits the derive attributes - - **all** - Composes all attributes +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(...)]` -# Models -Derivable models via the struct attributes. +| 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. -## **Patch** -A subset model where every field's type is an option in some way. It's called patch because it reflect a REST / DB patch of the original struct. +e.g. `#[model(defaults(...))]` -#### **Unique Args** -- `option` - A _Identifer_ that allows a different option implementation from supported crates [e.g. `option = MaybeUndefined` (from poem-openapi)] - - **Option** - The standard implementation of Option [**Default**] - - **MaybeUndefined** *['openapi' Feature Flag]* - Use Poem's OpenAPI crate and it's Option implmentation +| 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** + +### Example ```rust -#[derive(restructed::Models)] -#[patch(UserUpdate, omit(id), option = Option)] +#[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, @@ -120,83 +117,129 @@ struct User { extra: Option, password: String, } -``` -will expand into something like this: -```rust -struct UserUpdate { - display_name: Option, - bio: Option, - extra: Option>, - password: Option, +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); } ``` -## **View** -A simple subset of the deriving model/struct. +## `#[view]` +A model with generates a subset of the original/parent deriving `Model`. Useful for creating things like RESTful API or Database views. -#### **Unique Args** -- N/A +| 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"` | -#### **Example** ```rust -#[derive(restructed::Models)] -#[view(UserPublic, fields(display_name, bio))] +#[derive(Clone, restructed::Models)] +#[view(UserProfile, omit(id, password))] struct User { - id: i32, + id: i32, // Not in `UserProfile` display_name: String, bio: String, extra: Option, - password: String, + password: String, // Not in `UserProfile` } ``` -will expand into something like this: +## `#[patch]` +A model which creates subsets of your data except each field's type is wrapped in a `Option` 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` to wrap fields with | False | Option/MaybeUndefined | `option = MaybeUndefined` | + ```rust -struct UserPublic { - display_name: String, - bio: String, +#[derive(Clone, restructed::Models)] +#[patch(UserUpdate, fields(display_name, bio, extra, password))] +struct User { + id: i32, // Not in `UserUpdate` + display_name: String, // Option in `UserUpdate` + bio: String, // Option in `UserUpdate` + extra: Option, // Option> in `UserUpdate` (If this isn't desired, see *option* arg and the *openapi* crate feature) + password: String, // Not in `UserProfile` } ```
-# Complex Example -Just to demonstrate the versitility of this crate, here is an example using all the possible arguments at once using all features. +# Argument Behaviours -## Poem OpenAPI -Each attribute is copied over so all your validations are kept +## `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`). 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` from the source struct with `MaybeUndefined` from the [poem-openapi](https://github.com/poem-web/poem/tree/master/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.. ```rust use restructed::Models; -#[cfg(test)] // For rust_doc test purposes -#[derive(poem::Object, Models)] +#[derive(poem_openapi::Object, Models)] #[oai(skip_serializing_if_is_none, rename_all = "camelCase")] -#[patch(UserUpdate, preset = "write", derive(Object))] -#[view(UserProfile, preset = "view", derive(Object))] -#[view(UserNames, preset = "view", derive(Object))] +#[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(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, #[oai(validator(min_length = 2, max_length = 16, pattern = r"^[a-zA-Z\s]*$"))] - pub surname: Option, + pub surname: Option, // in patch modeels, this is `MaybeUndefined` type with default with preset `read` or `write` (or option = MaybeUndefined) #[oai(read_only)] pub joined: u64, } -``` - -
- -# Crate Features -Links are to other crates GitHub page that are related to the features.
- -## Poem OpenAPI -Enables wrapping `Option` from the source struct with `MaybeUndefined` from the [poem-openapi](https://github.com/poem-web/poem/tree/master/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.. \ No newline at end of file +``` \ No newline at end of file diff --git a/tests/mod.rs b/tests/mod.rs index 0cbf280..d81da79 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,3 +1,3 @@ -//! Entry point for integration tests. +//! Entry point for integration/feature tests. +mod feature_testing; -mod feature_testing; \ No newline at end of file From ca6df391948b6f9022313f41d5e3d0dacdc7cab7 Mon Sep 17 00:00:00 2001 From: Callum Cunha Date: Mon, 11 Mar 2024 16:53:44 +0000 Subject: [PATCH 5/5] Updated version and removed license file --- Cargo.toml | 5 ++--- license.txt | 24 ------------------------ 2 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 license.txt diff --git a/Cargo.toml b/Cargo.toml index 15f19d7..b4cd2bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,13 @@ [package] name = "restructed" -version = "0.1.4" +version = "0.2.0" edition = "2021" readme = "readme.md" authors = ["Nex "] description = "Quickly derive subsets of your structs" -keywords = ["structs", "views", "openapi", "generated", "derived"] +keywords = ["view", "patch", "openapi", "derive", "restructed"] categories = ["derive-macro", "structs"] license = "Unlicense" -license-file = "license.txt" [lib] proc-macro = true diff --git a/license.txt b/license.txt deleted file mode 100644 index 3c577b0..0000000 --- a/license.txt +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to \ No newline at end of file