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 diff --git a/derive/src/filler.rs b/derive/src/filler.rs index 3489578..a007493 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,25 @@ impl Filler { .map(|f| !matches!(f.addable, Addable::Disable)) .collect::>(); + // Nesting fields + #[cfg(not(feature = "nesting"))] + let nesting_field_names: Vec> = Vec::new(); + #[cfg(not(feature = "nesting"))] + let nesting_field_types: Vec<&Type> = Vec::new(); + + #[cfg(feature = "nesting")] + let nesting_field_names = fields + .iter() + .filter(|f| f.nesting) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(feature = "nesting")] + let nesting_field_types = fields + .iter() + .filter(|f| f.nesting) + .map(|f| &f.ty) + .collect::>(); + let mapped_attributes = attributes .iter() .map(|a| { @@ -163,6 +185,11 @@ impl Filler { return false } )* + #( + if !self.#nesting_field_names.is_empty() { + return false + } + )* true } } @@ -232,7 +259,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 +269,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 +313,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 +514,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 +572,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 +603,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/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 a1223f4..f5b9ffc 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -1,7 +1,12 @@ use struct_patch::{Filler, Patch}; -fn log_patch_field(field: &str) { - println!("[default_log] patch field: {field}"); +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}"); } fn log_filler_field(field: &str) { @@ -19,6 +24,37 @@ struct Config { debug: bool, } +// --- 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)))] +#[patch(default_log(log_patch_field))] +struct Server { + name: String, + #[patch(nesting)] + config: ConfigWithLogging, +} + // --- Filler example --- #[derive(Default, Filler)] @@ -55,7 +91,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 @@ -100,7 +143,14 @@ fn main() { theme: Some("light".into()), max_connections: None, }, - |field| println!("[custom_log] filler field '{}' was filled", field), + |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 @@ -109,4 +159,124 @@ 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: ConfigWithLoggingPatch { + host: Some("192.168.1.1".into()), + port: Some(443), + logging: LoggingPatch::default(), + }, + }); + // Prints: + // [default_log] patch field: name + // [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 (showing prefixes) --- + println!("\n--- Patch: apply_with_log() with nesting and prefix path ---"); + let mut server2 = Server::default(); + server2.apply_with_log( + ServerPatch { + name: None, + config: ConfigWithLoggingPatch { + host: Some("10.0.0.1".into()), + port: None, + logging: LoggingPatch::default(), + }, + }, + |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: '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 { + 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: 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={}", + server3.name, + server3.config.host, + server3.config.port, + server3.config.logging.level, + server3.config.logging.format + ); + + // --- Patch with deep nesting and apply_with_log (showing full path) --- + println!("\n--- Patch: apply_with_log() with deep nesting and full path ---"); + 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, + }, + }, + }, + |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: 'config.port' + // [custom_log] patch field: 'config.logging.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 + ); + } } 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/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-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