From 5d67040bc4c65a56dd46ad5c3bf3e6ebef67c84a Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 8 Sep 2026 21:46:53 +0800 Subject: [PATCH 1/8] readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8762a68..152a9cf 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Deriving `Substrate` on a struct exposes the field information so that other cra Deriving `Catalyst` reads the field information of a `Substrate` and generates a new complex struct. In the other words, the catalyst is a struct with extra fields that the developer writes down in the downstream crate. The complex is a generated struct that combines the substrate's fields with the catalyst's extra fields. The overall behavior is like [chemical catalysts](https://en.wikipedia.org/wiki/Enzyme_catalysis): a catalyst **binds** onto a substrate to form a complex struct, which has all fields from both. A complex can also **decouple** without cloning, returning the original catalyst and substrate. Check the [complex-examples](./examples/complex-examples/catalyst/src/lib.rs). -With the `unsafe` feature, `bind` and `decouple` use `ManuallyDrop` + `ptr::read` to avoid memory moves, and `__substrate_new` uses `MaybeUninit` + `ptr::write` while `__substrate_unpack` uses `ManuallyDrop` + `ptr::read`, such that the copy will be less. +With the `unsafe` feature, `bind` and `decouple` avoid memory moves, the copy will be less. In terms of crate dependencies, the crate using `Substrate` is **upstream** (a dependency), and the crate using `Catalyst` is **downstream** (it depends on the substrate crate). There are two ways for the downstream crate to read the substrate's field layout: @@ -274,7 +274,7 @@ This crate includes the following optional features: - `nesting` *(optional)*: allows a field to use `Patch` derive with the `#[patch(nesting)]` attribute. - `substrate` *(optional)*: enables the `Substrate` derive macro for exposing a struct's field layout so downstream crates can access it via `expose()` or source parsing. - `catalyst` *(optional)*: enables the `Catalyst` and `Complex` derive macros for extending a struct with fields from another crate. Implies `substrate`. -- `unsafe` *(optional)*: uses `ManuallyDrop` + `ptr::read` / `MaybeUninit` + `ptr::write` in the generated `bind`, `decouple`, `__substrate_new`, and `__substrate_unpack` to avoid memory moves. Only meaningful with the `catalyst` feature. +- `unsafe` *(optional)*: avoid memory moves. Only meaningful with the `catalyst` feature. [crates-badge]: https://img.shields.io/crates/v/struct-patch.svg [crate-url]: https://crates.io/crates/struct-patch From a6e09d9872d7df10f28ebae200b723beccde242d Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 8 Sep 2026 22:54:33 +0800 Subject: [PATCH 2/8] update log with nesting --- examples/patch-examples/examples/log.rs | 59 +++++++++++++++++++++++++ flake.nix | 3 +- nix/scripts/check-patch.sh | 4 +- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/examples/patch-examples/examples/log.rs b/examples/patch-examples/examples/log.rs index a1223f4..7986585 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -19,6 +19,18 @@ struct Config { debug: bool, } +// --- Patch with nesting example --- + +#[cfg(feature = "nesting")] +#[derive(Default, Patch)] +#[patch(attribute(derive(Debug, Default)))] +#[patch(default_log(log_patch_field))] +struct Server { + name: String, + #[patch(nesting)] + config: Config, +} + // --- Filler example --- #[derive(Default, Filler)] @@ -109,4 +121,51 @@ fn main() { "theme={:?}, max_connections={:?}", settings2.theme, settings2.max_connections ); + + // --- Patch with nesting and default_log --- + #[cfg(feature = "nesting")] + { + println!("\n--- Patch: apply() with nesting and default_log ---"); + let mut server = Server::default(); + server.apply(ServerPatch { + name: Some("prod-server".into()), + config: ConfigPatch { + host: Some("192.168.1.1".into()), + port: Some(443), + debug: None, + }, + }); + // Prints: + // [default_log] patch field: name + // [default_log] patch field: host + // [default_log] patch field: port + + println!( + "name={}, config.host={}, config.port={}, config.debug={}", + server.name, server.config.host, server.config.port, server.config.debug + ); + + // --- Patch with nesting and apply_with_log --- + println!("\n--- Patch: apply_with_log() with nesting ---"); + let mut server2 = Server::default(); + server2.apply_with_log( + ServerPatch { + name: None, + config: ConfigPatch { + host: Some("10.0.0.1".into()), + port: None, + debug: Some(false), + }, + }, + |field| println!("[custom_log] patch field: '{field}'"), + ); + // Prints: + // [custom_log] patch field: 'host' + // [custom_log] patch field: 'debug' + + println!( + "name={}, config.host={}, config.port={}, config.debug={}", + server2.name, server2.config.host, server2.config.port, server2.config.debug + ); + } } diff --git a/flake.nix b/flake.nix index fb330d7..7f154a9 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,8 @@ [[ -n $(git status --porcelain) ]] && dirty='*' echo "<$branch$dirty>" } - PS1='\[\e[33m\][$DEVSHELL] \w $(_git_ps1) \$\[\e[0m\] ' + export PS1='\[\e[33m\][$DEVSHELL] \w $(_git_ps1) \$\[\e[0m\] ' + export PS4='\033[31m ⊙ \033[0m' ''; in { diff --git a/nix/scripts/check-patch.sh b/nix/scripts/check-patch.sh index c1bf126..2ecd5a9 100644 --- a/nix/scripts/check-patch.sh +++ b/nix/scripts/check-patch.sh @@ -13,6 +13,7 @@ run_no_default() { cargo run --quiet --no-default-features --features=nesting --example nesting cargo run --quiet --no-default-features --features=option --example option cargo run --quiet --no-default-features --example log + cargo run --quiet --no-default-features --features=nesting --example log cargo run --quiet --no-default-features --example apply-by cargo run --quiet --no-default-features --features=box --example box } @@ -48,8 +49,7 @@ run_default() { cargo run --quiet --features=nesting --example nesting cargo run --quiet --features=nesting --example clap cargo run --quiet --example log - cargo run --quiet --example apply-by - cargo run --quiet --features=box --example box + cargo run --quiet --example apply-by cargo run --quiet --features=box --example box } case "${1:-}" in From faed6588414e0f7a34df79722a53f9d297b081fe Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 8 Sep 2026 23:03:45 +0800 Subject: [PATCH 3/8] adding nesting on nesting example for log --- examples/patch-examples/examples/log.rs | 98 ++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/examples/patch-examples/examples/log.rs b/examples/patch-examples/examples/log.rs index 7986585..a4ad90a 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -21,6 +21,25 @@ struct Config { // --- Patch with nesting example --- +#[cfg(feature = "nesting")] +#[derive(Default, Patch)] +#[patch(attribute(derive(Debug, Default)))] +struct Logging { + level: String, + format: String, +} + +#[cfg(feature = "nesting")] +#[derive(Default, Patch)] +#[patch(attribute(derive(Debug, Default)))] +#[patch(default_log(log_patch_field))] +struct ConfigWithLogging { + host: String, + port: u16, + #[patch(nesting)] + logging: Logging, +} + #[cfg(feature = "nesting")] #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] @@ -28,7 +47,7 @@ struct Config { struct Server { name: String, #[patch(nesting)] - config: Config, + config: ConfigWithLogging, } // --- Filler example --- @@ -129,10 +148,10 @@ fn main() { let mut server = Server::default(); server.apply(ServerPatch { name: Some("prod-server".into()), - config: ConfigPatch { + config: ConfigWithLoggingPatch { host: Some("192.168.1.1".into()), port: Some(443), - debug: None, + logging: LoggingPatch::default(), }, }); // Prints: @@ -141,8 +160,8 @@ fn main() { // [default_log] patch field: port println!( - "name={}, config.host={}, config.port={}, config.debug={}", - server.name, server.config.host, server.config.port, server.config.debug + "name={}, config.host={}, config.port={}", + server.name, server.config.host, server.config.port ); // --- Patch with nesting and apply_with_log --- @@ -151,21 +170,80 @@ fn main() { server2.apply_with_log( ServerPatch { name: None, - config: ConfigPatch { + config: ConfigWithLoggingPatch { host: Some("10.0.0.1".into()), port: None, - debug: Some(false), + logging: LoggingPatch::default(), }, }, |field| println!("[custom_log] patch field: '{field}'"), ); // Prints: // [custom_log] patch field: 'host' - // [custom_log] patch field: 'debug' println!( - "name={}, config.host={}, config.port={}, config.debug={}", - server2.name, server2.config.host, server2.config.port, server2.config.debug + "name={}, config.host={}, config.port={}", + server2.name, server2.config.host, server2.config.port + ); + + // --- Patch with deep nesting (nesting within nesting) and default_log --- + println!("\n--- Patch: apply() with deep nesting and default_log ---"); + let mut server3 = Server::default(); + server3.apply(ServerPatch { + name: Some("app-server".into()), + config: ConfigWithLoggingPatch { + host: Some("localhost".into()), + port: Some(8080), + logging: LoggingPatch { + level: Some("debug".into()), + format: Some("json".into()), + }, + }, + }); + // Prints: + // [default_log] patch field: name + // [default_log] patch field: host + // [default_log] patch field: port + // [default_log] patch field: level + // [default_log] patch field: format + + println!( + "name={}, config.host={}, config.port={}, config.logging.level={}, config.logging.format={}", + server3.name, + server3.config.host, + server3.config.port, + server3.config.logging.level, + server3.config.logging.format + ); + + // --- Patch with deep nesting and apply_with_log --- + println!("\n--- Patch: apply_with_log() with deep nesting ---"); + let mut server4 = Server::default(); + server4.apply_with_log( + ServerPatch { + name: None, + config: ConfigWithLoggingPatch { + host: None, + port: Some(9000), + logging: LoggingPatch { + level: Some("warn".into()), + format: None, + }, + }, + }, + |field| println!("[custom_log] patch field: '{field}'"), + ); + // Prints: + // [custom_log] patch field: 'port' + // [custom_log] patch field: 'level' + + println!( + "name={}, config.host={}, config.port={}, config.logging.level={}, config.logging.format={}", + server4.name, + server4.config.host, + server4.config.port, + server4.config.logging.level, + server4.config.logging.format ); } } From 03d3f4b417c2e8ea943808b148db55fca9c1da3f Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Wed, 9 Sep 2026 00:11:02 +0800 Subject: [PATCH 4/8] impl log x nesting feature --- .gitignore | 1 + derive/src/filler.rs | 101 ++++++++++++- derive/src/patch.rs | 93 ++++++++++-- docs/logs.md | 41 ++++-- examples/filler-examples/Cargo.toml | 1 + examples/filler-examples/examples/log.rs | 88 ++++++++++++ examples/patch-examples/examples/box.rs | 18 ++- examples/patch-examples/examples/instance.rs | 2 +- examples/patch-examples/examples/log.rs | 141 ++++++++----------- lib/src/traits.rs | 47 +++++-- nix/scripts/check-filler.sh | 3 + 11 files changed, 410 insertions(+), 126 deletions(-) create mode 100644 examples/filler-examples/examples/log.rs diff --git a/.gitignore b/.gitignore index 3ed6912..ace852d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ struct-patch/examples examples/no-std-examples/target examples/complex-examples/target examples/patch-examples/target +examples/filler-examples/target diff --git a/derive/src/filler.rs b/derive/src/filler.rs index 3489578..d840fff 100644 --- a/derive/src/filler.rs +++ b/derive/src/filler.rs @@ -12,6 +12,7 @@ const EXTENDABLE: &str = "extendable"; const EMPTY_VALUE: &str = "empty_value"; const ADDABLE: &str = "addable"; const DEFAULT_LOG: &str = "default_log"; +const NESTING: &str = "nesting"; pub(crate) struct Filler { visibility: syn::Visibility, @@ -55,6 +56,8 @@ struct Field { fty: FillerType, #[cfg(feature = "op")] addable: Addable, + #[cfg(feature = "nesting")] + nesting: bool, } impl Filler { @@ -126,6 +129,17 @@ impl Filler { .map(|f| !matches!(f.addable, Addable::Disable)) .collect::>(); + // Nesting fields + #[cfg(not(feature = "nesting"))] + let nesting_field_names: Vec> = Vec::new(); + + #[cfg(feature = "nesting")] + let nesting_field_names = fields + .iter() + .filter(|f| f.nesting) + .map(|f| f.ident.as_ref()) + .collect::>(); + let mapped_attributes = attributes .iter() .map(|a| { @@ -163,6 +177,11 @@ impl Filler { return false } )* + #( + if !self.#nesting_field_names.is_empty() { + return false + } + )* true } } @@ -232,7 +251,7 @@ impl Filler { if let Some(f) = default_log_fn { names .iter() - .map(|n| quote! { #f(stringify!(#n)); }) + .map(|n| quote! { #f(&[], stringify!(#n)); }) .collect() } else { names.iter().map(|_| quote! {}).collect() @@ -242,6 +261,26 @@ impl Filler { let extendable_log_calls = make_log_calls(&extendable_field_names); let option_log_calls = make_log_calls(&option_field_names); + // For the `apply` method: propagate `default_log_fn` into nesting fields + #[cfg(feature = "nesting")] + let nesting_apply_section: TokenStream = if let Some(ref f) = default_log_fn { + quote! { + #( + self.#nesting_field_names.apply_with_log(filler.#nesting_field_names, |_prefixes: &[&str], field: &str| { + #f(&[], field); + }); + )* + } + } else { + quote! { + #( + self.#nesting_field_names.apply(filler.#nesting_field_names); + )* + } + }; + #[cfg(not(feature = "nesting"))] + let nesting_apply_section: TokenStream = quote! {}; + let filler_impl = quote! { #[automatically_derived] impl #generics struct_patch::traits::Filler< #name #generics > for #struct_name #generics #where_clause { @@ -266,36 +305,71 @@ impl Filler { } } )* + #nesting_apply_section } - fn apply_with_log<__L: FnMut(&str)>(&mut self, filler: #name #generics, mut log: __L) { + #[cfg(not(feature = "nesting"))] + fn apply_with_log<__L: FnMut(&[&str], &str)>(&mut self, filler: #name #generics, mut log: __L) { #( if self.#native_value_field_names == #native_value_field_empty_values { - log(stringify!(#native_value_field_names)); + log(&[], stringify!(#native_value_field_names)); self.#native_value_field_names = filler.#native_value_field_names; } )* #( if self.#extendable_field_names.is_empty() { - log(stringify!(#extendable_field_names)); + log(&[], stringify!(#extendable_field_names)); self.#extendable_field_names.extend(filler.#extendable_field_names.into_iter()); } )* #( if let Some(v) = filler.#option_field_names { if self.#option_field_names.is_none() { - log(stringify!(#option_field_names)); + log(&[], stringify!(#option_field_names)); self.#option_field_names = Some(v); } } )* } + #[cfg(feature = "nesting")] + fn apply_with_log<__L: FnMut(&[&str], &str)>(&mut self, filler: #name #generics, mut log: __L) { + #( + if self.#native_value_field_names == #native_value_field_empty_values { + log(&[], stringify!(#native_value_field_names)); + self.#native_value_field_names = filler.#native_value_field_names; + } + )* + #( + if self.#extendable_field_names.is_empty() { + log(&[], stringify!(#extendable_field_names)); + self.#extendable_field_names.extend(filler.#extendable_field_names.into_iter()); + } + )* + #( + if let Some(v) = filler.#option_field_names { + if self.#option_field_names.is_none() { + log(&[], stringify!(#option_field_names)); + self.#option_field_names = Some(v); + } + } + )* + #( + let nesting_field_name = stringify!(#nesting_field_names); + self.#nesting_field_names.apply_with_log(filler.#nesting_field_names, |prefixes: &[&str], field: &str| { + let mut new_prefixes = Vec::from(prefixes); + new_prefixes.push(nesting_field_name); + log(&new_prefixes, field); + }); + )* + } + fn new_empty_filler() -> #name #generics { #name { #(#option_field_names: None,)* #(#extendable_field_names: #extendable_field_types::default(),)* #(#native_value_field_names: #native_value_field_empty_values,)* + #(#nesting_field_names: Default::default(),)* } } } @@ -432,6 +506,8 @@ impl Field { let mut attributes = vec![]; #[cfg(feature = "op")] let mut addable = Addable::Disable; + #[cfg(feature = "nesting")] + let mut nesting = false; for attr in attrs { if attr.path().to_string().as_str() != FILLER { @@ -488,6 +564,19 @@ impl Field { "`addable` needs `op` feature", )); }, + #[cfg(feature = "nesting")] + NESTING => { + // #[filler(nesting)] + nesting = true; + } + #[cfg(not(feature = "nesting"))] + NESTING => { + use syn::spanned::Spanned; + return Err(syn::Error::new( + ident.span(), + "`nesting` needs `nesting` feature", + )); + }, _ => { return Err(meta.error(format_args!( "unknown patch field attribute `{}`", @@ -506,6 +595,8 @@ impl Field { fty, #[cfg(feature = "op")] addable, + #[cfg(feature = "nesting")] + nesting, })) } } diff --git a/derive/src/patch.rs b/derive/src/patch.rs index 8523c1b..7e23e49 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -765,13 +765,13 @@ impl Patch { let op_impl = quote!(); // Per-field log-call token streams, parallel with each field-name vec. - // Emit `default_log_fn(stringify!(field));` when a struct-level log is configured, + // Emit `default_log_fn(&[], stringify!(field));` when a struct-level log is configured, // or an empty token stream otherwise. let make_log_calls = |names: &[Option<&Ident>]| -> Vec { if let Some(f) = default_log_fn { names .iter() - .map(|n| quote! { #f(stringify!(#n)); }) + .map(|n| quote! { #f(&[], stringify!(#n)); }) .collect() } else { names.iter().map(|_| quote! {}).collect() @@ -796,7 +796,12 @@ impl Patch { let nesting_apply_section: TokenStream = if let Some(ref f) = default_log_fn { quote! { #( - self.#nesting_field_names.apply_with_log(patch.#nesting_field_names, #f); + let nesting_field_name = stringify!(#nesting_field_names); + self.#nesting_field_names.apply_with_log(patch.#nesting_field_names, |prefixes: &[&str], field: &str| { + let mut new_prefixes = Vec::from(prefixes); + new_prefixes.push(nesting_field_name); + #f(&new_prefixes, field); + }); )* } } else { @@ -866,40 +871,41 @@ impl Patch { #nesting_apply_section } - fn apply_with_log(&mut self, patch: #name #generics, mut log: F) { + #[cfg(not(feature = "nesting"))] + fn apply_with_log(&mut self, patch: #name #generics, mut log: F) { #( if let Some(v) = patch.#renamed_field_names { - log(stringify!(#renamed_field_names)); + log(&[], stringify!(#renamed_field_names)); self.#renamed_field_names.apply(v); } )* #( if patch.#renamed_field_names_by_empty_value != #renamed_field_name_empty_values { - log(stringify!(#renamed_field_names_by_empty_value)); + log(&[], stringify!(#renamed_field_names_by_empty_value)); self.#renamed_field_names_by_empty_value.apply(patch.#renamed_field_names_by_empty_value); } )* #( if let Some(v) = patch.#original_field_names { - log(stringify!(#original_field_names)); + log(&[], stringify!(#original_field_names)); self.#original_field_names = v; } )* #( if patch.#original_field_names_by_empty_value != #original_field_name_empty_values { - log(stringify!(#original_field_names_by_empty_value)); + log(&[], stringify!(#original_field_names_by_empty_value)); self.#original_field_names_by_empty_value = patch.#original_field_names_by_empty_value; } )* #( if let Some(v) = patch.#skip_wrap_field_names { - log(stringify!(#skip_wrap_field_names)); + log(&[], stringify!(#skip_wrap_field_names)); self.#skip_wrap_field_names = Some(v); } )* #( if let Some(v) = patch.#skip_wrap_apply_by_option_field_names { - log(stringify!(#skip_wrap_apply_by_option_field_names)); + log(&[], stringify!(#skip_wrap_apply_by_option_field_names)); if let Some(ref mut orig) = self.#skip_wrap_apply_by_option_field_names { #skip_wrap_apply_by_option_fns(orig, v); } @@ -907,18 +913,77 @@ impl Patch { )* #( { - log(stringify!(#skip_wrap_apply_by_plain_field_names)); + log(&[], stringify!(#skip_wrap_apply_by_plain_field_names)); #skip_wrap_apply_by_plain_fns(&mut self.#skip_wrap_apply_by_plain_field_names, patch.#skip_wrap_apply_by_plain_field_names); } )* #( if let Some(v) = patch.#apply_by_field_names { - log(stringify!(#apply_by_field_names)); + log(&[], stringify!(#apply_by_field_names)); + #apply_by_fns(&mut self.#apply_by_field_names, v); + } + )* + } + + #[cfg(feature = "nesting")] + fn apply_with_log(&mut self, patch: #name #generics, mut log: F) { + #( + if let Some(v) = patch.#renamed_field_names { + log(&[], stringify!(#renamed_field_names)); + self.#renamed_field_names.apply(v); + } + )* + #( + if patch.#renamed_field_names_by_empty_value != #renamed_field_name_empty_values { + log(&[], stringify!(#renamed_field_names_by_empty_value)); + self.#renamed_field_names_by_empty_value.apply(patch.#renamed_field_names_by_empty_value); + } + )* + #( + if let Some(v) = patch.#original_field_names { + log(&[], stringify!(#original_field_names)); + self.#original_field_names = v; + } + )* + #( + if patch.#original_field_names_by_empty_value != #original_field_name_empty_values { + log(&[], stringify!(#original_field_names_by_empty_value)); + self.#original_field_names_by_empty_value = patch.#original_field_names_by_empty_value; + } + )* + #( + if let Some(v) = patch.#skip_wrap_field_names { + log(&[], stringify!(#skip_wrap_field_names)); + self.#skip_wrap_field_names = Some(v); + } + )* + #( + if let Some(v) = patch.#skip_wrap_apply_by_option_field_names { + log(&[], stringify!(#skip_wrap_apply_by_option_field_names)); + if let Some(ref mut orig) = self.#skip_wrap_apply_by_option_field_names { + #skip_wrap_apply_by_option_fns(orig, v); + } + } + )* + #( + { + log(&[], stringify!(#skip_wrap_apply_by_plain_field_names)); + #skip_wrap_apply_by_plain_fns(&mut self.#skip_wrap_apply_by_plain_field_names, patch.#skip_wrap_apply_by_plain_field_names); + } + )* + #( + if let Some(v) = patch.#apply_by_field_names { + log(&[], stringify!(#apply_by_field_names)); #apply_by_fns(&mut self.#apply_by_field_names, v); } )* #( - self.#nesting_field_names.apply_with_log(patch.#nesting_field_names, &mut log); + let nesting_field_name = stringify!(#nesting_field_names); + self.#nesting_field_names.apply_with_log(patch.#nesting_field_names, |prefixes: &[&str], field: &str| { + let mut new_prefixes = Vec::from(prefixes); + new_prefixes.push(nesting_field_name); + log(&new_prefixes, field); + }); )* } @@ -1066,7 +1131,7 @@ impl Patch { struct_patch::traits::Patch::apply(self, *patch); } - fn apply_with_log<__F: ::core::ops::FnMut(&str)>( + fn apply_with_log<__F: ::core::ops::FnMut(&[&str], &str)>( &mut self, patch: struct_patch::__Box< #name #generics >, log: __F, diff --git a/docs/logs.md b/docs/logs.md index f61825b..b4aca2d 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -3,7 +3,7 @@ Both `Patch` and `Filler` support two ways to observe which fields are changed: **Ad-hoc at the call site** — use `apply_with_log`, which takes a closure that -is called with each patched/filled field name: +is called with each patched/filled field name and its nesting path: ```rust use struct_patch::{Filler, Patch}; @@ -18,7 +18,14 @@ let mut item = Item::default(); let patch = ItemPatch { field_int: Some(42), field_string: None }; let mut patched_fields = Vec::new(); -item.apply_with_log(patch, |field| patched_fields.push(field.to_string())); +item.apply_with_log(patch, |prefixes, field| { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + patched_fields.push(path); +}); assert_eq!(patched_fields, vec!["field_int"]); assert_eq!(item.field_int, 42); @@ -32,13 +39,21 @@ let mut settings = Settings::default(); let mut filled_fields = Vec::new(); settings.apply_with_log( SettingsFiller { theme: Some("dark".into()) }, - |field| filled_fields.push(field.to_string()), + |prefixes, field| { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + filled_fields.push(path); + }, ); assert_eq!(filled_fields, vec!["theme"]); ``` For structs using `#[patch(nesting)]`, the log closure is threaded into nested -patches so you receive field names from all levels of nesting. +patches so you receive field names with prefixes showing the path through nested +structures. **Always-on via struct attribute** — use `#[patch(default_log(fn_path))]` or `#[filler(default_log(fn_path))]` to wire a specific function into `apply` @@ -49,8 +64,13 @@ Has no effect on `apply_with_log`. ```rust use struct_patch::{Filler, Patch}; -fn my_log(field: &str) { - println!("patched: {field}"); +fn my_log(prefixes: &[&str], field: &str) { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("patched: {path}"); } #[derive(Default, Patch)] @@ -64,8 +84,13 @@ let mut cfg = Config::default(); cfg.apply(ConfigPatch { retries: Some(3), timeout: None }); // prints: patched: retries -fn my_filler_log(field: &str) { - println!("filled: {field}"); +fn my_filler_log(prefixes: &[&str], field: &str) { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("filled: {path}"); } #[derive(Default, Filler)] diff --git a/examples/filler-examples/Cargo.toml b/examples/filler-examples/Cargo.toml index 07ae398..1ded1fb 100644 --- a/examples/filler-examples/Cargo.toml +++ b/examples/filler-examples/Cargo.toml @@ -12,3 +12,4 @@ struct-patch = { path = "../../lib" } default = ["status", "op"] status = ["struct-patch/status"] op = ["struct-patch/op"] +nesting = ["struct-patch/nesting"] diff --git a/examples/filler-examples/examples/log.rs b/examples/filler-examples/examples/log.rs new file mode 100644 index 0000000..4ddf7e3 --- /dev/null +++ b/examples/filler-examples/examples/log.rs @@ -0,0 +1,88 @@ +use struct_patch::Filler; + +#[cfg(not(feature = "nesting"))] +fn log_filler_field(field: &str) { + println!("[default_log] filler field: {field}"); +} + +#[cfg(feature = "nesting")] +fn log_filler_field(prefixes: &[&str], field: &str) { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("[default_log] filler field: {path}"); +} + +#[cfg(not(feature = "nesting"))] +fn log_filler_field_wrapper(_prefixes: &[&str], field: &str) { + log_filler_field(field); +} + +#[cfg(feature = "nesting")] +fn log_filler_field_wrapper(prefixes: &[&str], field: &str) { + log_filler_field(prefixes, field); +} + +#[derive(Default, Filler)] +#[filler(attribute(derive(Debug, Default)))] +#[filler(default_log(log_filler_field_wrapper))] +struct Settings { + theme: Option, + max_connections: Option, +} + +fn main() { + // --- Filler with default_log --- + println!("--- Filler: apply() with default_log ---"); + let mut settings = Settings::default(); + settings.apply(SettingsFiller { + theme: Some("dark".into()), + max_connections: Some(100), + }); + // Prints: + // [default_log] filler field: theme + // [default_log] filler field: max_connections + + println!( + "theme={:?}, max_connections={:?}", + settings.theme, settings.max_connections + ); + + // Applying again has no effect because the fields are already filled. + println!("\n--- Filler: apply() again (fields already filled, no log) ---"); + settings.apply(SettingsFiller { + theme: Some("light".into()), + max_connections: Some(999), + }); + println!( + "theme={:?}, max_connections={:?}", + settings.theme, settings.max_connections + ); + + // --- Filler with apply_with_log (custom format) --- + println!("\n--- Filler: apply_with_log() with custom format ---"); + let mut settings2 = Settings::default(); + settings2.apply_with_log( + SettingsFiller { + theme: Some("light".into()), + max_connections: None, + }, + |prefixes, field| { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("[custom_log] filler field '{}' was filled", path); + }, + ); + // Prints: + // [custom_log] filler field 'theme' was filled + + println!( + "theme={:?}, max_connections={:?}", + settings2.theme, settings2.max_connections + ); +} diff --git a/examples/patch-examples/examples/box.rs b/examples/patch-examples/examples/box.rs index c97bcbb..a8b835b 100644 --- a/examples/patch-examples/examples/box.rs +++ b/examples/patch-examples/examples/box.rs @@ -1,7 +1,12 @@ use struct_patch::Patch; -fn log_field(field: &str) { - println!("[default_log] field changed: {field}"); +fn log_field(prefixes: &[&str], field: &str) { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("[default_log] field changed: {path}"); } #[derive(Default, Patch)] @@ -52,7 +57,14 @@ fn main() { port: None, debug: Some(true), }), - |field| patched_fields.push(field.to_string()), + |prefixes, field| { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + patched_fields.push(path); + }, ); assert!(config.debug); diff --git a/examples/patch-examples/examples/instance.rs b/examples/patch-examples/examples/instance.rs index dbee17e..61de624 100644 --- a/examples/patch-examples/examples/instance.rs +++ b/examples/patch-examples/examples/instance.rs @@ -22,7 +22,7 @@ struct Item { // } fn main() { - fn log(field: &str) { + fn log(_prefixed: &[&str], field: &str) { println!("TRACE: {field} patched") } diff --git a/examples/patch-examples/examples/log.rs b/examples/patch-examples/examples/log.rs index a4ad90a..94b1baa 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -1,26 +1,39 @@ -use struct_patch::{Filler, Patch}; +use struct_patch::Patch; +#[cfg(not(feature = "nesting"))] fn log_patch_field(field: &str) { println!("[default_log] patch field: {field}"); } -fn log_filler_field(field: &str) { - println!("[default_log] filler field: {field}"); +#[cfg(feature = "nesting")] +fn log_patch_field(prefixes: &[&str], field: &str) { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("[default_log] patch field: {path}"); +} + +#[cfg(not(feature = "nesting"))] +fn log_patch_field_wrapper(_prefixes: &[&str], field: &str) { + log_patch_field(field); } -// --- Patch example --- +#[cfg(feature = "nesting")] +fn log_patch_field_wrapper(prefixes: &[&str], field: &str) { + log_patch_field(prefixes, field); +} #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] -#[patch(default_log(log_patch_field))] +#[patch(default_log(log_patch_field_wrapper))] struct Config { host: String, port: u16, debug: bool, } -// --- Patch with nesting example --- - #[cfg(feature = "nesting")] #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] @@ -32,7 +45,7 @@ struct Logging { #[cfg(feature = "nesting")] #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] -#[patch(default_log(log_patch_field))] +#[patch(default_log(log_patch_field_wrapper))] struct ConfigWithLogging { host: String, port: u16, @@ -43,25 +56,14 @@ struct ConfigWithLogging { #[cfg(feature = "nesting")] #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] -#[patch(default_log(log_patch_field))] +#[patch(default_log(log_patch_field_wrapper))] struct Server { name: String, #[patch(nesting)] config: ConfigWithLogging, } -// --- Filler example --- - -#[derive(Default, Filler)] -#[filler(attribute(derive(Debug, Default)))] -#[filler(default_log(log_filler_field))] -struct Settings { - theme: Option, - max_connections: Option, -} - fn main() { - // --- Patch with default_log --- println!("--- Patch: apply() with default_log ---"); let mut config = Config::default(); config.apply(ConfigPatch { @@ -78,7 +80,6 @@ fn main() { config.host, config.port, config.debug ); - // --- Patch with apply_with_log (custom format) --- println!("\n--- Patch: apply_with_log() with custom format ---"); config.apply_with_log( ConfigPatch { @@ -86,7 +87,14 @@ fn main() { port: None, debug: Some(true), }, - |field| println!("[custom_log] patch field '{}' was updated", field), + |prefixes, field| { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("[custom_log] patch field '{}' was updated", path); + }, ); // Prints: // [custom_log] patch field 'debug' was updated @@ -96,52 +104,6 @@ fn main() { config.host, config.port, config.debug ); - // --- Filler with default_log --- - println!("\n--- Filler: apply() with default_log ---"); - let mut settings = Settings::default(); - settings.apply(SettingsFiller { - theme: Some("dark".into()), - max_connections: Some(100), - }); - // Prints: - // [default_log] filler field: theme - // [default_log] filler field: max_connections - - println!( - "theme={:?}, max_connections={:?}", - settings.theme, settings.max_connections - ); - - // Applying again has no effect because the fields are already filled. - println!("\n--- Filler: apply() again (fields already filled, no log) ---"); - settings.apply(SettingsFiller { - theme: Some("light".into()), - max_connections: Some(999), - }); - println!( - "theme={:?}, max_connections={:?}", - settings.theme, settings.max_connections - ); - - // --- Filler with apply_with_log (custom format) --- - println!("\n--- Filler: apply_with_log() with custom format ---"); - let mut settings2 = Settings::default(); - settings2.apply_with_log( - SettingsFiller { - theme: Some("light".into()), - max_connections: None, - }, - |field| println!("[custom_log] filler field '{}' was filled", field), - ); - // Prints: - // [custom_log] filler field 'theme' was filled - - println!( - "theme={:?}, max_connections={:?}", - settings2.theme, settings2.max_connections - ); - - // --- Patch with nesting and default_log --- #[cfg(feature = "nesting")] { println!("\n--- Patch: apply() with nesting and default_log ---"); @@ -156,16 +118,15 @@ fn main() { }); // Prints: // [default_log] patch field: name - // [default_log] patch field: host - // [default_log] patch field: port + // [default_log] patch field: config.host + // [default_log] patch field: config.port println!( "name={}, config.host={}, config.port={}", server.name, server.config.host, server.config.port ); - // --- Patch with nesting and apply_with_log --- - println!("\n--- Patch: apply_with_log() with nesting ---"); + println!("\n--- Patch: apply_with_log() with nesting and prefix path ---"); let mut server2 = Server::default(); server2.apply_with_log( ServerPatch { @@ -176,17 +137,23 @@ fn main() { logging: LoggingPatch::default(), }, }, - |field| println!("[custom_log] patch field: '{field}'"), + |prefixes, field| { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("[custom_log] patch field: '{path}'"); + }, ); // Prints: - // [custom_log] patch field: 'host' + // [custom_log] patch field: 'config.host' println!( "name={}, config.host={}, config.port={}", server2.name, server2.config.host, server2.config.port ); - // --- Patch with deep nesting (nesting within nesting) and default_log --- println!("\n--- Patch: apply() with deep nesting and default_log ---"); let mut server3 = Server::default(); server3.apply(ServerPatch { @@ -202,10 +169,10 @@ fn main() { }); // Prints: // [default_log] patch field: name - // [default_log] patch field: host - // [default_log] patch field: port - // [default_log] patch field: level - // [default_log] patch field: format + // [default_log] patch field: config.host + // [default_log] patch field: config.port + // [default_log] patch field: config.logging.level + // [default_log] patch field: config.logging.format println!( "name={}, config.host={}, config.port={}, config.logging.level={}, config.logging.format={}", @@ -216,8 +183,7 @@ fn main() { server3.config.logging.format ); - // --- Patch with deep nesting and apply_with_log --- - println!("\n--- Patch: apply_with_log() with deep nesting ---"); + println!("\n--- Patch: apply_with_log() with deep nesting and full path ---"); let mut server4 = Server::default(); server4.apply_with_log( ServerPatch { @@ -231,11 +197,18 @@ fn main() { }, }, }, - |field| println!("[custom_log] patch field: '{field}'"), + |prefixes, field| { + let path = if prefixes.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefixes.join("."), field) + }; + println!("[custom_log] patch field: '{path}'"); + }, ); // Prints: - // [custom_log] patch field: 'port' - // [custom_log] patch field: 'level' + // [custom_log] patch field: 'config.port' + // [custom_log] patch field: 'config.logging.level' println!( "name={}, config.host={}, config.port={}, config.logging.level={}, config.logging.format={}", diff --git a/lib/src/traits.rs b/lib/src/traits.rs index 9e63c3c..293da3f 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -61,12 +61,20 @@ /// ``` /// /// ### `#[patch(default_log(fn_path))]` -/// Automatically call `fn_path(&str)` with each patched field name inside +/// Automatically call `fn_path(&[&str], &str)` with prefixes and field name inside /// every generated `apply` call. Has no effect on `apply_with_log`. The path -/// may be any function path visible at the call site. +/// may be any function path visible at the call site. The `prefixes` slice contains +/// the path through nested structures (empty for top-level fields). /// ```rust /// # use struct_patch::Patch; -/// fn log_field(field: &str) { let _ = field; } +/// fn log_field(prefixes: &[&str], field: &str) { +/// let path = if prefixes.is_empty() { +/// field.to_string() +/// } else { +/// format!("{}.{}", prefixes.join("."), field) +/// }; +/// println!("patched: {path}"); +/// } /// /// #[derive(Default, Patch)] /// #[patch(default_log(log_field))] @@ -77,7 +85,7 @@ /// /// let mut item = Item::default(); /// item.apply(ItemPatch { field_int: Some(1), field_string: None }); -/// // log_field("field_int") is called automatically +/// // log_field(&[], "field_int") is called automatically /// ``` /// /// ## Field attributes @@ -132,11 +140,12 @@ pub trait Patch

{ /// Apply a patch fn apply(&mut self, patch: P); - /// Apply a patch, calling `log` with each patched field name. + /// Apply a patch, calling `log` with each patched field name and its nesting path. /// /// The default implementation ignores `log` and delegates to [`apply`](Patch::apply). /// The derive macro generates an override that calls `log` once per field that is - /// actually changed. + /// actually changed. The `prefixes` slice contains the path to the field through + /// nested structures (empty for top-level fields). /// /// ```rust /// # use struct_patch::Patch; @@ -150,11 +159,18 @@ pub trait Patch

{ /// let patch = ItemPatch { field_int: Some(42), field_string: None }; /// /// let mut patched_fields = Vec::new(); - /// item.apply_with_log(patch, |field| patched_fields.push(field.to_string())); + /// item.apply_with_log(patch, |prefixes, field| { + /// let path = if prefixes.is_empty() { + /// field.to_string() + /// } else { + /// format!("{}.{}", prefixes.join("."), field) + /// }; + /// patched_fields.push(path); + /// }); /// /// assert_eq!(patched_fields, vec!["field_int"]); /// ``` - fn apply_with_log(&mut self, patch: P, _log: F) { + fn apply_with_log(&mut self, patch: P, _log: F) { self.apply(patch); } @@ -172,11 +188,13 @@ pub trait Filler { /// Apply a filler fn apply(&mut self, filler: F); - /// Apply a filler, calling `log` with each field name that is actually filled. + /// Apply a filler, calling `log` with each field name that is actually filled and its nesting path. /// /// The default implementation ignores `log` and delegates to [`apply`](Filler::apply). /// The derive macro generates an override that calls `log` once per field that is /// actually filled (i.e. the field was empty and the filler supplied a value). + /// The `prefixes` slice contains the path to the field through nested structures + /// (empty for top-level fields). /// /// ```rust /// # use struct_patch::Filler; @@ -189,11 +207,18 @@ pub trait Filler { /// let filler = ItemFiller { value: Some(42) }; /// /// let mut filled_fields = Vec::new(); - /// item.apply_with_log(filler, |field| filled_fields.push(field.to_string())); + /// item.apply_with_log(filler, |prefixes, field| { + /// let path = if prefixes.is_empty() { + /// field.to_string() + /// } else { + /// format!("{}.{}", prefixes.join("."), field) + /// }; + /// filled_fields.push(path); + /// }); /// /// assert_eq!(filled_fields, vec!["value"]); /// ``` - fn apply_with_log(&mut self, filler: F, _log: L) { + fn apply_with_log(&mut self, filler: F, _log: L) { self.apply(filler); } diff --git a/nix/scripts/check-filler.sh b/nix/scripts/check-filler.sh index 9b56287..0609fb2 100644 --- a/nix/scripts/check-filler.sh +++ b/nix/scripts/check-filler.sh @@ -4,11 +4,14 @@ cd examples/filler-examples run_no_default() { cargo run --quiet --no-default-features --example filler + cargo run --quiet --no-default-features --example log } run_default() { cargo run --quiet --example filler cargo run --quiet --example filler-op + cargo run --quiet --example log + cargo run --quiet --features nesting --example log } case "${1:-}" in From 0d3600bcbaf153e4677de115200b05491921c942 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Thu, 10 Sep 2026 00:05:31 +0800 Subject: [PATCH 5/8] readme & doc --- README.md | 5 +++-- docs/logs.md | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 152a9cf..9635a62 100644 --- a/README.md +++ b/README.md @@ -199,9 +199,9 @@ Two attribute namespaces are provided for the catalyst feature because we need t - `#[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. -- `#[patch(default_log(fn_path))]`: call `fn_path` with each patched field name on every `apply` call. Has no effect on `apply_with_log`. The function must accept `&str`. +- `#[patch(default_log(fn_path))]`: call `fn_path` with each patched field name on every `apply` call. Has no effect on `apply_with_log`. Function signature: `fn(&str)` without nesting feature, or `fn(&[&str], &str)` with nesting feature. - `#[filler(attribute(...))]`: add attributes to the generated filler struct. -- `#[filler(default_log(fn_path))]`: call `fn_path` with each filled field name on every `apply` call. Has no effect on `apply_with_log`. The function must accept `&str`. +- `#[filler(default_log(fn_path))]`: call `fn_path` with each filled field name on every `apply` call. Has no effect on `apply_with_log`. Function signature: `fn(&str)` without nesting feature, or `fn(&[&str], &str)` with nesting feature. - `#[catalyst(bind = ...)]`: specify the base (substrate) structure. Need substrate expose() in build (catalyst feature) - `#[catalyst(bind = ..., src = "crate_name:/path/to/file")]`: specify the base (substrate) structure. No need substrate expose() and based on source code. Avoide syn protocol change (catalyst feature) - `#[catalyst(keep_field_attribute)]`: pass all field attributes from a substrate or catalyst through to the complex, unless an override is explicitly specified for that field. (catalyst feature) @@ -252,6 +252,7 @@ Examples are organised into focused sub-projects under [`examples/`](./examples) **[filler-examples](./examples/filler-examples)** — `Filler` derive macro scenarios: - show filler with all possible types (`filler.rs`) - show operators on fillers (`filler-op.rs`) +- demonstrate `default_log` and `apply_with_log` for `Filler` with optional nesting support (`log.rs`) **[no-std-examples](./examples/no-std-examples)** — `no_std` usage with a bare-metal target. diff --git a/docs/logs.md b/docs/logs.md index b4aca2d..392e5de 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -51,9 +51,13 @@ settings.apply_with_log( assert_eq!(filled_fields, vec!["theme"]); ``` -For structs using `#[patch(nesting)]`, the log closure is threaded into nested -patches so you receive field names with prefixes showing the path through nested -structures. +**Function Signature for `default_log`:** +- **Without `nesting` feature**: Define your logging function as `fn(&str)` that takes only the + field name. Create a wrapper with `fn(&[&str], &str)` that the derive macro will call. +- **With `nesting` feature enabled**: Define your logging function as `fn(&[&str], &str)` where + the first parameter contains path segments for nested fields (e.g., `["config", "logging"]` for a + nested field), and the second parameter is the field name. This allows you to see the + complete path through nested structures. **Always-on via struct attribute** — use `#[patch(default_log(fn_path))]` or `#[filler(default_log(fn_path))]` to wire a specific function into `apply` @@ -61,6 +65,8 @@ itself. Every call to `apply` on that struct will automatically invoke the function for each field that is changed, with no extra effort at call sites. Has no effect on `apply_with_log`. +Example with `nesting` feature: + ```rust use struct_patch::{Filler, Patch}; @@ -106,3 +112,31 @@ settings.apply(SettingsFiller { theme: Some("dark".into()) }); The path may be any item path (`crate::logging::log_field`, `tracing::debug!` wrapped in a thin function, etc.). + +Example without `nesting` feature (using a wrapper): + +```rust +use struct_patch::Filler; + +// Your clean logging function that takes only the field name +#[cfg(not(feature = "nesting"))] +fn my_filler_log(field: &str) { + println!("filled: {field}"); +} + +// Wrapper function that the derive macro will call +#[cfg(not(feature = "nesting"))] +fn my_filler_log_wrapper(_prefixes: &[&str], field: &str) { + my_filler_log(field); +} + +#[derive(Default, Filler)] +#[filler(default_log(my_filler_log_wrapper))] +struct Settings { + theme: Option, +} + +let mut settings = Settings::default(); +settings.apply(SettingsFiller { theme: Some("dark".into()) }); +// prints: filled: theme +``` From 0de3f0e0643159239f66633884719e6000b05ad9 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Thu, 10 Sep 2026 00:39:58 +0800 Subject: [PATCH 6/8] fixup! impl log x nesting feature --- derive/src/filler.rs | 12 ++++++++++++ derive/src/patch.rs | 17 +++++++++++++++-- examples/filler-examples/examples/log.rs | 12 +----------- examples/patch-examples/examples/log.rs | 16 +++------------- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/derive/src/filler.rs b/derive/src/filler.rs index d840fff..8bda4fc 100644 --- a/derive/src/filler.rs +++ b/derive/src/filler.rs @@ -247,6 +247,7 @@ impl Filler { #[cfg(not(feature = "op"))] let op_impl = quote!(); + #[cfg(feature = "nesting")] let make_log_calls = |names: &[Option<&Ident>]| -> Vec { if let Some(f) = default_log_fn { names @@ -257,6 +258,17 @@ impl Filler { names.iter().map(|_| quote! {}).collect() } }; + #[cfg(not(feature = "nesting"))] + let make_log_calls = |names: &[Option<&Ident>]| -> Vec { + if let Some(f) = default_log_fn { + names + .iter() + .map(|n| quote! { #f(stringify!(#n)); }) + .collect() + } else { + names.iter().map(|_| quote! {}).collect() + } + }; let native_value_log_calls = make_log_calls(&native_value_field_names); let extendable_log_calls = make_log_calls(&extendable_field_names); let option_log_calls = make_log_calls(&option_field_names); diff --git a/derive/src/patch.rs b/derive/src/patch.rs index 7e23e49..2506a5f 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -765,8 +765,10 @@ impl Patch { let op_impl = quote!(); // Per-field log-call token streams, parallel with each field-name vec. - // Emit `default_log_fn(&[], stringify!(field));` when a struct-level log is configured, - // or an empty token stream otherwise. + // With nesting feature: emit `default_log_fn(&[], stringify!(field));` + // Without nesting feature: emit `default_log_fn(stringify!(field));` + // or empty token stream otherwise. + #[cfg(feature = "nesting")] let make_log_calls = |names: &[Option<&Ident>]| -> Vec { if let Some(f) = default_log_fn { names @@ -777,6 +779,17 @@ impl Patch { names.iter().map(|_| quote! {}).collect() } }; + #[cfg(not(feature = "nesting"))] + let make_log_calls = |names: &[Option<&Ident>]| -> Vec { + if let Some(f) = default_log_fn { + names + .iter() + .map(|n| quote! { #f(stringify!(#n)); }) + .collect() + } else { + names.iter().map(|_| quote! {}).collect() + } + }; let renamed_log_calls = make_log_calls(&renamed_field_names); let renamed_by_ev_log_calls = make_log_calls(&renamed_field_names_by_empty_value); let original_log_calls = make_log_calls(&original_field_names); diff --git a/examples/filler-examples/examples/log.rs b/examples/filler-examples/examples/log.rs index 4ddf7e3..e2f43b6 100644 --- a/examples/filler-examples/examples/log.rs +++ b/examples/filler-examples/examples/log.rs @@ -15,19 +15,9 @@ fn log_filler_field(prefixes: &[&str], field: &str) { println!("[default_log] filler field: {path}"); } -#[cfg(not(feature = "nesting"))] -fn log_filler_field_wrapper(_prefixes: &[&str], field: &str) { - log_filler_field(field); -} - -#[cfg(feature = "nesting")] -fn log_filler_field_wrapper(prefixes: &[&str], field: &str) { - log_filler_field(prefixes, field); -} - #[derive(Default, Filler)] #[filler(attribute(derive(Debug, Default)))] -#[filler(default_log(log_filler_field_wrapper))] +#[filler(default_log(log_filler_field))] struct Settings { theme: Option, max_connections: Option, diff --git a/examples/patch-examples/examples/log.rs b/examples/patch-examples/examples/log.rs index 94b1baa..78b7f06 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -15,19 +15,9 @@ fn log_patch_field(prefixes: &[&str], field: &str) { println!("[default_log] patch field: {path}"); } -#[cfg(not(feature = "nesting"))] -fn log_patch_field_wrapper(_prefixes: &[&str], field: &str) { - log_patch_field(field); -} - -#[cfg(feature = "nesting")] -fn log_patch_field_wrapper(prefixes: &[&str], field: &str) { - log_patch_field(prefixes, field); -} - #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] -#[patch(default_log(log_patch_field_wrapper))] +#[patch(default_log(log_patch_field))] struct Config { host: String, port: u16, @@ -45,7 +35,7 @@ struct Logging { #[cfg(feature = "nesting")] #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] -#[patch(default_log(log_patch_field_wrapper))] +#[patch(default_log(log_patch_field))] struct ConfigWithLogging { host: String, port: u16, @@ -56,7 +46,7 @@ struct ConfigWithLogging { #[cfg(feature = "nesting")] #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] -#[patch(default_log(log_patch_field_wrapper))] +#[patch(default_log(log_patch_field))] struct Server { name: String, #[patch(nesting)] From c9e1b92d7838ec61fc93a9a129952548fc1f15d3 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Thu, 10 Sep 2026 09:35:45 +0800 Subject: [PATCH 7/8] fixup! impl log x nesting feature --- examples/patch-examples/examples/box.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/patch-examples/examples/box.rs b/examples/patch-examples/examples/box.rs index a8b835b..1de1d37 100644 --- a/examples/patch-examples/examples/box.rs +++ b/examples/patch-examples/examples/box.rs @@ -1,12 +1,7 @@ use struct_patch::Patch; -fn log_field(prefixes: &[&str], field: &str) { - let path = if prefixes.is_empty() { - field.to_string() - } else { - format!("{}.{}", prefixes.join("."), field) - }; - println!("[default_log] field changed: {path}"); +fn log_field(field: &str) { + println!("[default_log] field changed: {field}"); } #[derive(Default, Patch)] From 4f8d5f5aaf51c23ad5f15aba9b112a068bf720c3 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Thu, 10 Sep 2026 09:43:52 +0800 Subject: [PATCH 8/8] fixup! readme & doc --- docs/logs.md | 73 +++++++++++++++++++--------------------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/docs/logs.md b/docs/logs.md index 392e5de..cbe1ff4 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -3,7 +3,7 @@ Both `Patch` and `Filler` support two ways to observe which fields are changed: **Ad-hoc at the call site** — use `apply_with_log`, which takes a closure that -is called with each patched/filled field name and its nesting path: +is called with each patched/filled field name: ```rust use struct_patch::{Filler, Patch}; @@ -18,14 +18,7 @@ let mut item = Item::default(); let patch = ItemPatch { field_int: Some(42), field_string: None }; let mut patched_fields = Vec::new(); -item.apply_with_log(patch, |prefixes, field| { - let path = if prefixes.is_empty() { - field.to_string() - } else { - format!("{}.{}", prefixes.join("."), field) - }; - patched_fields.push(path); -}); +item.apply_with_log(patch, |field| patched_fields.push(field.to_string()) ); assert_eq!(patched_fields, vec!["field_int"]); assert_eq!(item.field_int, 42); @@ -39,21 +32,14 @@ let mut settings = Settings::default(); let mut filled_fields = Vec::new(); settings.apply_with_log( SettingsFiller { theme: Some("dark".into()) }, - |prefixes, field| { - let path = if prefixes.is_empty() { - field.to_string() - } else { - format!("{}.{}", prefixes.join("."), field) - }; - filled_fields.push(path); - }, + |field| filled_fields.push(field.to_string()) ); assert_eq!(filled_fields, vec!["theme"]); ``` **Function Signature for `default_log`:** - **Without `nesting` feature**: Define your logging function as `fn(&str)` that takes only the - field name. Create a wrapper with `fn(&[&str], &str)` that the derive macro will call. + field name. - **With `nesting` feature enabled**: Define your logging function as `fn(&[&str], &str)` where the first parameter contains path segments for nested fields (e.g., `["config", "logging"]` for a nested field), and the second parameter is the field name. This allows you to see the @@ -65,6 +51,29 @@ itself. Every call to `apply` on that struct will automatically invoke the function for each field that is changed, with no extra effort at call sites. Has no effect on `apply_with_log`. +Example without `nesting` feature: + +```rust +use struct_patch::Filler; + +// Your clean logging function that takes only the field name +#[cfg(not(feature = "nesting"))] +fn my_filler_log(field: &str) { + println!("filled: {field}"); +} + + +#[derive(Default, Filler)] +#[filler(default_log(my_filler_log))] +struct Settings { + theme: Option, +} + +let mut settings = Settings::default(); +settings.apply(SettingsFiller { theme: Some("dark".into()) }); +// prints: filled: theme +``` + Example with `nesting` feature: ```rust @@ -112,31 +121,3 @@ settings.apply(SettingsFiller { theme: Some("dark".into()) }); The path may be any item path (`crate::logging::log_field`, `tracing::debug!` wrapped in a thin function, etc.). - -Example without `nesting` feature (using a wrapper): - -```rust -use struct_patch::Filler; - -// Your clean logging function that takes only the field name -#[cfg(not(feature = "nesting"))] -fn my_filler_log(field: &str) { - println!("filled: {field}"); -} - -// Wrapper function that the derive macro will call -#[cfg(not(feature = "nesting"))] -fn my_filler_log_wrapper(_prefixes: &[&str], field: &str) { - my_filler_log(field); -} - -#[derive(Default, Filler)] -#[filler(default_log(my_filler_log_wrapper))] -struct Settings { - theme: Option, -} - -let mut settings = Settings::default(); -settings.apply(SettingsFiller { theme: Some("dark".into()) }); -// prints: filled: theme -```