From 8e55314ef39b188529ade206a8dc9727efd0abfd Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 26 May 2026 10:41:07 +0800 Subject: [PATCH 1/3] bump version --- Cargo.toml | 2 +- complex-example/Cargo.toml | 2 +- lib/Cargo.toml | 2 +- no-std-examples/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bad96a5..ce20139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ exclude = [ [workspace.package] authors = ["Antonio Yang "] -version = "0.12.1" +version = "0.12.2" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/complex-example/Cargo.toml b/complex-example/Cargo.toml index c5f9e00..7b23e37 100644 --- a/complex-example/Cargo.toml +++ b/complex-example/Cargo.toml @@ -9,7 +9,7 @@ struct-patch = { path = "../lib", features = ["catalyst"] } [workspace.package] authors = ["Antonio Yang "] -version = "0.12.1" +version = "0.12.2" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 711aeb6..fa58d1d 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,7 +12,7 @@ readme.workspace = true rust-version.workspace = true [dependencies] -struct-patch-derive = { version = "=0.12.1", path = "../derive" } +struct-patch-derive = { version = "=0.12.2", path = "../derive" } [dev-dependencies] serde_json = "1.0" diff --git a/no-std-examples/Cargo.toml b/no-std-examples/Cargo.toml index 3fdf6fd..e6f153f 100644 --- a/no-std-examples/Cargo.toml +++ b/no-std-examples/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "no-std-examples" authors = ["Antonio Yang "] -version = "0.12.1" +version = "0.12.2" edition = "2021" license = "MIT" From 6f6655fa2c143d93ea33f51f49b1f815739ad5d3 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 26 May 2026 13:42:41 +0800 Subject: [PATCH 2/3] catalyst: add #[complex(override_field_attribute(...))] Implement override_field_attribute, such that developer can easier to modify the attributes on the fields of complex. --- README.md | 11 ++-- complex-example/catalyst/src/lib.rs | 19 ++++++- complex-example/substrate/src/lib.rs | 1 + derive/src/catalyst.rs | 85 ++++++++++++++++++++++++---- 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1d8f111..36a4b50 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,9 @@ 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. 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. Check the [complex-example](./complex-example/catalyst/src/lib.rs). +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. ```rust /// In $dependency_crate/src/lib.rs @@ -153,14 +154,16 @@ struct Amyloid { ``` ## Documentation and Examples -You can modify the patch structure by defining `#[patch(...)]`, `#[filler(...)]` or `#[complex(...)]`, `#[catalyst(...)]` attributes on the original struct or fields. +You can modify the patch structure by defining `#[patch(...)]`, `#[filler(...)]` or `#[complex(...)]`(catalyst feature), `#[catalyst(...)]`(catalyst feature) attributes on the original struct or fields. +Two macros are provided for the catalyst feature because we need to handle the behaviors of two structs simultaneously — the catalyst itself and the product (complex). In general, the complex macro takes precedence over the catalyst macro when any conflict arises. 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) -- `#[catalyst(keep_field_attribute)]`: the attributes of fields in substrate will also pass to complex. (catalyst feature) +- `#[catalyst(keep_field_attribute)]`: All field attributes from a substrate or catalyst will be passed through to the complex, unless an override is explicitly specified for that field. (catalyst feature) +- `#[complex(override_field_attribute("$substrate_field_name", ...))]`: override the complex field attribute, for example `serde(default = "default_str").` (catalyst feature) - `#[complex(name = "...")]`: change the name of the generated complex struct. (catalyst feature) - `#[complex(attribute(...))]`: add attributes to the generated complex struct. (catalyst feature) diff --git a/complex-example/catalyst/src/lib.rs b/complex-example/catalyst/src/lib.rs index 2bc0bea..9d47923 100644 --- a/complex-example/catalyst/src/lib.rs +++ b/complex-example/catalyst/src/lib.rs @@ -24,7 +24,9 @@ fn default_str() -> String { #[catalyst(bind = Base)] #[complex(name = "SmallCpx")] #[allow(dead_code)] -#[complex(attribute(derive(Default)))] +#[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, } @@ -75,4 +77,19 @@ 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/substrate/src/lib.rs b/complex-example/substrate/src/lib.rs index 6716e84..2833a84 100644 --- a/complex-example/substrate/src/lib.rs +++ b/complex-example/substrate/src/lib.rs @@ -2,6 +2,7 @@ use serde::Deserialize; use struct_patch::Substrate; +// TODO verify no rename on a struct using Substrate #[derive(Deserialize, Default, Substrate)] pub struct Base { #[serde(default)] diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 6258220..d019b49 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -1,8 +1,11 @@ extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; +use std::collections::HashMap; use std::str::FromStr; -use syn::{Attribute, meta::ParseNestedMeta, parenthesized, DeriveInput, LitStr, Result, Type}; +use syn::{ + meta::ParseNestedMeta, parenthesized, Attribute, DeriveInput, LitStr, Result, Token, Type, +}; pub(crate) struct Catalyst { visibility: syn::Visibility, @@ -13,6 +16,7 @@ pub(crate) struct Catalyst { fields: syn::Fields, bind: String, keep_field_attribute: bool, + override_field_attributes: HashMap>, // TODO handle no-std } struct Field { @@ -27,6 +31,7 @@ const COMPLEX: &str = "complex"; const BIND: &str = "bind"; const NAME: &str = "name"; const ATTRIBUTE: &str = "attribute"; +const OVERRIDE: &str = "override_field_attribute"; // TODO #[catalyst(keep_field_attrs = [ "serde" ])] for more detail selections const KEEP_FIELD_ATTRIBUTE: &str = "keep_field_attribute"; @@ -42,6 +47,7 @@ impl Catalyst { fields, bind, keep_field_attribute, + override_field_attributes, } = self; let substrate_name: Ident = { @@ -54,7 +60,8 @@ impl Catalyst { let mut substrate_fields: Vec = Vec::new(); let mut catalyst_fields: Vec = Vec::new(); - let substrate_str = std::env::var(bind).expect("field information of substrate is absent, please expose it in build.rs"); + 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() { @@ -69,7 +76,7 @@ impl Catalyst { let complex_fields = raw_complex_fields .iter() - .map(|f| f.to_token_stream(*keep_field_attribute)) + .map(|f| f.to_token_stream(*keep_field_attribute, override_field_attributes)) .collect::>>()?; let unpack_complex_fields = raw_complex_fields @@ -160,6 +167,7 @@ impl Catalyst { let mut attributes = vec![]; let mut bind = String::new(); let mut keep_field_attribute = false; + let mut override_field_attributes = HashMap::>::new(); for attr in attrs { let attr_str = attr.path().to_string(); @@ -193,6 +201,19 @@ impl Catalyst { let attribute: TokenStream = content.parse()?; attributes.push(attribute); } + OVERRIDE if attr_str == COMPLEX => { + let content; + syn::parenthesized!(content in meta.input); + + let key: LitStr = content.parse()?; + content.parse::()?; + + let tokens: TokenStream = content.parse()?; + override_field_attributes + .entry(key.value()) + .or_default() + .push(tokens); + } BIND if attr_str == CATALYST => { // #[catalyst(bind = SubstrateStruct)] if let Some(lit) = get_struct(&meta)? { @@ -217,7 +238,10 @@ impl Catalyst { } if bind.is_empty() { - return Err(syn::Error::new(ident.span(), "No substrate for Catalyst, please specify with #[catalyst(bind = ...)]")); + 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(); @@ -234,26 +258,44 @@ impl Catalyst { fields, bind, keep_field_attribute, + override_field_attributes, }) } } impl Field { /// Generate the token stream for the Complex struct fields - pub fn to_token_stream(&self, keep_field_attribute: bool) -> Result { + pub fn to_token_stream( + &self, + keep_field_attribute: bool, + override_field_attributes: &HashMap>, + ) -> Result { let Field { attrs, attributes, ident, ty, } = self; + + if let Some(ident) = ident { + // from override + if let Some(attributes) = override_field_attributes.get(&ident.to_string()) { + return Ok(quote! { + #( #[ #attributes ] )* + pub #ident: #ty, + }); + } + } + if keep_field_attribute { if attrs.len() > 0 { + // from substrate Ok(quote! { #( #attrs )* pub #ident: #ty, }) } else { + // from catalyst Ok(quote! { #( #[ #attributes ] )* pub #ident: #ty, @@ -268,14 +310,23 @@ impl Field { /// Generate the token stream for unpack Complex struct fields pub fn to_unpack_stream(&self) -> Result { - let Field { ident, ty: _, attributes: _, attrs: _ } = self; + let Field { + ident, + ty: _, + attributes: _, + attrs: _, + } = self; Ok(quote! { #ident, }) } /// Parse the Catalyst struct field - pub fn from_cat_ast(syn::Field { ident, ty, attrs, .. }: syn::Field) -> Field { + pub fn from_cat_ast( + syn::Field { + ident, ty, attrs, .. + }: syn::Field, + ) -> Field { let mut attributes = Vec::new(); for attr in attrs.iter() { let attr_str = attr.path().to_string(); @@ -297,10 +348,24 @@ impl Field { Ok(()) }); } - Field { ident, ty, attributes, attrs: Vec::new() } + Field { + ident, + ty, + attributes, + attrs: Vec::new(), + } } - pub fn from_ast(syn::Field { ident, ty, attrs, .. }: syn::Field) -> Field { - Field { ident, ty, attrs, attributes: Vec::new() } + pub fn from_ast( + syn::Field { + ident, ty, attrs, .. + }: syn::Field, + ) -> Field { + Field { + ident, + ty, + attrs, + attributes: Vec::new(), + } } } From 0d8c36f21c40508a53ab2b6812c1433306c85477 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Thu, 28 May 2026 18:59:09 +0800 Subject: [PATCH 3/3] ci: use dependabot --- .github/dependabot.yml | 20 ++++++++ flake.lock | 112 ++--------------------------------------- flake.nix | 23 +-------- 3 files changed, 25 insertions(+), 130 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ace460..b14e5af 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,23 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/lib" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/derive" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/no-std-examples" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/complex-example" + schedule: + interval: "weekly" diff --git a/flake.lock b/flake.lock index 4235ae2..c0799ab 100644 --- a/flake.lock +++ b/flake.lock @@ -1,25 +1,5 @@ { "nodes": { - "dependency-refresh": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - }, - "locked": { - "lastModified": 1762135743, - "narHash": "sha256-keA+IvUbKtvEnnMZn5kWeTVgOjFxy65thZGHbLQh2Ks=", - "owner": "yanganto", - "repo": "dependency-refresh", - "rev": "8816e18b6f6ddfffc33104ae69d9a87afcaa3fc7", - "type": "github" - }, - "original": { - "owner": "yanganto", - "repo": "dependency-refresh", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -38,57 +18,7 @@ "type": "github" } }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { - "locked": { - "lastModified": 1761999846, - "narHash": "sha256-IYlYnp4O4dzEpL77BD/lj5NnJy2J8qbHkNSFiPBCbqo=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3de8f8d73e35724bf9abef41f1bdbedda1e14a31", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { "locked": { "lastModified": 1776830795, "narHash": "sha256-Gg5hJkg5jCRpgqnWrMRsv91BaPSD4i30VIz2VO2ojkI=", @@ -104,7 +34,7 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_2": { "locked": { "lastModified": 1744536153, "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", @@ -122,34 +52,15 @@ }, "root": { "inputs": { - "dependency-refresh": "dependency-refresh", - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_3", - "rust-overlay": "rust-overlay_2" + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" } }, "rust-overlay": { "inputs": { "nixpkgs": "nixpkgs_2" }, - "locked": { - "lastModified": 1762051177, - "narHash": "sha256-pESNTx/m3WnrYx+OujBtDP5Bj0/mAyHa4MgEwzkgkLE=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "08c33e87c4829bbdd42b5af247cf7a19e126369f", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "rust-overlay_2": { - "inputs": { - "nixpkgs": "nixpkgs_4" - }, "locked": { "lastModified": 1776827647, "narHash": "sha256-sYixYhp5V8jCajO8TRorE4fzs7IkL4MZdfLTKgkPQBk=", @@ -178,21 +89,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 4c693e6..390d5d2 100644 --- a/flake.nix +++ b/flake.nix @@ -4,18 +4,15 @@ nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11-small"; rust-overlay.url = "github:oxalica/rust-overlay"; flake-utils.url = "github:numtide/flake-utils"; - dependency-refresh.url = "github:yanganto/dependency-refresh"; }; - outputs = { self, rust-overlay, nixpkgs, flake-utils, dependency-refresh }: + outputs = { self, rust-overlay, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let overlays = [ (import rust-overlay) ]; pkgs = import nixpkgs { inherit system overlays; }; - dr = dependency-refresh.defaultPackage.${system}; - publishScript = pkgs.writeShellScriptBin "crate-publish" '' cargo login $1 cargo publish -p struct-patch-derive || echo "publish struct-patch-derive fail" @@ -28,22 +25,6 @@ cargo test -p substrate cargo test -p catalyst ''; - updateDependencyScript = pkgs.writeShellScriptBin "update-dependency" '' - dr ./Cargo.toml - - cd no-std-examples - dr ./Cargo.toml - - 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 - ''; in with pkgs; { @@ -72,9 +53,7 @@ openssl pkg-config - dr publishScript - updateDependencyScript checkCatalystScript ];