From 9be9c9818959d087eb65fb55b39df9bc2b18a347 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Fri, 1 May 2026 23:04:08 +0800 Subject: [PATCH 01/13] catalyst: init substrate trait and macro --- .github/workflows/test.yml | 3 ++ .gitignore | 1 + Cargo.toml | 5 +++- complex-example/Cargo.toml | 19 ++++++++++++ complex-example/substrate/Cargo.toml | 15 ++++++++++ complex-example/substrate/src/lib.rs | 19 ++++++++++++ derive/Cargo.toml | 2 ++ derive/src/lib.rs | 14 +++++++++ derive/src/substrate.rs | 45 ++++++++++++++++++++++++++++ flake.nix | 15 +++++++++- lib/Cargo.toml | 4 ++- lib/src/lib.rs | 3 ++ lib/src/traits.rs | 5 ++++ 13 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 complex-example/Cargo.toml create mode 100644 complex-example/substrate/Cargo.toml create mode 100644 complex-example/substrate/src/lib.rs create mode 100644 derive/src/substrate.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 32cb589..777fc4b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,3 +73,6 @@ jobs: cd no-std-examples nix develop .#no-std -c cargo run --features=box --bin no-std-box nix develop .#no-std -c cargo run --features=option --bin no-std-option + + - name: Test with catalyst + nix develop .#ci -c check-catalyst diff --git a/.gitignore b/.gitignore index cb76358..1fa5e3d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Cargo.lock .direnv/ struct-patch/examples no-std-examples/target +complex-example/target diff --git a/Cargo.toml b/Cargo.toml index 9f23a03..ad791a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,10 @@ members = [ "lib", "derive", ] -exclude = [ "no-std-examples" ] +exclude = [ + "no-std-examples", + "complex-example", +] [workspace.package] authors = ["Antonio Yang "] diff --git a/complex-example/Cargo.toml b/complex-example/Cargo.toml new file mode 100644 index 0000000..b605a33 --- /dev/null +++ b/complex-example/Cargo.toml @@ -0,0 +1,19 @@ +[workspace] +resolver = "2" +members = [ + "substrate", +] +[workspace.dependencies] +struct-patch = { path = "../lib", features = ["catalyst"] } + +[workspace.package] +authors = ["Antonio Yang "] +version = "0.11.0" +edition = "2021" +categories = ["development-tools"] +keywords = ["struct", "patch", "macro", "derive", "overlay"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/yanganto/struct-patch/" +description = "A library that helps you implement partial updates for your structs." +rust-version = "1.61.0" diff --git a/complex-example/substrate/Cargo.toml b/complex-example/substrate/Cargo.toml new file mode 100644 index 0000000..eeff6db --- /dev/null +++ b/complex-example/substrate/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "substrate" +authors.workspace = true +version.workspace = true +edition.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +description.workspace = true +rust-version.workspace = true + +[dependencies] +struct-patch.workspace = true diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs new file mode 100644 index 0000000..8c23685 --- /dev/null +++ b/complex-example/substrate/src/lib.rs @@ -0,0 +1,19 @@ +use struct_patch::Substrate; + +#[derive(Substrate)] +struct Base { + field_bool: bool, + field_string: String, + field_option: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn expose_works() { + assert_eq!(Base::expose(), + r#""{\"named\":[{\"ident\":\"field_bool\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"bool\"}]}}},{\"ident\":\"field_string\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"String\"}]}}},{\"ident\":\"field_option\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"Option\",\"arguments\":{\"angle_bracketed\":{\"args\":[{\"type\":{\"path\":{\"segments\":[{\"ident\":\"usize\"}]}}}]}}}]}}}]}""#); + } +} diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 29f7b62..ef1e6db 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -18,12 +18,14 @@ proc-macro = true proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["parsing"] } +syn-serde = { version = "0.3.2", features = ["json"], optional = true } [features] status = [] op = [] merge = [] nesting = [] +catalyst = [ "syn-serde" ] [dev-dependencies] pretty_assertions_sorted = "1.2.3" diff --git a/derive/src/lib.rs b/derive/src/lib.rs index c7b6853..90e87e9 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,9 +1,13 @@ extern crate proc_macro; mod filler; mod patch; +#[cfg(feature = "catalyst")] +mod substrate; use filler::Filler; use patch::Patch; +#[cfg(feature = "catalyst")] +use substrate::Substrate; use syn::meta::ParseNestedMeta; use syn::spanned::Spanned; @@ -34,6 +38,16 @@ pub fn derive_filler(item: proc_macro::TokenStream) -> proc_macro::TokenStream { .into() } +#[cfg(feature = "catalyst")] +#[proc_macro_derive(Substrate, attributes(substrate))] +pub fn derive_substrate(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + Substrate::from_ast(syn::parse_macro_input!(item as syn::DeriveInput)) + .unwrap() + .to_token_stream() + .unwrap() + .into() +} + fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { let expr: syn::Expr = meta.value()?.parse()?; let mut value = &expr; diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs new file mode 100644 index 0000000..e532cfa --- /dev/null +++ b/derive/src/substrate.rs @@ -0,0 +1,45 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::{DeriveInput, Result}; +use syn_serde::json; + +pub(crate) struct Substrate { + struct_name: Ident, + fields: syn::Fields, +} + +impl Substrate { + /// Generate the token stream which provide expose for Substrate + pub fn to_token_stream(&self) -> Result { + let Substrate { + struct_name, + fields, + } = self; + + let active_site = json::to_string(fields); + + Ok(quote! { + impl struct_patch::traits::Substrate for #struct_name { + fn expose() -> &'static str { + stringify!(#active_site) + } + } + }) + } + /// Parse the filler struct + pub fn from_ast(DeriveInput { ident, data, .. }: syn::DeriveInput) -> Result { + let fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = data { + fields + } else { + return Err(syn::Error::new( + ident.span(), + "Substrate derive only use for struct", + )); + }; + + Ok(Substrate { + struct_name: ident, + fields, + }) + } +} diff --git a/flake.nix b/flake.nix index 6b56f0a..1492f0e 100644 --- a/flake.nix +++ b/flake.nix @@ -22,15 +22,24 @@ sleep 10 cargo publish -p struct-patch ''; + checkCatalystScript = pkgs.writeShellScriptBin "check-catalyst" '' + cd $(git rev-parse --show-toplevel 2>/dev/null) + cd complex-example + cargo test -p substrate + ''; updateDependencyScript = pkgs.writeShellScriptBin "update-dependency" '' dr ./Cargo.toml cd no-std-examples dr ./Cargo.toml - if [[ -f "Cargo.toml.old" || -f "no-std-examples/Cargo.toml.old" ]]; then + cd ../complex-example + dr ./Cargo.toml + + if [[ -f "Cargo.toml.old" || -f "no-std-examples/Cargo.toml.old" || -f "complex-example/Cargo.toml.old" ]]; then rm -f Cargo.toml.old rm -f no-std-examples/Cargo.toml.old + rm -f complex-example/Cargo.toml.old exit 1 fi ''; @@ -51,6 +60,8 @@ rust-bin.stable.latest.minimal openssl pkg-config + + checkCatalystScript ]; }; @@ -63,6 +74,8 @@ dr publishScript updateDependencyScript + + checkCatalystScript ]; }; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 795af15..7616d8c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -43,4 +43,6 @@ nesting = [ ] none_as_default = ["option"] keep_none = ["option"] - +catalyst = [ + "struct-patch-derive/catalyst" +] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 61df30e..cf10796 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -82,6 +82,9 @@ extern crate alloc; pub use struct_patch_derive::Filler; #[doc(hidden)] pub use struct_patch_derive::Patch; +#[cfg(feature = "catalyst")] +#[doc(hidden)] +pub use struct_patch_derive::Substrate; pub mod r#box; pub mod option; pub mod traits; diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 87d8ea9..8181a9c 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -111,3 +111,8 @@ pub trait Status { pub trait Merge { fn merge(self, other: Self) -> Self; } + +/// A substrate struct that can expose the fields information thereof +pub trait Substrate { + fn expose() -> &'static str; +} From dd6dc3c95e5d5a48722437714a7a84f7b4832eeb Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sat, 2 May 2026 02:34:51 +0800 Subject: [PATCH 02/13] catalyst: init catalyst mod --- complex-example/Cargo.toml | 1 + complex-example/catalyst/Cargo.toml | 16 +++ complex-example/catalyst/src/lib.rs | 22 ++++ complex-example/substrate/src/lib.rs | 2 +- derive/src/catalyst.rs | 152 +++++++++++++++++++++++++++ derive/src/lib.rs | 44 ++++++++ derive/src/patch.rs | 32 +----- derive/src/substrate.rs | 4 +- flake.nix | 1 + lib/src/lib.rs | 3 + 10 files changed, 243 insertions(+), 34 deletions(-) create mode 100644 complex-example/catalyst/Cargo.toml create mode 100644 complex-example/catalyst/src/lib.rs create mode 100644 derive/src/catalyst.rs diff --git a/complex-example/Cargo.toml b/complex-example/Cargo.toml index b605a33..c221c03 100644 --- a/complex-example/Cargo.toml +++ b/complex-example/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "substrate", + "catalyst", ] [workspace.dependencies] struct-patch = { path = "../lib", features = ["catalyst"] } diff --git a/complex-example/catalyst/Cargo.toml b/complex-example/catalyst/Cargo.toml new file mode 100644 index 0000000..569016a --- /dev/null +++ b/complex-example/catalyst/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "catalyst" +authors.workspace = true +version.workspace = true +edition.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +description.workspace = true +rust-version.workspace = true + +[dependencies] +struct-patch.workspace = true +substrate = { path = "../substrate" } diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs new file mode 100644 index 0000000..15f2afb --- /dev/null +++ b/complex-example/catalyst/src/lib.rs @@ -0,0 +1,22 @@ +use struct_patch::Catalyst; +use substrate::Base; + +#[derive(Catalyst)] +// #[catalyst(bind = Base)] +// #[complex(name = Complex)] +// #[complex(attribute(derive(Default)))] +struct Amyloid { + extra_bool: bool, + extra_string: String, + extra_option: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn complex_works() { + let complex = AmyloidComplex { }; + } +} diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 8c23685..d64c4f3 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -1,7 +1,7 @@ use struct_patch::Substrate; #[derive(Substrate)] -struct Base { +pub struct Base { field_bool: bool, field_string: String, field_option: Option, diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs new file mode 100644 index 0000000..47bea8a --- /dev/null +++ b/derive/src/catalyst.rs @@ -0,0 +1,152 @@ +extern crate proc_macro; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, ToTokens}; +use std::str::FromStr; +use syn::{ + meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, Lit, LitStr, + Result, Type, +}; +use syn_serde::json; + +pub(crate) struct Catalyst { + visibility: syn::Visibility, + struct_name: Ident, + complex_struct_name: Ident, + generics: syn::Generics, + attributes: Vec, + fields: syn::Fields, + bind: String, +} + +const CATALYST: &str = "patch"; +const COMPLEX: &str = "complex"; +const BIND: &str = "bind"; +const NAME: &str = "name"; +const ATTRIBUTE: &str = "attribute"; + +impl Catalyst { + /// let catalyst bind the substrate and generate the token stream for complex + pub fn to_token_stream(&self) -> Result { + let Catalyst { + visibility, + struct_name, + complex_struct_name, + generics, + attributes, + fields: _fields, + bind: _bind, + } = self; + + // let active_site = json::to_string(fields); + + let mapped_attributes = attributes + .iter() + .map(|a| { + quote! { + #[#a] + } + }) + .collect::>(); + + Ok(quote! { + #(#mapped_attributes)* + #visibility struct #complex_struct_name #generics { + } + }) + } + /// Parse the Catalyst struct + pub fn from_ast( + DeriveInput { + ident, + data, + generics, + attrs, + vis, + }: syn::DeriveInput, + ) -> Result { + let fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = data { + fields + } else { + return Err(syn::Error::new( + ident.span(), + "Catalyst derive only use for struct", + )); + }; + let mut name = None; + let mut attributes = vec![]; + let mut bind = String::new(); + + for attr in attrs { + // TODO + // Fix not cross + // O complex(name = ..); X catalyst(name = ..) + // O catalyst(bind = ..); X complex(bind = ..) + if attr.path().to_string().as_str() != CATALYST + || attr.path().to_string().as_str() != COMPLEX + { + continue; + } + + if let syn::Meta::List(meta) = &attr.meta { + if meta.tokens.is_empty() { + continue; + } + } + + attr.parse_nested_meta(|meta| { + let path = meta.path.to_string(); + match path.as_str() { + NAME => { + if let Some(lit) = crate::get_lit_str(path, &meta)? { + if name.is_some() { + return Err(meta + .error("The name attribute can't be defined more than once")); + } + name = Some(lit.parse()?); + } + } + ATTRIBUTE => { + let content; + parenthesized!(content in meta.input); + let attribute: TokenStream = content.parse()?; + attributes.push(attribute); + } + BIND => { + // TODO + } + _ => { + return Err(meta.error(format_args!( + "unknown catalyst container attribute `{}`", + path.replace(' ', "") + ))); + } + } + Ok(()) + })?; + } + + Ok(Catalyst { + visibility: vis, + complex_struct_name: name.unwrap_or({ + let ts = TokenStream::from_str(&format!("{}Complex", &ident,)).unwrap(); + let lit = LitStr::new(&ts.to_string(), Span::call_site()); + lit.parse()? + }), + struct_name: ident, + generics, + attributes, + fields, + bind, + }) + } +} + +trait ToStr { + fn to_string(&self) -> String; +} + +impl ToStr for syn::Path { + fn to_string(&self) -> String { + self.to_token_stream().to_string() + } +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 90e87e9..e614741 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,9 +1,13 @@ extern crate proc_macro; +#[cfg(feature = "catalyst")] +mod catalyst; mod filler; mod patch; #[cfg(feature = "catalyst")] mod substrate; +#[cfg(feature = "catalyst")] +use catalyst::Catalyst; use filler::Filler; use patch::Patch; #[cfg(feature = "catalyst")] @@ -48,6 +52,16 @@ pub fn derive_substrate(item: proc_macro::TokenStream) -> proc_macro::TokenStrea .into() } +#[cfg(feature = "catalyst")] +#[proc_macro_derive(Catalyst, attributes(catalyst))] +pub fn derive_catalyst(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + Catalyst::from_ast(syn::parse_macro_input!(item as syn::DeriveInput)) + .unwrap() + .to_token_stream() + .unwrap() + .into() +} + fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { let expr: syn::Expr = meta.value()?.parse()?; let mut value = &expr; @@ -66,3 +80,33 @@ fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result syn::Result> { + let expr: syn::Expr = meta.value()?.parse()?; + let mut value = &expr; + while let syn::Expr::Group(e) = value { + value = &e.expr; + } + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = value + { + let suffix = lit.suffix(); + if !suffix.is_empty() { + return Err(Error::new( + lit.span(), + format!("unexpected suffix `{}` on string literal", suffix), + )); + } + Ok(Some(lit.clone())) + } else { + Err(Error::new( + expr.span(), + format!( + "expected serde {} attribute to be a string: `{} = \"...\"`", + attr_name, attr_name + ), + )) + } +} diff --git a/derive/src/patch.rs b/derive/src/patch.rs index af6c581..e504fb0 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -592,7 +592,7 @@ impl Patch { match path.as_str() { NAME => { // #[patch(name = "PatchStruct")] - if let Some(lit) = get_lit_str(path, &meta)? { + if let Some(lit) = crate::get_lit_str(path, &meta)? { if name.is_some() { return Err(meta .error("The name attribute can't be defined more than once")); @@ -872,36 +872,6 @@ impl ToStr for syn::Path { } } -fn get_lit_str(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { - let expr: syn::Expr = meta.value()?.parse()?; - let mut value = &expr; - while let syn::Expr::Group(e) = value { - value = &e.expr; - } - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = value - { - let suffix = lit.suffix(); - if !suffix.is_empty() { - return Err(Error::new( - lit.span(), - format!("unexpected suffix `{}` on string literal", suffix), - )); - } - Ok(Some(lit.clone())) - } else { - Err(Error::new( - expr.span(), - format!( - "expected serde {} attribute to be a string: `{} = \"...\"`", - attr_name, attr_name - ), - )) - } -} - #[cfg(test)] mod tests { use pretty_assertions_sorted::assert_eq_sorted; diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index e532cfa..d33f2c1 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -21,12 +21,12 @@ impl Substrate { Ok(quote! { impl struct_patch::traits::Substrate for #struct_name { fn expose() -> &'static str { - stringify!(#active_site) + stringify!(#active_site) } } }) } - /// Parse the filler struct + /// Parse the substrate struct pub fn from_ast(DeriveInput { ident, data, .. }: syn::DeriveInput) -> Result { let fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = data { fields diff --git a/flake.nix b/flake.nix index 1492f0e..4c693e6 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,7 @@ cd $(git rev-parse --show-toplevel 2>/dev/null) cd complex-example cargo test -p substrate + cargo test -p catalyst ''; updateDependencyScript = pkgs.writeShellScriptBin "update-dependency" '' dr ./Cargo.toml diff --git a/lib/src/lib.rs b/lib/src/lib.rs index cf10796..df03015 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -78,6 +78,9 @@ #[cfg(feature = "alloc")] extern crate alloc; +#[cfg(feature = "catalyst")] +#[doc(hidden)] +pub use struct_patch_derive::Catalyst; #[doc(hidden)] pub use struct_patch_derive::Filler; #[doc(hidden)] From 2ea24637d65f80d1d8ec944b91b8ced30a85f4a9 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sat, 2 May 2026 11:34:56 +0800 Subject: [PATCH 03/13] catalyst: keep catalyst fields --- complex-example/catalyst/src/lib.rs | 6 +++- derive/src/catalyst.rs | 43 +++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 15f2afb..8e6b73c 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -17,6 +17,10 @@ mod tests { #[test] fn complex_works() { - let complex = AmyloidComplex { }; + let complex = AmyloidComplex { + extra_bool: false, + extra_string: String::new(), + extra_option: None, + }; } } diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 47bea8a..03bded6 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -18,6 +18,11 @@ pub(crate) struct Catalyst { bind: String, } +struct Field { + ident: Option, + ty: Type, +} + const CATALYST: &str = "patch"; const COMPLEX: &str = "complex"; const BIND: &str = "bind"; @@ -33,11 +38,22 @@ impl Catalyst { complex_struct_name, generics, attributes, - fields: _fields, + fields, bind: _bind, } = self; - // let active_site = json::to_string(fields); + let mut complex_fields: Vec = Vec::new(); + + // TODO: get fields from substrate + // let active_site = json::load(fields); + + for field in fields.into_iter() { + complex_fields.push(Field::from_ast(field.clone())); + } + let complex_fields = complex_fields + .iter() + .map(|f| f.to_token_stream()) + .collect::>>()?; let mapped_attributes = attributes .iter() @@ -51,6 +67,7 @@ impl Catalyst { Ok(quote! { #(#mapped_attributes)* #visibility struct #complex_struct_name #generics { + #(#complex_fields)* } }) } @@ -141,6 +158,28 @@ impl Catalyst { } } +impl Field { + /// Generate the token stream for the Complex struct fields + pub fn to_token_stream(&self) -> Result { + let Field { ident, ty } = self; + let attributes: Vec = Vec::new(); + + Ok(quote! { + #(#attributes)* + pub #ident: #ty, + }) + } + + /// Parse the Catalyst struct field + pub fn from_ast( + syn::Field { + ident, ty, attrs, .. + }: syn::Field, + ) -> Field { + Field { ident, ty } + } +} + trait ToStr { fn to_string(&self) -> String; } From 431389b93ae9e26cff927bc3ad496389636544ef Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 10 May 2026 15:32:42 +0800 Subject: [PATCH 04/13] fix expose function --- complex-example/substrate/Cargo.toml | 4 ++++ complex-example/substrate/src/lib.rs | 5 ++++- derive/src/substrate.rs | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/complex-example/substrate/Cargo.toml b/complex-example/substrate/Cargo.toml index eeff6db..e7b8148 100644 --- a/complex-example/substrate/Cargo.toml +++ b/complex-example/substrate/Cargo.toml @@ -13,3 +13,7 @@ rust-version.workspace = true [dependencies] struct-patch.workspace = true + +[dev-dependencies] +syn = { version = "2", features = ["full"] } +syn-serde = { version = "0.3.2", features = ["json"] } diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index d64c4f3..6dd8547 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -14,6 +14,9 @@ mod tests { #[test] fn expose_works() { assert_eq!(Base::expose(), - r#""{\"named\":[{\"ident\":\"field_bool\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"bool\"}]}}},{\"ident\":\"field_string\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"String\"}]}}},{\"ident\":\"field_option\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"Option\",\"arguments\":{\"angle_bracketed\":{\"args\":[{\"type\":{\"path\":{\"segments\":[{\"ident\":\"usize\"}]}}}]}}}]}}}]}""#); + r#"{"named":[{"ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"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()).unwrap(); } } diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index d33f2c1..98905a0 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -21,7 +21,7 @@ impl Substrate { Ok(quote! { impl struct_patch::traits::Substrate for #struct_name { fn expose() -> &'static str { - stringify!(#active_site) + #active_site } } }) From 9a6e01190b21ba99255333db2719ecf89836e18e Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 10 May 2026 15:34:56 +0800 Subject: [PATCH 05/13] tmp: hard code to have active sites --- complex-example/catalyst/src/lib.rs | 3 +++ derive/src/catalyst.rs | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 8e6b73c..0a648af 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -18,6 +18,9 @@ mod tests { #[test] fn complex_works() { let complex = AmyloidComplex { + field_bool: false, + field_string: String::new(), + field_option: None, extra_bool: false, extra_string: String::new(), extra_option: None, diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 03bded6..8d74ca1 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -45,11 +45,18 @@ impl Catalyst { let mut complex_fields: Vec = Vec::new(); // TODO: get fields from substrate - // let active_site = json::load(fields); + let substrate_fields: syn::Fields = syn_serde::json::from_str( + r#"{"named":[{"ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"ident":"field_option","colon_token":true,"ty":{"path":{"segments":[{"ident":"Option","arguments":{"angle_bracketed":{"args":[{"type":{"path":{"segments":[{"ident":"usize"}]}}}]}}}]}}}]}"# + ).unwrap(); - for field in fields.into_iter() { + for field in substrate_fields.into_iter() { complex_fields.push(Field::from_ast(field.clone())); } + + for field in fields.iter() { + complex_fields.push(Field::from_ast(field.clone())); + } + let complex_fields = complex_fields .iter() .map(|f| f.to_token_stream()) From b3fa6c80b4745e4b1da698ac4316b25afc5237b5 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 10 May 2026 21:22:40 +0800 Subject: [PATCH 06/13] catalyst: implement bind --- complex-example/catalyst/Cargo.toml | 4 +++ complex-example/catalyst/build.rs | 5 +++ complex-example/catalyst/src/lib.rs | 5 ++- complex-example/substrate/src/lib.rs | 7 ++-- derive/src/catalyst.rs | 53 ++++++++++++++++++---------- derive/src/substrate.rs | 5 ++- lib/src/traits.rs | 3 +- 7 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 complex-example/catalyst/build.rs diff --git a/complex-example/catalyst/Cargo.toml b/complex-example/catalyst/Cargo.toml index 569016a..409b5f8 100644 --- a/complex-example/catalyst/Cargo.toml +++ b/complex-example/catalyst/Cargo.toml @@ -14,3 +14,7 @@ rust-version.workspace = true [dependencies] struct-patch.workspace = true substrate = { path = "../substrate" } + +[build-dependencies] +struct-patch.workspace = true +substrate = { path = "../substrate" } diff --git a/complex-example/catalyst/build.rs b/complex-example/catalyst/build.rs new file mode 100644 index 0000000..faab882 --- /dev/null +++ b/complex-example/catalyst/build.rs @@ -0,0 +1,5 @@ +use struct_patch::Substrate; + +fn main() { + substrate::Base::expose(); +} diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 0a648af..a96ab67 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,8 +1,7 @@ use struct_patch::Catalyst; -use substrate::Base; #[derive(Catalyst)] -// #[catalyst(bind = Base)] +#[catalyst(bind = substrate::Base)] // #[complex(name = Complex)] // #[complex(attribute(derive(Default)))] struct Amyloid { @@ -17,7 +16,7 @@ mod tests { #[test] fn complex_works() { - let complex = AmyloidComplex { + let _complex = AmyloidComplex { field_bool: false, field_string: String::new(), field_option: None, diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 6dd8547..fe73dcb 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -13,10 +13,11 @@ mod tests { #[test] fn expose_works() { - assert_eq!(Base::expose(), + assert_eq!( + Base::expose_content(), r#"{"named":[{"ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"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()).unwrap(); + 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 8d74ca1..89b069f 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -23,7 +23,7 @@ struct Field { ty: Type, } -const CATALYST: &str = "patch"; +const CATALYST: &str = "catalyst"; const COMPLEX: &str = "complex"; const BIND: &str = "bind"; const NAME: &str = "name"; @@ -39,15 +39,13 @@ impl Catalyst { generics, attributes, fields, - bind: _bind, + bind, } = self; let mut complex_fields: Vec = Vec::new(); - // TODO: get fields from substrate - let substrate_fields: syn::Fields = syn_serde::json::from_str( - r#"{"named":[{"ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"ident":"field_option","colon_token":true,"ty":{"path":{"segments":[{"ident":"Option","arguments":{"angle_bracketed":{"args":[{"type":{"path":{"segments":[{"ident":"usize"}]}}}]}}}]}}}]}"# - ).unwrap(); + let substrate_str = std::env::var(bind).expect("not found"); + let substrate_fields: syn::Fields = syn_serde::json::from_str(&substrate_str).unwrap(); for field in substrate_fields.into_iter() { complex_fields.push(Field::from_ast(field.clone())); @@ -101,13 +99,8 @@ impl Catalyst { let mut bind = String::new(); for attr in attrs { - // TODO - // Fix not cross - // O complex(name = ..); X catalyst(name = ..) - // O catalyst(bind = ..); X complex(bind = ..) - if attr.path().to_string().as_str() != CATALYST - || attr.path().to_string().as_str() != COMPLEX - { + let attr_str = attr.path().to_string(); + if attr_str != CATALYST && attr_str != COMPLEX { continue; } @@ -120,7 +113,8 @@ impl Catalyst { attr.parse_nested_meta(|meta| { let path = meta.path.to_string(); match path.as_str() { - NAME => { + NAME if attr_str == COMPLEX => { + // #[complex(name = "PatchStruct")] if let Some(lit) = crate::get_lit_str(path, &meta)? { if name.is_some() { return Err(meta @@ -129,18 +123,24 @@ impl Catalyst { name = Some(lit.parse()?); } } - ATTRIBUTE => { + ATTRIBUTE if attr_str == COMPLEX => { + // #[complex(attribute(derive(Deserialize)))] let content; parenthesized!(content in meta.input); let attribute: TokenStream = content.parse()?; attributes.push(attribute); } - BIND => { - // TODO + BIND if attr_str == CATALYST => { + // #[catalyst(bind = SubstrateStruct)] + if let Some(lit) = get_struct(path, &meta)? { + if bind.is_empty() { + bind = lit; + } + } } _ => { return Err(meta.error(format_args!( - "unknown catalyst container attribute `{}`", + "unknown complex attribute `{}`", path.replace(' ', "") ))); } @@ -149,6 +149,10 @@ impl Catalyst { })?; } + if bind.is_empty() { + return Err(syn::Error::new(ident.span(), "No substrate for Catalyst")); + } + Ok(Catalyst { visibility: vis, complex_struct_name: name.unwrap_or({ @@ -196,3 +200,16 @@ impl ToStr for syn::Path { self.to_token_stream().to_string() } } + +fn get_struct(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { + let expr: syn::Expr = meta.value()?.parse()?; + let mut value = &expr; + while let syn::Expr::Group(e) = value { + value = &e.expr; + } + if let syn::Expr::Path(syn::ExprPath { path, .. }) = value { + Ok(path.segments.last().map(|seg| seg.ident.to_string())) + } else { + Ok(None) + } +} diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index 98905a0..e482de1 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -20,9 +20,12 @@ impl Substrate { Ok(quote! { impl struct_patch::traits::Substrate for #struct_name { - fn expose() -> &'static str { + fn expose_content() -> &'static str { #active_site } + fn expose() { + println!("cargo:rustc-env={}={}", stringify!(#struct_name), Self::expose_content()); + } } }) } diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 8181a9c..eded849 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -114,5 +114,6 @@ pub trait Merge { /// A substrate struct that can expose the fields information thereof pub trait Substrate { - fn expose() -> &'static str; + fn expose_content() -> &'static str; + fn expose(); } From af97d6d1b880a56a9a837b6631dc3228cb7e5835 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 10 May 2026 21:43:48 +0800 Subject: [PATCH 07/13] impl #[complex(name = "...")] and #[complex(attribute(...))] --- complex-example/catalyst/src/lib.rs | 18 ++++++++++++++++-- complex-example/substrate/src/lib.rs | 1 + derive/src/lib.rs | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index a96ab67..18be04f 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -2,14 +2,22 @@ use struct_patch::Catalyst; #[derive(Catalyst)] #[catalyst(bind = substrate::Base)] -// #[complex(name = Complex)] -// #[complex(attribute(derive(Default)))] +#[allow(dead_code)] struct Amyloid { extra_bool: bool, extra_string: String, extra_option: Option, } +#[derive(Catalyst)] +#[catalyst(bind = substrate::Base)] +#[complex(name = "SmallCpx")] +#[allow(dead_code)] +#[complex(attribute(derive(Default)))] +struct SmallAmyloid { + extra_bool: bool, +} + #[cfg(test)] mod tests { use super::*; @@ -24,5 +32,11 @@ mod tests { extra_string: String::new(), extra_option: None, }; + + let small_complex = SmallCpx::default(); + assert_eq!(small_complex.field_bool, false); + assert_eq!(small_complex.field_string, String::new()); + assert_eq!(small_complex.field_option, None); + assert_eq!(small_complex.extra_bool, false); } } diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index fe73dcb..bc8578e 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use struct_patch::Substrate; #[derive(Substrate)] diff --git a/derive/src/lib.rs b/derive/src/lib.rs index e614741..e41ce64 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -53,7 +53,7 @@ pub fn derive_substrate(item: proc_macro::TokenStream) -> proc_macro::TokenStrea } #[cfg(feature = "catalyst")] -#[proc_macro_derive(Catalyst, attributes(catalyst))] +#[proc_macro_derive(Catalyst, attributes(catalyst, complex))] pub fn derive_catalyst(item: proc_macro::TokenStream) -> proc_macro::TokenStream { Catalyst::from_ast(syn::parse_macro_input!(item as syn::DeriveInput)) .unwrap() From fcf223a284056d7b7c8148a17f248c6514bc64a1 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 10 May 2026 21:48:06 +0800 Subject: [PATCH 08/13] lint --- derive/src/catalyst.rs | 19 ++++--------------- derive/src/patch.rs | 7 +++---- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 89b069f..e9692b5 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -2,15 +2,10 @@ extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; -use syn::{ - meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, Lit, LitStr, - Result, Type, -}; -use syn_serde::json; +use syn::{meta::ParseNestedMeta, parenthesized, DeriveInput, LitStr, Result, Type}; pub(crate) struct Catalyst { visibility: syn::Visibility, - struct_name: Ident, complex_struct_name: Ident, generics: syn::Generics, attributes: Vec, @@ -34,7 +29,6 @@ impl Catalyst { pub fn to_token_stream(&self) -> Result { let Catalyst { visibility, - struct_name, complex_struct_name, generics, attributes, @@ -132,7 +126,7 @@ impl Catalyst { } BIND if attr_str == CATALYST => { // #[catalyst(bind = SubstrateStruct)] - if let Some(lit) = get_struct(path, &meta)? { + if let Some(lit) = get_struct(&meta)? { if bind.is_empty() { bind = lit; } @@ -160,7 +154,6 @@ impl Catalyst { let lit = LitStr::new(&ts.to_string(), Span::call_site()); lit.parse()? }), - struct_name: ident, generics, attributes, fields, @@ -182,11 +175,7 @@ impl Field { } /// Parse the Catalyst struct field - pub fn from_ast( - syn::Field { - ident, ty, attrs, .. - }: syn::Field, - ) -> Field { + pub fn from_ast(syn::Field { ident, ty, .. }: syn::Field) -> Field { Field { ident, ty } } } @@ -201,7 +190,7 @@ impl ToStr for syn::Path { } } -fn get_struct(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { +fn get_struct(meta: &ParseNestedMeta) -> syn::Result> { let expr: syn::Expr = meta.value()?.parse()?; let mut value = &expr; while let syn::Expr::Group(e) = value { diff --git a/derive/src/patch.rs b/derive/src/patch.rs index e504fb0..0292859 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -2,10 +2,9 @@ extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; -use syn::{ - meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, Lit, LitStr, - Result, Type, -}; +use syn::{parenthesized, DeriveInput, Lit, LitStr, Result, Type}; +#[cfg(not(feature = "op"))] +use syn::spanned::Spanned; #[cfg(feature = "op")] use crate::Addable; From c9495c1d0f4f42a1b339ae04ad9c5ec5a0968a6a Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Mon, 11 May 2026 09:11:22 +0800 Subject: [PATCH 09/13] update readme --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e23746..ae846cb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ [![MIT licensed][mit-badge]][mit-url] [![Docs][doc-badge]][doc-url] -A lib help you patch Rust instance, and easy to partial update configures. +A lib help you modify the config struct, you can +- patch an instance, and easy to partial update with `Patch` derive macro +- fill up an instance with `Filler` derive macro +- extend with extra fields with `Substrate` and `Catalyst` derive macros ## Introduction This crate provides the `Patch`, `Filler` traits and accompanying derive macro. @@ -12,6 +15,7 @@ If the any field in the instance is none then it will try to fill the field with Currently, `Filler` only support `Option` and `Vec` fields, and also you can check this [template](https://github.com/yanganto/ConfigTemplate) if you already work on a big project with a lot of configs. This crate support `no_std`, please check [no-std-examples](./no-std-examples). +When extending a config, the substrate struct should be expose in the build script, please check [complex-example](./complex-example) ## Quick Example Deriving `Patch` on a struct will generate a struct similar to the original one, but with all fields wrapped in an `Option`. @@ -99,13 +103,54 @@ assert_eq!(item.maybe_field_int, Some(7)); assert_eq!(item.list, vec![7]); ``` +Deriving `Substrate` on a struct will help you expose the field information, and you can easy to expose in build.rs of other crate. +Deriving `Catalyst` on can read the field information of Substrate and generate a new Complex struct. +The overall behavior likes biological catalysts, a catalyst bind on a substrate to form a complex struct, which has all fields from substrate and catalyst. + +```rust +/// In $dependency-crate/src/lib.rs +use struct_patch::Substrate; +#[derive(Substrate)] +pub struct Base { + field_bool: bool, + field_string: String, +} + +/// In $main-crate/src/build.rs +use struct_patch::Substrate; + +fn main() { + substrate::Base::expose(); +} + +/// In $main-crate/src/lib.rs +use struct_patch::Catalyst; + +#[derive(Catalyst)] +#[catalyst(bind = substrate::Base)] +struct Amyloid { + extra_bool: bool, + extra_option: Option, +} +// Now AmyloidComplex is generated +// struct AmyloidComplex { +// field_bool: bool, +// field_string: String, +// extra_bool: bool, +// extra_option: Option, +//} +``` + ## Documentation and Examples -Also, you can modify the patch structure by defining `#[patch(...)]` or `#[filler(...)]` attributes on the original struct or fields. +Also, you can modify the patch structure by defining `#[patch(...)]`, `#[filler(...)]` or `#[complex(...)]`, `#[catalyst(...)]` attributes on the original struct or fields. Struct attributes: - `#[patch(name = "...")]`: change the name of the generated patch struct. - `#[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) +- `#[complex(name = "...")]`: change the name of the generated complex struct. (catalyst feature) +- `#[complex(attribute(...))]`: add attributes to the generated complex struct. (catalyst feature) Field attributes: - `#[patch(skip)]`: skip the field in the generated patch struct. From 55742fbb134c1178e1401f5d18a1d7886cce049d Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 12 May 2026 13:11:53 +0800 Subject: [PATCH 10/13] update doc --- derive/src/patch.rs | 2 +- lib/src/traits.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/derive/src/patch.rs b/derive/src/patch.rs index 0292859..76eec67 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -2,9 +2,9 @@ extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; -use syn::{parenthesized, DeriveInput, Lit, LitStr, Result, Type}; #[cfg(not(feature = "op"))] use syn::spanned::Spanned; +use syn::{parenthesized, DeriveInput, Lit, LitStr, Result, Type}; #[cfg(feature = "op")] use crate::Addable; diff --git a/lib/src/traits.rs b/lib/src/traits.rs index eded849..d14771c 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -112,8 +112,11 @@ pub trait Merge { fn merge(self, other: Self) -> Self; } +#[cfg(feature = "catalyst")] /// A substrate struct that can expose the fields information thereof pub trait Substrate { fn expose_content() -> &'static str; + + /// Expose the field information, by call this function in Build.rs fn expose(); } From e3ff90230259a6d1d0d35a7e974d20c1c7693bcb Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 12 May 2026 15:12:24 +0800 Subject: [PATCH 11/13] impl decouple for complex --- complex-example/catalyst/src/lib.rs | 32 ++++++----- complex-example/substrate/src/lib.rs | 16 ++++-- derive/src/catalyst.rs | 85 ++++++++++++++++++++++------ lib/src/traits.rs | 14 +++++ 4 files changed, 113 insertions(+), 34 deletions(-) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 18be04f..1fdfb1f 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,21 +1,22 @@ use struct_patch::Catalyst; +use substrate::Base; #[derive(Catalyst)] -#[catalyst(bind = substrate::Base)] +#[catalyst(bind = Base)] #[allow(dead_code)] struct Amyloid { - extra_bool: bool, - extra_string: String, - extra_option: Option, + pub extra_bool: bool, + pub extra_string: String, + pub extra_option: Option, } -#[derive(Catalyst)] -#[catalyst(bind = substrate::Base)] +#[derive(Default, Catalyst)] +#[catalyst(bind = Base)] #[complex(name = "SmallCpx")] #[allow(dead_code)] #[complex(attribute(derive(Default)))] struct SmallAmyloid { - extra_bool: bool, + pub extra_bool: bool, } #[cfg(test)] @@ -24,6 +25,17 @@ mod tests { #[test] fn complex_works() { + let small_complex = SmallCpx::default(); + assert_eq!(small_complex.field_bool, false); + assert_eq!(small_complex.field_string, String::new()); + assert_eq!(small_complex.field_option, None); + assert_eq!(small_complex.extra_bool, false); + + use struct_patch::Complex; + let (_cat, mut substrate) = small_complex.decouple(); + + substrate.field_bool = true; + let _complex = AmyloidComplex { field_bool: false, field_string: String::new(), @@ -32,11 +44,5 @@ mod tests { extra_string: String::new(), extra_option: None, }; - - let small_complex = SmallCpx::default(); - assert_eq!(small_complex.field_bool, false); - assert_eq!(small_complex.field_string, String::new()); - assert_eq!(small_complex.field_option, None); - assert_eq!(small_complex.extra_bool, false); } } diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index bc8578e..7f4c9a4 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -1,11 +1,17 @@ #![allow(unused)] use struct_patch::Substrate; -#[derive(Substrate)] +#[derive(Default, Substrate)] pub struct Base { - field_bool: bool, - field_string: String, - field_option: Option, + pub field_bool: bool, + pub field_string: String, + pub field_option: Option, +} + +impl Base { + fn has_bool(&self) -> bool { + self.field_bool + } } #[cfg(test)] @@ -16,7 +22,7 @@ mod tests { fn expose_works() { assert_eq!( Base::expose_content(), - r#"{"named":[{"ident":"field_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"ident":"field_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"ident":"field_option","colon_token":true,"ty":{"path":{"segments":[{"ident":"Option","arguments":{"angle_bracketed":{"args":[{"type":{"path":{"segments":[{"ident":"usize"}]}}}]}}}]}}}]}"# + 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"}]}}}]}}}]}}}]}"# ); 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 e9692b5..aeb7d41 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -6,6 +6,7 @@ use syn::{meta::ParseNestedMeta, parenthesized, DeriveInput, LitStr, Result, Typ pub(crate) struct Catalyst { visibility: syn::Visibility, + struct_name: Ident, complex_struct_name: Ident, generics: syn::Generics, attributes: Vec, @@ -29,6 +30,7 @@ impl Catalyst { pub fn to_token_stream(&self) -> Result { let Catalyst { visibility, + struct_name, complex_struct_name, generics, attributes, @@ -36,24 +38,49 @@ impl Catalyst { bind, } = self; - let mut complex_fields: Vec = Vec::new(); + let substrate_name: Ident = { + let lit = LitStr::new(&bind, Span::call_site()); + lit.parse()? + }; + + // TODO refact + let mut raw_complex_fields: Vec = Vec::new(); + let mut substrate_fields: Vec = Vec::new(); + let mut catalyst_fields: Vec = Vec::new(); let substrate_str = std::env::var(bind).expect("not found"); - let substrate_fields: syn::Fields = syn_serde::json::from_str(&substrate_str).unwrap(); + let raw_substrate_fields: syn::Fields = syn_serde::json::from_str(&substrate_str).unwrap(); - for field in substrate_fields.into_iter() { - complex_fields.push(Field::from_ast(field.clone())); + for field in raw_substrate_fields.into_iter() { + raw_complex_fields.push(Field::from_ast(field.clone())); + substrate_fields.push(Field::from_ast(field)); } for field in fields.iter() { - complex_fields.push(Field::from_ast(field.clone())); + raw_complex_fields.push(Field::from_ast(field.clone())); + catalyst_fields.push(Field::from_ast(field.clone())); } - let complex_fields = complex_fields + let complex_fields = raw_complex_fields .iter() .map(|f| f.to_token_stream()) .collect::>>()?; + let unpack_complex_fields = raw_complex_fields + .iter() + .map(|f| f.to_unpack_stream()) + .collect::>>()?; + + let catalyst_fields = catalyst_fields + .iter() + .map(|f| f.to_unpack_stream()) + .collect::>>()?; + + let substrate_fields = substrate_fields + .iter() + .map(|f| f.to_unpack_stream()) + .collect::>>()?; + let mapped_attributes = attributes .iter() .map(|a| { @@ -62,12 +89,31 @@ impl Catalyst { } }) .collect::>(); - - Ok(quote! { + let complex_impl = quote! { #(#mapped_attributes)* #visibility struct #complex_struct_name #generics { #(#complex_fields)* } + + impl struct_patch::traits::Complex < #struct_name, #substrate_name > for #complex_struct_name { + fn decouple(self) -> (#struct_name, #substrate_name) { + let #complex_struct_name { + #(#unpack_complex_fields)* + } = self; + ( + #struct_name { + #(#catalyst_fields)* + }, + #substrate_name { + #(#substrate_fields)* + } + ) + } + } + }; + + Ok(quote! { + #complex_impl }) } /// Parse the Catalyst struct @@ -146,14 +192,16 @@ impl Catalyst { if bind.is_empty() { return Err(syn::Error::new(ident.span(), "No substrate for Catalyst")); } + let complex_struct_name = name.unwrap_or({ + let ts = TokenStream::from_str(&format!("{}Complex", &ident,)).unwrap(); + let lit = LitStr::new(&ts.to_string(), Span::call_site()); + lit.parse()? + }); Ok(Catalyst { visibility: vis, - complex_struct_name: name.unwrap_or({ - let ts = TokenStream::from_str(&format!("{}Complex", &ident,)).unwrap(); - let lit = LitStr::new(&ts.to_string(), Span::call_site()); - lit.parse()? - }), + struct_name: ident, + complex_struct_name, generics, attributes, fields, @@ -166,14 +214,19 @@ impl Field { /// Generate the token stream for the Complex struct fields pub fn to_token_stream(&self) -> Result { let Field { ident, ty } = self; - let attributes: Vec = Vec::new(); - Ok(quote! { - #(#attributes)* pub #ident: #ty, }) } + /// Generate the token stream for unpack Complex struct fields + pub fn to_unpack_stream(&self) -> Result { + let Field { ident, ty: _ } = self; + Ok(quote! { + #ident, + }) + } + /// Parse the Catalyst struct field pub fn from_ast(syn::Field { ident, ty, .. }: syn::Field) -> Field { Field { ident, ty } diff --git a/lib/src/traits.rs b/lib/src/traits.rs index d14771c..609e51c 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -120,3 +120,17 @@ pub trait Substrate { /// Expose the field information, by call this function in Build.rs fn expose(); } + +#[cfg(feature = "catalyst")] +/// A catalyst struct that can expose the fields information thereof +pub trait Catalyst { + /// catalyst bind on substrate and generate complex + fn bind(self, substrate: S) -> Cpx; +} + +#[cfg(feature = "catalyst")] +/// A complex struct that can decouple return catalyst and substrate +pub trait Complex { + /// complex decouple to catalyst and substrate + fn decouple(self) -> (Cat, S); +} From 45e775a0ccd8393f6255427aa52a14e80ef7e4a5 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 12 May 2026 15:29:01 +0800 Subject: [PATCH 12/13] impl bind for Catalyst --- complex-example/catalyst/src/lib.rs | 17 +++++++---------- complex-example/substrate/src/lib.rs | 2 +- derive/src/catalyst.rs | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 1fdfb1f..f7a2375 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,7 +1,7 @@ use struct_patch::Catalyst; use substrate::Base; -#[derive(Catalyst)] +#[derive(Default, Catalyst)] #[catalyst(bind = Base)] #[allow(dead_code)] struct Amyloid { @@ -10,7 +10,7 @@ struct Amyloid { pub extra_option: Option, } -#[derive(Default, Catalyst)] +#[derive(Catalyst)] #[catalyst(bind = Base)] #[complex(name = "SmallCpx")] #[allow(dead_code)] @@ -35,14 +35,11 @@ mod tests { let (_cat, mut substrate) = small_complex.decouple(); substrate.field_bool = true; + assert!(substrate.has_bool()); + + let amyloid = Amyloid::default(); - let _complex = AmyloidComplex { - field_bool: false, - field_string: String::new(), - field_option: None, - extra_bool: false, - extra_string: String::new(), - extra_option: None, - }; + let complex = amyloid.bind(substrate); + assert_eq!(complex.field_bool, true); } } diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 7f4c9a4..88166f5 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -9,7 +9,7 @@ pub struct Base { } impl Base { - fn has_bool(&self) -> bool { + pub fn has_bool(&self) -> bool { self.field_bool } } diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index aeb7d41..335d443 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -89,6 +89,21 @@ impl Catalyst { } }) .collect::>(); + let catalyst_impl = quote! { + impl struct_patch::traits::Catalyst < #substrate_name, #complex_struct_name > for #struct_name { + fn bind(self, s: #substrate_name) -> #complex_struct_name { + let #substrate_name { + #(#substrate_fields)* + } = s; + let #struct_name { + #(#catalyst_fields)* + } = self; + #complex_struct_name { + #(#unpack_complex_fields)* + } + } + } + }; let complex_impl = quote! { #(#mapped_attributes)* #visibility struct #complex_struct_name #generics { @@ -113,6 +128,7 @@ impl Catalyst { }; Ok(quote! { + #catalyst_impl #complex_impl }) } From f870added130d5e11ef5ea623ee276666e6014a9 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 12 May 2026 16:02:18 +0800 Subject: [PATCH 13/13] refine readme, doc, error msg and example --- README.md | 34 +++++++++++++++++------------ complex-example/catalyst/src/lib.rs | 17 ++++++++++----- derive/src/catalyst.rs | 6 ++--- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ae846cb..23a9440 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,11 @@ If the any field in the instance is none then it will try to fill the field with Currently, `Filler` only support `Option` and `Vec` fields, and also you can check this [template](https://github.com/yanganto/ConfigTemplate) if you already work on a big project with a lot of configs. This crate support `no_std`, please check [no-std-examples](./no-std-examples). -When extending a config, the substrate struct should be expose in the build script, please check [complex-example](./complex-example) +When extending a config, the base struct should be expose in the build script with `Substrate` trait, then a catalyst struct can bind and produce complex struct, +please check [complex-example](./complex-example) and the [Quick Example: case 3](#case-3---extend-a-struct-from-a-crate). ## Quick Example +#### Case 1 - Patch on a Config Deriving `Patch` on a struct will generate a struct similar to the original one, but with all fields wrapped in an `Option`. An instance of such a patch struct can be applied onto the original struct, replacing values only if they are set to `Some`, leaving them unchanged otherwise. ```rust @@ -69,6 +71,7 @@ fn patch_json() { } ``` +#### Case 2 - Fill up on a Config Deriving `Filler` on a struct will generate a struct similar to the original one with the field with `Option`. Unlike `Patch`, the `Filler` only work on the empty fields of instance. ```rust @@ -103,41 +106,44 @@ assert_eq!(item.maybe_field_int, Some(7)); assert_eq!(item.list, vec![7]); ``` +#### Case 3 - Extend a struct from a crate Deriving `Substrate` on a struct will help you expose the field information, and you can easy to expose in build.rs of other crate. Deriving `Catalyst` on can read the field information of Substrate and generate a new Complex struct. -The overall behavior likes biological catalysts, a catalyst bind on a substrate to form a complex struct, which has all fields from substrate and catalyst. +All the fields in substrate and catalyst need be public, and the fields in complex are also public. +The overall behavior likes chemical catalysts, a catalyst **bind** on a substrate to form a complex struct, which has all fields from substrate and catalyst. +Also, a complex can **decouple** and return a catalyst and substrate, please check [complex-example](./complex-example/catalyst/src/lib.rs). ```rust -/// In $dependency-crate/src/lib.rs +/// In $dependency_crate/src/lib.rs use struct_patch::Substrate; #[derive(Substrate)] pub struct Base { - field_bool: bool, - field_string: String, + pub field_bool: bool, + pub field_string: String, } -/// In $main-crate/src/build.rs +/// In $main_crate/src/build.rs use struct_patch::Substrate; fn main() { - substrate::Base::expose(); + $dependency_crate::Base::expose(); } -/// In $main-crate/src/lib.rs +/// In $main_crate/src/lib.rs use struct_patch::Catalyst; #[derive(Catalyst)] #[catalyst(bind = substrate::Base)] struct Amyloid { - extra_bool: bool, - extra_option: Option, + pub extra_bool: bool, + pub extra_option: Option, } // Now AmyloidComplex is generated // struct AmyloidComplex { -// field_bool: bool, -// field_string: String, -// extra_bool: bool, -// extra_option: Option, +// pub field_bool: bool, +// pub field_string: String, +// pub extra_bool: bool, +// pub extra_option: Option, //} ``` diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index f7a2375..aba2d89 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -19,26 +19,33 @@ struct SmallAmyloid { pub extra_bool: bool, } +#[allow(dead_code)] +impl SmallCpx { + /// A reaction to change the substrate + pub fn reaction(&mut self) { + self.field_bool = true; + } +} + #[cfg(test)] mod tests { use super::*; + use struct_patch::Complex; #[test] fn complex_works() { - let small_complex = SmallCpx::default(); + let mut small_complex = SmallCpx::default(); assert_eq!(small_complex.field_bool, false); assert_eq!(small_complex.field_string, String::new()); assert_eq!(small_complex.field_option, None); assert_eq!(small_complex.extra_bool, false); - use struct_patch::Complex; - let (_cat, mut substrate) = small_complex.decouple(); + small_complex.reaction(); - substrate.field_bool = true; + let (_cat, substrate) = small_complex.decouple(); assert!(substrate.has_bool()); let amyloid = Amyloid::default(); - let complex = amyloid.bind(substrate); assert_eq!(complex.field_bool, true); } diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 335d443..43c4973 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -48,7 +48,7 @@ impl Catalyst { let mut substrate_fields: Vec = Vec::new(); let mut catalyst_fields: Vec = Vec::new(); - let substrate_str = std::env::var(bind).expect("not found"); + let substrate_str = std::env::var(bind).expect("field information of substrate is absent, please expose it in build.rs"); let raw_substrate_fields: syn::Fields = syn_serde::json::from_str(&substrate_str).unwrap(); for field in raw_substrate_fields.into_iter() { @@ -196,7 +196,7 @@ impl Catalyst { } _ => { return Err(meta.error(format_args!( - "unknown complex attribute `{}`", + "unknown catalyst/complex attribute `{}`", path.replace(' ', "") ))); } @@ -206,7 +206,7 @@ impl Catalyst { } if bind.is_empty() { - return Err(syn::Error::new(ident.span(), "No substrate for Catalyst")); + return Err(syn::Error::new(ident.span(), "No substrate for Catalyst, please specify with #[catalyst(bind = ...)]")); } let complex_struct_name = name.unwrap_or({ let ts = TokenStream::from_str(&format!("{}Complex", &ident,)).unwrap();