From 9be9c9818959d087eb65fb55b39df9bc2b18a347 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Fri, 1 May 2026 23:04:08 +0800 Subject: [PATCH 1/2] 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 2/2] 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)]