diff --git a/Cargo.toml b/Cargo.toml index df2734e..bad96a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ exclude = [ [workspace.package] authors = ["Antonio Yang "] -version = "0.12.0" +version = "0.12.1" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/README.md b/README.md index f8f4950..1d8f111 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ Struct attributes: - `#[patch(attribute(...))]`: add attributes to the generated patch struct. - `#[patch(attribute(derive(...)))]`: add derives to the generated patch struct. - `#[catalyst(bind = "...")]`: decide the base structure. (catalyst feature) +- `#[catalyst(keep_field_attribute)]`: the attributes of fields in substrate will also pass to complex. (catalyst feature) - `#[complex(name = "...")]`: change the name of the generated complex struct. (catalyst feature) - `#[complex(attribute(...))]`: add attributes to the generated complex struct. (catalyst feature) @@ -171,6 +172,7 @@ Field attributes: - `#[patch(empty_value = ...)]`: define a value as empty, so the corresponding field of patch will not wrapped by Option, and apply patch when the field is empty. - `#[filler(extendable)]`: use the field of the struct for filler, the struct needs implement `Default`, `Extend`, `IntoIterator` and `is_empty`. - `#[filler(empty_value = ...)]`: define a value as empty, so the corresponding field of Filler will be applied, even the field is not `Option` or `Extendable`. +- `#[complex(attribute(...))]`: add attributes to the field in the generated complex struct. (catalyst feature) Please check the [traits][doc-traits] of document to learn more. diff --git a/complex-example/Cargo.toml b/complex-example/Cargo.toml index bfa947c..c5f9e00 100644 --- a/complex-example/Cargo.toml +++ b/complex-example/Cargo.toml @@ -9,7 +9,7 @@ struct-patch = { path = "../lib", features = ["catalyst"] } [workspace.package] authors = ["Antonio Yang "] -version = "0.12.0" +version = "0.12.1" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/complex-example/catalyst/Cargo.toml b/complex-example/catalyst/Cargo.toml index 409b5f8..f36d943 100644 --- a/complex-example/catalyst/Cargo.toml +++ b/complex-example/catalyst/Cargo.toml @@ -14,6 +14,8 @@ rust-version.workspace = true [dependencies] struct-patch.workspace = true substrate = { path = "../substrate" } +serde = { version = "1", features = ["derive"] } +toml = "1.1.2+spec-1.1.0" [build-dependencies] struct-patch.workspace = true diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index aba2d89..2bc0bea 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,15 +1,25 @@ +use serde::{Deserialize, Serialize}; use struct_patch::Catalyst; use substrate::Base; #[derive(Default, Catalyst)] #[catalyst(bind = Base)] +// The Substrate has `#[serde(...)]` on fields , and catalyst keep_field_attribute +// so the complex should have the corresponding Serialize derive +#[catalyst(keep_field_attribute)] +#[complex(attribute(derive(Debug, Deserialize, Serialize)))] #[allow(dead_code)] struct Amyloid { pub extra_bool: bool, + #[complex(attribute(serde(default = "default_str")))] pub extra_string: String, pub extra_option: Option, } +fn default_str() -> String { + "default".to_string() +} + #[derive(Catalyst)] #[catalyst(bind = Base)] #[complex(name = "SmallCpx")] @@ -48,5 +58,21 @@ mod tests { let amyloid = Amyloid::default(); let complex = amyloid.bind(substrate); assert_eq!(complex.field_bool, true); + + let toml_str = toml::to_string_pretty(&complex).unwrap(); + assert_eq!( + toml_str, + r#"field_bool = true +field_string = "" +extra_bool = false +extra_string = "" +"# + ); + let toml_str = r#" field_bool = true +field_string = "" +extra_bool = true + "#; + let complex: AmyloidComplex = toml::from_str(toml_str).unwrap(); + assert_eq!(complex.extra_string, "default"); } } diff --git a/complex-example/substrate/Cargo.toml b/complex-example/substrate/Cargo.toml index e7b8148..f39a444 100644 --- a/complex-example/substrate/Cargo.toml +++ b/complex-example/substrate/Cargo.toml @@ -13,6 +13,7 @@ rust-version.workspace = true [dependencies] struct-patch.workspace = true +serde = { version = "1", features = ["derive"] } [dev-dependencies] syn = { version = "2", features = ["full"] } diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 88166f5..6716e84 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -1,8 +1,10 @@ #![allow(unused)] +use serde::Deserialize; use struct_patch::Substrate; -#[derive(Default, Substrate)] +#[derive(Deserialize, Default, Substrate)] pub struct Base { + #[serde(default)] pub field_bool: bool, pub field_string: String, pub field_option: Option, @@ -22,7 +24,7 @@ mod tests { fn expose_works() { assert_eq!( Base::expose_content(), - r#"{"named":[{"vis":"pub","ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"vis":"pub","ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"vis":"pub","ident":"field_option","colon_token":true,"ty":{"path":{"segments":[{"ident":"Option","arguments":{"angle_bracketed":{"args":[{"type":{"path":{"segments":[{"ident":"usize"}]}}}]}}}]}}}]}"# + r#"{"named":[{"attrs":[{"style":"outer","meta":{"list":{"path":{"segments":[{"ident":"serde"}]},"delimiter":"paren","tokens":[{"ident":"default"}]}}}],"vis":"pub","ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"vis":"pub","ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"vis":"pub","ident":"field_option","colon_token":true,"ty":{"path":{"segments":[{"ident":"Option","arguments":{"angle_bracketed":{"args":[{"type":{"path":{"segments":[{"ident":"usize"}]}}}]}}}]}}}]}"# ); let _fields: syn::Fields = syn_serde::json::from_str(&Base::expose_content()).unwrap(); diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 43c4973..6258220 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -2,7 +2,7 @@ extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; -use syn::{meta::ParseNestedMeta, parenthesized, DeriveInput, LitStr, Result, Type}; +use syn::{Attribute, meta::ParseNestedMeta, parenthesized, DeriveInput, LitStr, Result, Type}; pub(crate) struct Catalyst { visibility: syn::Visibility, @@ -12,9 +12,12 @@ pub(crate) struct Catalyst { attributes: Vec, fields: syn::Fields, bind: String, + keep_field_attribute: bool, } struct Field { + attrs: Vec, + attributes: Vec, ident: Option, ty: Type, } @@ -24,6 +27,8 @@ const COMPLEX: &str = "complex"; const BIND: &str = "bind"; const NAME: &str = "name"; const ATTRIBUTE: &str = "attribute"; +// TODO #[catalyst(keep_field_attrs = [ "serde" ])] for more detail selections +const KEEP_FIELD_ATTRIBUTE: &str = "keep_field_attribute"; impl Catalyst { /// let catalyst bind the substrate and generate the token stream for complex @@ -36,6 +41,7 @@ impl Catalyst { attributes, fields, bind, + keep_field_attribute, } = self; let substrate_name: Ident = { @@ -57,13 +63,13 @@ impl Catalyst { } for field in fields.iter() { - raw_complex_fields.push(Field::from_ast(field.clone())); - catalyst_fields.push(Field::from_ast(field.clone())); + raw_complex_fields.push(Field::from_cat_ast(field.clone())); + catalyst_fields.push(Field::from_cat_ast(field.clone())); } let complex_fields = raw_complex_fields .iter() - .map(|f| f.to_token_stream()) + .map(|f| f.to_token_stream(*keep_field_attribute)) .collect::>>()?; let unpack_complex_fields = raw_complex_fields @@ -153,6 +159,7 @@ impl Catalyst { let mut name = None; let mut attributes = vec![]; let mut bind = String::new(); + let mut keep_field_attribute = false; for attr in attrs { let attr_str = attr.path().to_string(); @@ -194,6 +201,10 @@ impl Catalyst { } } } + KEEP_FIELD_ATTRIBUTE if attr_str == CATALYST => { + // #[catalyst(KEEP_FIELD_ATTRIBUTE )] + keep_field_attribute = true; + } _ => { return Err(meta.error(format_args!( "unknown catalyst/complex attribute `{}`", @@ -222,30 +233,74 @@ impl Catalyst { attributes, fields, bind, + keep_field_attribute, }) } } impl Field { /// Generate the token stream for the Complex struct fields - pub fn to_token_stream(&self) -> Result { - let Field { ident, ty } = self; - Ok(quote! { - pub #ident: #ty, - }) + pub fn to_token_stream(&self, keep_field_attribute: bool) -> Result { + let Field { + attrs, + attributes, + ident, + ty, + } = self; + if keep_field_attribute { + if attrs.len() > 0 { + Ok(quote! { + #( #attrs )* + pub #ident: #ty, + }) + } else { + Ok(quote! { + #( #[ #attributes ] )* + pub #ident: #ty, + }) + } + } else { + Ok(quote! { + pub #ident: #ty, + }) + } } /// Generate the token stream for unpack Complex struct fields pub fn to_unpack_stream(&self) -> Result { - let Field { ident, ty: _ } = self; + let Field { ident, ty: _, attributes: _, attrs: _ } = self; Ok(quote! { #ident, }) } /// Parse the Catalyst struct field - pub fn from_ast(syn::Field { ident, ty, .. }: syn::Field) -> Field { - Field { ident, ty } + pub fn from_cat_ast(syn::Field { ident, ty, attrs, .. }: syn::Field) -> Field { + let mut attributes = Vec::new(); + for attr in attrs.iter() { + let attr_str = attr.path().to_string(); + if attr_str != COMPLEX { + continue; + } + let _ = attr.parse_nested_meta(|meta| { + let path = meta.path.to_string(); + match path.as_str() { + ATTRIBUTE => { + // #[complex(attribute(serde(default))] + let content; + parenthesized!(content in meta.input); + let attribute: TokenStream = content.parse()?; + attributes.push(attribute); + } + _ => {} + } + Ok(()) + }); + } + Field { ident, ty, attributes, attrs: Vec::new() } + } + pub fn from_ast(syn::Field { ident, ty, attrs, .. }: syn::Field) -> Field { + Field { ident, ty, attrs, attributes: Vec::new() } } } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 176c7a2..711aeb6 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,7 +12,7 @@ readme.workspace = true rust-version.workspace = true [dependencies] -struct-patch-derive = { version = "=0.12.0", path = "../derive" } +struct-patch-derive = { version = "=0.12.1", path = "../derive" } [dev-dependencies] serde_json = "1.0" diff --git a/no-std-examples/Cargo.toml b/no-std-examples/Cargo.toml index 538ad68..3fdf6fd 100644 --- a/no-std-examples/Cargo.toml +++ b/no-std-examples/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "no-std-examples" authors = ["Antonio Yang "] -version = "0.12.0" +version = "0.12.1" edition = "2021" license = "MIT"