From d0facd4a7e0e5d52694407e6e807acd71392a1a9 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 11:12:06 +0800 Subject: [PATCH 01/10] catalyst: init private struct test --- complex-example/catalyst/src/lib.rs | 97 +------------------ complex-example/catalyst/src/priv_struct.rs | 9 ++ complex-example/catalyst/src/pub_struct.rs | 101 ++++++++++++++++++++ 3 files changed, 115 insertions(+), 92 deletions(-) create mode 100644 complex-example/catalyst/src/priv_struct.rs create mode 100644 complex-example/catalyst/src/pub_struct.rs diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 9d47923..ff62a4c 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,95 +1,8 @@ -use serde::{Deserialize, Serialize}; -use struct_patch::Catalyst; -use substrate::Base; +//! The demo crate how to use Catalyst bind Substrate and produce Complex -#[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() -} +// All fields of Substrate and all fields of Catalyst are public +mod pub_struct; -#[derive(Catalyst)] -#[catalyst(bind = Base)] -#[complex(name = "SmallCpx")] -#[allow(dead_code)] -#[complex(attribute(derive(Default, Deserialize)))] -#[complex(override_field_attribute("field_string", serde(default = "default_str")))] -#[complex(override_field_attribute("field_string", serde(rename = "renamed_field")))] -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 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); - - small_complex.reaction(); - - 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); - - 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"); - } - - #[test] - fn override_works() { - let toml_str = r#"field_bool = false -extra_bool = false -"#; - let complex: SmallCpx = toml::from_str(toml_str).unwrap(); - assert_eq!(complex.field_string, "default"); - let toml_str = r#"field_bool = false -renamed_field = "Renamed" -extra_bool = false -"#; - let complex: SmallCpx = toml::from_str(toml_str).unwrap(); - assert_eq!(complex.field_string, "Renamed"); - } -} +// Some fields of Substrate and some fields of Catalyst are private +mod priv_struct; diff --git a/complex-example/catalyst/src/priv_struct.rs b/complex-example/catalyst/src/priv_struct.rs new file mode 100644 index 0000000..0df65aa --- /dev/null +++ b/complex-example/catalyst/src/priv_struct.rs @@ -0,0 +1,9 @@ +//! This module demo how to use a Substrate from dependant crate +//! In which some fields of Substrate and some field of Catalyst are private + +#[cfg(test)] +mod tests { + #[test] + fn complex_with_private_fields_works() { + } +} diff --git a/complex-example/catalyst/src/pub_struct.rs b/complex-example/catalyst/src/pub_struct.rs new file mode 100644 index 0000000..ebefcd8 --- /dev/null +++ b/complex-example/catalyst/src/pub_struct.rs @@ -0,0 +1,101 @@ +//! This module demo how to use a Substrate from dependant crate +//! In which all fields of Substrate and all field of Catalyst are public +//! +//! Here we have two Catalyst (Amyloid, SmallAmyloid), both bind on the same Substrate, +//! We can easy to `bind` and `decouple` to manipulate these. + +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")] +#[allow(dead_code)] +#[complex(attribute(derive(Default, Deserialize)))] +#[complex(override_field_attribute("field_string", serde(default = "default_str")))] +#[complex(override_field_attribute("field_string", serde(rename = "renamed_field")))] +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 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); + + small_complex.reaction(); + + 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); + + 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"); + } + + #[test] + fn override_works() { + let toml_str = r#"field_bool = false +extra_bool = false +"#; + let complex: SmallCpx = toml::from_str(toml_str).unwrap(); + assert_eq!(complex.field_string, "default"); + let toml_str = r#"field_bool = false +renamed_field = "Renamed" +extra_bool = false +"#; + let complex: SmallCpx = toml::from_str(toml_str).unwrap(); + assert_eq!(complex.field_string, "Renamed"); + } +} From fa58a9d566f15e632997eba9b96b7c82c4ec7884 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 11:20:58 +0800 Subject: [PATCH 02/10] catalyst: add substrate with private fields --- complex-example/substrate/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 2833a84..caacbd9 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -17,6 +17,13 @@ impl Base { } } +#[derive(Deserialize, Default, Substrate)] +pub struct PrivateBase { + private_bool: bool, + private_string: String, + private_option: Option, +} + #[cfg(test)] mod tests { use super::*; @@ -30,4 +37,13 @@ mod tests { let _fields: syn::Fields = syn_serde::json::from_str(&Base::expose_content()).unwrap(); } + + #[test] + fn expose_private_works() { + assert_eq!( + PrivateBase::expose_content(), + r#"{"named":[{"ident":"private_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"ident":"private_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"ident":"private_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(&PrivateBase::expose_content()).unwrap(); + } } From bcb0e507c1a57940cc85b1bc5b6eeefcb9f4ca01 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 11:54:32 +0800 Subject: [PATCH 03/10] catalyst: handle private field --- derive/src/catalyst.rs | 4 ++-- derive/src/substrate.rs | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index d019b49..d78331b 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -132,9 +132,9 @@ impl Catalyst { #struct_name { #(#catalyst_fields)* }, - #substrate_name { + #substrate_name::substrate_new( #(#substrate_fields)* - } + ) ) } } diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index e482de1..9a95391 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -16,10 +16,17 @@ impl Substrate { fields, } = self; + let mut field_idents = Vec::new(); + let mut field_tys = Vec::new(); + for field in fields.iter() { + field_idents.push(field.ident.clone()); + field_tys.push(field.ty.clone()); + } + let active_site = json::to_string(fields); Ok(quote! { - impl struct_patch::traits::Substrate for #struct_name { + impl struct_patch::traits::Substrate for #struct_name { fn expose_content() -> &'static str { #active_site } @@ -27,6 +34,16 @@ impl Substrate { println!("cargo:rustc-env={}={}", stringify!(#struct_name), Self::expose_content()); } } + impl #struct_name { + /// A new function generated by Substrate macro + pub fn substrate_new( + #( #field_idents: #field_tys, )* + ) -> #struct_name { + #struct_name { + #( #field_idents, )* + } + } + } }) } /// Parse the substrate struct From e4d3b4b45b66c71d110e7444458aa2dce082b02c Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 11:55:09 +0800 Subject: [PATCH 04/10] Revert "catalyst: add substrate with private fields" This reverts commit fa58a9d566f15e632997eba9b96b7c82c4ec7884. --- complex-example/substrate/src/lib.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index caacbd9..2833a84 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -17,13 +17,6 @@ impl Base { } } -#[derive(Deserialize, Default, Substrate)] -pub struct PrivateBase { - private_bool: bool, - private_string: String, - private_option: Option, -} - #[cfg(test)] mod tests { use super::*; @@ -37,13 +30,4 @@ mod tests { let _fields: syn::Fields = syn_serde::json::from_str(&Base::expose_content()).unwrap(); } - - #[test] - fn expose_private_works() { - assert_eq!( - PrivateBase::expose_content(), - r#"{"named":[{"ident":"private_bool","colon_token":true,"ty":{"path":{"segments":[{"ident":"bool"}]}}},{"ident":"private_string","colon_token":true,"ty":{"path":{"segments":[{"ident":"String"}]}}},{"ident":"private_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(&PrivateBase::expose_content()).unwrap(); - } } From 8f943df10fe4786b7c61824bc745d5ed8041cd83 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 11:55:48 +0800 Subject: [PATCH 05/10] Revert "catalyst: init private struct test" This reverts commit d0facd4a7e0e5d52694407e6e807acd71392a1a9. --- complex-example/catalyst/src/lib.rs | 97 ++++++++++++++++++- complex-example/catalyst/src/priv_struct.rs | 9 -- complex-example/catalyst/src/pub_struct.rs | 101 -------------------- 3 files changed, 92 insertions(+), 115 deletions(-) delete mode 100644 complex-example/catalyst/src/priv_struct.rs delete mode 100644 complex-example/catalyst/src/pub_struct.rs diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index ff62a4c..9d47923 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -1,8 +1,95 @@ -//! The demo crate how to use Catalyst bind Substrate and produce Complex +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, +} -// All fields of Substrate and all fields of Catalyst are public -mod pub_struct; +fn default_str() -> String { + "default".to_string() +} -// Some fields of Substrate and some fields of Catalyst are private -mod priv_struct; +#[derive(Catalyst)] +#[catalyst(bind = Base)] +#[complex(name = "SmallCpx")] +#[allow(dead_code)] +#[complex(attribute(derive(Default, Deserialize)))] +#[complex(override_field_attribute("field_string", serde(default = "default_str")))] +#[complex(override_field_attribute("field_string", serde(rename = "renamed_field")))] +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 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); + + small_complex.reaction(); + + 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); + + 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"); + } + + #[test] + fn override_works() { + let toml_str = r#"field_bool = false +extra_bool = false +"#; + let complex: SmallCpx = toml::from_str(toml_str).unwrap(); + assert_eq!(complex.field_string, "default"); + let toml_str = r#"field_bool = false +renamed_field = "Renamed" +extra_bool = false +"#; + let complex: SmallCpx = toml::from_str(toml_str).unwrap(); + assert_eq!(complex.field_string, "Renamed"); + } +} diff --git a/complex-example/catalyst/src/priv_struct.rs b/complex-example/catalyst/src/priv_struct.rs deleted file mode 100644 index 0df65aa..0000000 --- a/complex-example/catalyst/src/priv_struct.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! This module demo how to use a Substrate from dependant crate -//! In which some fields of Substrate and some field of Catalyst are private - -#[cfg(test)] -mod tests { - #[test] - fn complex_with_private_fields_works() { - } -} diff --git a/complex-example/catalyst/src/pub_struct.rs b/complex-example/catalyst/src/pub_struct.rs deleted file mode 100644 index ebefcd8..0000000 --- a/complex-example/catalyst/src/pub_struct.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! This module demo how to use a Substrate from dependant crate -//! In which all fields of Substrate and all field of Catalyst are public -//! -//! Here we have two Catalyst (Amyloid, SmallAmyloid), both bind on the same Substrate, -//! We can easy to `bind` and `decouple` to manipulate these. - -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")] -#[allow(dead_code)] -#[complex(attribute(derive(Default, Deserialize)))] -#[complex(override_field_attribute("field_string", serde(default = "default_str")))] -#[complex(override_field_attribute("field_string", serde(rename = "renamed_field")))] -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 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); - - small_complex.reaction(); - - 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); - - 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"); - } - - #[test] - fn override_works() { - let toml_str = r#"field_bool = false -extra_bool = false -"#; - let complex: SmallCpx = toml::from_str(toml_str).unwrap(); - assert_eq!(complex.field_string, "default"); - let toml_str = r#"field_bool = false -renamed_field = "Renamed" -extra_bool = false -"#; - let complex: SmallCpx = toml::from_str(toml_str).unwrap(); - assert_eq!(complex.field_string, "Renamed"); - } -} From 42a9c11eeba7872334696c7c6c86c1d83fee3cd7 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 12:14:38 +0800 Subject: [PATCH 06/10] catalyst: handle private fields from substrate --- complex-example/catalyst/src/lib.rs | 4 +++ complex-example/substrate/src/lib.rs | 37 +++++++++++++++++++++++++++- derive/src/catalyst.rs | 4 +-- derive/src/substrate.rs | 11 +++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 9d47923..144b85a 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -66,12 +66,14 @@ mod tests { toml_str, r#"field_bool = true field_string = "" +private_number = 0 extra_bool = false extra_string = "" "# ); let toml_str = r#" field_bool = true field_string = "" +private_number = 0 extra_bool = true "#; let complex: AmyloidComplex = toml::from_str(toml_str).unwrap(); @@ -81,11 +83,13 @@ extra_bool = true #[test] fn override_works() { let toml_str = r#"field_bool = false +private_number = 0 extra_bool = false "#; let complex: SmallCpx = toml::from_str(toml_str).unwrap(); assert_eq!(complex.field_string, "default"); let toml_str = r#"field_bool = false +private_number = 0 renamed_field = "Renamed" extra_bool = false "#; diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 2833a84..013feab 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -9,12 +9,17 @@ pub struct Base { pub field_bool: bool, pub field_string: String, pub field_option: Option, + private_number: u8, } impl Base { pub fn has_bool(&self) -> bool { self.field_bool } + + pub fn get_private_number(&self) -> u8 { + self.private_number + } } #[cfg(test)] @@ -25,9 +30,39 @@ mod tests { fn expose_works() { assert_eq!( Base::expose_content(), - 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"}]}}}]}}}]}}}]}"# + 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"}]}}}]}}}]}}},{"ident":"private_number","colon_token":true,"ty":{"path":{"segments":[{"ident":"u8"}]}}}]}"# ); let _fields: syn::Fields = syn_serde::json::from_str(&Base::expose_content()).unwrap(); } + + #[test] + fn substrate_new_works() { + let b = Base::substrate_new( + true, + "test".to_string(), + Some(100), + 7u8, + ); + assert_eq!(b.field_bool, true); + assert_eq!(b.field_string, "test"); + assert_eq!(b.field_option, Some(100)); + assert_eq!(b.get_private_number(), 7); + } + + #[test] + fn substrate_unpack_works() { + let b = Base::substrate_new( + true, + "test".to_string(), + Some(100), + 7u8, + ); + let (field_bool, field_string, field_option, private_number) = b.substrate_unpack(); + + assert_eq!(field_bool, true); + assert_eq!(field_string, "test"); + assert_eq!(field_option, Some(100)); + assert_eq!(private_number, 7); + } } diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index d78331b..bc3bfc2 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -105,9 +105,9 @@ impl Catalyst { 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 { + let ( #(#substrate_fields)* - } = s; + ) = s.substrate_unpack(); let #struct_name { #(#catalyst_fields)* } = self; diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index 9a95391..e701d97 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -43,6 +43,17 @@ impl Substrate { #( #field_idents, )* } } + + /// A unpack function generated by Substrate macro + pub fn substrate_unpack( + self + ) -> ( + #( #field_tys, )* + ) { + ( + #( self.#field_idents, )* + ) + } } }) } From 928f7843f2b0aaef4bbb2affd36ac128a78f92bb Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 12:37:35 +0800 Subject: [PATCH 07/10] catalyst: upate testcase with private fields from Catalyst --- complex-example/catalyst/src/lib.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 144b85a..577c0d8 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -14,12 +14,26 @@ struct Amyloid { #[complex(attribute(serde(default = "default_str")))] pub extra_string: String, pub extra_option: Option, + #[complex(attribute(serde(default = "default_extra_private_number")))] + extra_private_number: u8, } fn default_str() -> String { "default".to_string() } +fn default_extra_private_number() -> u8 { + 7 +} + +#[allow(dead_code)] +impl AmyloidComplex { + /// Sum up the private_number(from Substrate) and the extra_private_number(from Catalyst) + fn private_number_sum(&self) -> u8 { + self.private_number + self.extra_private_number + } +} + #[derive(Catalyst)] #[catalyst(bind = Base)] #[complex(name = "SmallCpx")] @@ -60,6 +74,7 @@ mod tests { let amyloid = Amyloid::default(); let complex = amyloid.bind(substrate); assert_eq!(complex.field_bool, true); + assert_eq!(complex.private_number_sum(), 0); let toml_str = toml::to_string_pretty(&complex).unwrap(); assert_eq!( @@ -69,15 +84,20 @@ field_string = "" private_number = 0 extra_bool = false extra_string = "" +extra_private_number = 0 "# ); let toml_str = r#" field_bool = true field_string = "" -private_number = 0 +private_number = 1 extra_bool = true "#; let complex: AmyloidComplex = toml::from_str(toml_str).unwrap(); assert_eq!(complex.extra_string, "default"); + + // the `extra_private_number` is 7 generated by `default_extra_private_number`, when serialize with a missing field + // such that the `private_number` + `extra_private_number` = 1 + 7 = 8 + assert_eq!(complex.private_number_sum(), 8); } #[test] From e5367dfef8a01fdc27cd35fb0c0845177a4010a7 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 12:41:53 +0800 Subject: [PATCH 08/10] doc: update readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 36a4b50..8aea66a 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,6 @@ 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. -All the fields in substrate and catalyst need be public, and the fields in complex are also public. The overall behavior likes [chemical catalysts](https://en.wikipedia.org/wiki/Enzyme_catalysis), a catalyst **bind** on a substrate to form a complex struct, which has all fields from substrate and catalyst. Also, a complex can **decouple** without clone and return a catalyst and substrate. Check the [complex-example](./complex-example/catalyst/src/lib.rs). In the future, we may have a sub feature with catalyst, such that it will provide no memory moving decouple, but need unsafe code. From 279f622e8252e2212fddfa1e97a113a96b84b73f Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 12:53:16 +0800 Subject: [PATCH 09/10] doc: refine docs for methods of Substrate --- derive/src/substrate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index e701d97..0aba216 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -35,7 +35,7 @@ impl Substrate { } } impl #struct_name { - /// A new function generated by Substrate macro + /// Support `decouple` of Complex with private fields, a new function generated by Substrate macro pub fn substrate_new( #( #field_idents: #field_tys, )* ) -> #struct_name { @@ -44,7 +44,7 @@ impl Substrate { } } - /// A unpack function generated by Substrate macro + /// Support `bind` of Catalyst with private fields, an unpack function generated by Substrate macro pub fn substrate_unpack( self ) -> ( From 2fa9162b317ad8f0cd62a0340211e763838e474a Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Sun, 14 Jun 2026 20:09:21 +0800 Subject: [PATCH 10/10] catalyst: rename methods for Substrate --- complex-example/substrate/src/lib.rs | 6 +++--- derive/src/catalyst.rs | 4 ++-- derive/src/substrate.rs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/complex-example/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 013feab..5fae9ce 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -38,7 +38,7 @@ mod tests { #[test] fn substrate_new_works() { - let b = Base::substrate_new( + let b = Base::__substrate_new( true, "test".to_string(), Some(100), @@ -52,13 +52,13 @@ mod tests { #[test] fn substrate_unpack_works() { - let b = Base::substrate_new( + let b = Base::__substrate_new( true, "test".to_string(), Some(100), 7u8, ); - let (field_bool, field_string, field_option, private_number) = b.substrate_unpack(); + let (field_bool, field_string, field_option, private_number) = b.__substrate_unpack(); assert_eq!(field_bool, true); assert_eq!(field_string, "test"); diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index bc3bfc2..6ec7199 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -107,7 +107,7 @@ impl Catalyst { fn bind(self, s: #substrate_name) -> #complex_struct_name { let ( #(#substrate_fields)* - ) = s.substrate_unpack(); + ) = s.__substrate_unpack(); let #struct_name { #(#catalyst_fields)* } = self; @@ -132,7 +132,7 @@ impl Catalyst { #struct_name { #(#catalyst_fields)* }, - #substrate_name::substrate_new( + #substrate_name::__substrate_new( #(#substrate_fields)* ) ) diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index 0aba216..c546f2d 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -36,7 +36,7 @@ impl Substrate { } impl #struct_name { /// Support `decouple` of Complex with private fields, a new function generated by Substrate macro - pub fn substrate_new( + pub fn __substrate_new( #( #field_idents: #field_tys, )* ) -> #struct_name { #struct_name { @@ -45,7 +45,7 @@ impl Substrate { } /// Support `bind` of Catalyst with private fields, an unpack function generated by Substrate macro - pub fn substrate_unpack( + pub fn __substrate_unpack( self ) -> ( #( #field_tys, )*