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/README.md b/README.md index 8762a68..9635a62 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: @@ -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. @@ -274,7 +275,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..8bda4fc 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 } } @@ -228,6 +247,18 @@ 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 + .iter() + .map(|n| quote! { #f(&[], stringify!(#n)); }) + .collect() + } else { + 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 @@ -242,6 +273,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,29 +317,63 @@ impl Filler { } } )* + #nesting_apply_section + } + + #[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)); + 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); + } + } + )* } - fn apply_with_log<__L: FnMut(&str)>(&mut self, filler: #name #generics, mut log: __L) { + #[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)); + 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); } } )* + #( + 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 { @@ -296,6 +381,7 @@ impl Filler { #(#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 +518,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 +576,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 +607,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..2506a5f 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -765,8 +765,21 @@ 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 + .iter() + .map(|n| quote! { #f(&[], stringify!(#n)); }) + .collect() + } else { + 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 @@ -796,7 +809,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 +884,95 @@ 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)); + 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); + } + )* + } + + #[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)); + 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 +980,23 @@ 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); } )* #( - 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 +1144,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..cbe1ff4 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -18,7 +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, |field| patched_fields.push(field.to_string())); +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); @@ -32,13 +32,18 @@ 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()), + |field| filled_fields.push(field.to_string()) ); 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. +**Function Signature for `default_log`:** +- **Without `nesting` feature**: Define your logging function as `fn(&str)` that takes only the + 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 + 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` @@ -46,11 +51,41 @@ 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 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 +99,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..e2f43b6 --- /dev/null +++ b/examples/filler-examples/examples/log.rs @@ -0,0 +1,78 @@ +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}"); +} + +#[derive(Default, Filler)] +#[filler(attribute(derive(Debug, Default)))] +#[filler(default_log(log_filler_field))] +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..1de1d37 100644 --- a/examples/patch-examples/examples/box.rs +++ b/examples/patch-examples/examples/box.rs @@ -52,7 +52,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..78b7f06 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -1,15 +1,20 @@ -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}"); } -// --- Patch example --- - #[derive(Default, Patch)] #[patch(attribute(derive(Debug, Default)))] #[patch(default_log(log_patch_field))] @@ -19,18 +24,36 @@ struct Config { debug: bool, } -// --- Filler example --- +#[cfg(feature = "nesting")] +#[derive(Default, Patch)] +#[patch(attribute(derive(Debug, Default)))] +struct Logging { + level: String, + format: String, +} -#[derive(Default, Filler)] -#[filler(attribute(derive(Debug, Default)))] -#[filler(default_log(log_filler_field))] -struct Settings { - theme: Option, - max_connections: Option, +#[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, } fn main() { - // --- Patch with default_log --- println!("--- Patch: apply() with default_log ---"); let mut config = Config::default(); config.apply(ConfigPatch { @@ -47,7 +70,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 { @@ -55,7 +77,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 @@ -65,48 +94,119 @@ 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 + #[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!( - "theme={:?}, max_connections={:?}", - settings.theme, settings.max_connections - ); + println!( + "name={}, config.host={}, config.port={}", + server.name, server.config.host, server.config.port + ); - // 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 - ); + 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' - // --- 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!( + "name={}, config.host={}, config.port={}", + server2.name, server2.config.host, server2.config.port + ); - println!( - "theme={:?}, max_connections={:?}", - settings2.theme, settings2.max_connections - ); + 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 + ); + + 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-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 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