From 17baacd328d9f65812f53353ca8048e39411311e Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sat, 16 May 2026 00:51:23 +0800 Subject: [PATCH 1/2] catalyst: add keep_field_attribute Also passing the field attributes to the complex struct --- Cargo.toml | 2 +- README.md | 1 + complex-example/Cargo.toml | 2 +- complex-example/catalyst/Cargo.toml | 2 ++ complex-example/catalyst/src/lib.rs | 16 +++++++++++ complex-example/substrate/Cargo.toml | 1 + complex-example/substrate/src/lib.rs | 7 +++-- derive/src/catalyst.rs | 42 +++++++++++++++++++++------- lib/Cargo.toml | 2 +- no-std-examples/Cargo.toml | 2 +- 10 files changed, 61 insertions(+), 16 deletions(-) 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..1bd1224 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) 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..aabc761 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,8 +1,16 @@ use struct_patch::Catalyst; use substrate::Base; +use serde::Serialize; + #[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(Serialize)))] + #[allow(dead_code)] struct Amyloid { pub extra_bool: bool, @@ -48,5 +56,13 @@ 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 = "" +"#); + } } 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..bab46ba 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -1,8 +1,11 @@ #![allow(unused)] use struct_patch::Substrate; +use serde::Deserialize; -#[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 +25,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..664ede0 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,11 @@ pub(crate) struct Catalyst { attributes: Vec, fields: syn::Fields, bind: String, + keep_field_attribute: bool, } struct Field { + attrs: Vec, ident: Option, ty: Type, } @@ -24,6 +26,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 +40,7 @@ impl Catalyst { attributes, fields, bind, + keep_field_attribute, } = self; let substrate_name: Ident = { @@ -63,7 +68,7 @@ impl Catalyst { 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 +158,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 +200,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 +232,42 @@ 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, + ident, + ty, + } = self; + if keep_field_attribute { + Ok(quote! { + #(#attrs)* + 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: _, 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_ast(syn::Field { ident, ty, attrs, .. }: syn::Field) -> Field { + Field { ident, ty, attrs } } } 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" From deae038bcdd8a4156b3c132df2b9887b98a23614 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sat, 16 May 2026 23:26:33 +0800 Subject: [PATCH 2/2] catalyst: add #[complex(attribute(...))] --- README.md | 1 + complex-example/catalyst/src/lib.rs | 26 +++++++++----- complex-example/substrate/src/lib.rs | 3 +- derive/src/catalyst.rs | 53 ++++++++++++++++++++++------ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1bd1224..1d8f111 100644 --- a/README.md +++ b/README.md @@ -172,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/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index aabc761..2bc0bea 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,23 +1,25 @@ +use serde::{Deserialize, Serialize}; use struct_patch::Catalyst; use substrate::Base; -use serde::Serialize; - #[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(Serialize)))] - +#[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")] @@ -58,11 +60,19 @@ mod tests { assert_eq!(complex.field_bool, true); let toml_str = toml::to_string_pretty(&complex).unwrap(); - assert_eq!(toml_str, r#"field_bool = true + 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/src/lib.rs b/complex-example/substrate/src/lib.rs index bab46ba..6716e84 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -1,7 +1,6 @@ #![allow(unused)] -use struct_patch::Substrate; use serde::Deserialize; - +use struct_patch::Substrate; #[derive(Deserialize, Default, Substrate)] pub struct Base { diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 664ede0..6258220 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -17,6 +17,7 @@ pub(crate) struct Catalyst { struct Field { attrs: Vec, + attributes: Vec, ident: Option, ty: Type, } @@ -62,8 +63,8 @@ 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 @@ -240,16 +241,24 @@ impl Catalyst { impl Field { /// Generate the token stream for the Complex struct fields pub fn to_token_stream(&self, keep_field_attribute: bool) -> Result { - let Field { + let Field { attrs, - ident, + attributes, + ident, ty, } = self; if keep_field_attribute { - Ok(quote! { - #(#attrs)* - pub #ident: #ty, - }) + if attrs.len() > 0 { + Ok(quote! { + #( #attrs )* + pub #ident: #ty, + }) + } else { + Ok(quote! { + #( #[ #attributes ] )* + pub #ident: #ty, + }) + } } else { Ok(quote! { pub #ident: #ty, @@ -259,15 +268,39 @@ impl Field { /// Generate the token stream for unpack Complex struct fields pub fn to_unpack_stream(&self) -> Result { - let Field { ident, ty: _, attrs: _ } = self; + let Field { ident, ty: _, attributes: _, attrs: _ } = self; Ok(quote! { #ident, }) } /// Parse the Catalyst struct field + 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 } + Field { ident, ty, attrs, attributes: Vec::new() } } }