diff --git a/Cargo.toml b/Cargo.toml index 0c3ed5a..636cdaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ exclude = [ [workspace.package] authors = ["Antonio Yang "] -version = "0.14.3" +version = "0.14.4" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/README.md b/README.md index 9635a62..11862d1 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`. Function signature: `fn(&str)` without nesting feature, or `fn(&[&str], &str)` with nesting feature. +- `#[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, `fn(&str, &str)` with `simple-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`. Function signature: `fn(&str)` without nesting feature, or `fn(&[&str], &str)` with nesting feature. +- `#[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, `fn(&str, &str)` with `simple-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) @@ -273,6 +273,7 @@ This crate includes the following optional features: - `none_as_default` *(optional)*: `T` needs to implement `Default`. When patching on `None`, it patches on a default instance. Mutually exclusive with `keep_none`. - `keep_none` *(optional)*: when patching on `None`, it stays `None`. Mutually exclusive with `none_as_default`. - `nesting` *(optional)*: allows a field to use `Patch` derive with the `#[patch(nesting)]` attribute. +- simple-`nesting` *(optional)*: allows a field to use `Patch` derive with the `#[patch(nesting)]` attribute, only one layer nesting, in which the log in `fn(prefix: &str, field: &str)` without vector, such that is good for no-std without alloc. - `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)*: avoid memory moves. Only meaningful with the `catalyst` feature. diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 9f82382..c1d1a7f 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -25,7 +25,9 @@ cargo_metadata = { version = "0.23", optional = true } status = [] op = [] merge = [] +log = [] nesting = [] +simple-nesting = ["nesting"] substrate = [ "syn-serde", "syn/full" ] catalyst = [ "substrate", "dep:cargo_metadata" ] unsafe = [] diff --git a/derive/src/filler.rs b/derive/src/filler.rs index 569a8a2..dda4b75 100644 --- a/derive/src/filler.rs +++ b/derive/src/filler.rs @@ -21,6 +21,7 @@ pub(crate) struct Filler { generics: syn::Generics, attributes: Vec, fields: Vec, + #[cfg(feature = "log")] default_log_fn: Option, } @@ -70,6 +71,7 @@ impl Filler { generics, attributes, fields, + #[cfg(feature = "log")] default_log_fn, } = self; @@ -248,6 +250,7 @@ impl Filler { let op_impl = quote!(); // Per-field log-call token streams, parallel with each field-name vec. + #[cfg(feature = "log")] let make_log_calls = |names: &[Option<&Ident>]| -> Vec { if let Some(f) = default_log_fn { #[cfg(feature = "nesting")] @@ -267,17 +270,22 @@ impl Filler { } names.iter().map(|_| quote! {}).collect() }; + #[cfg(not(feature = "log"))] + let make_log_calls = |names: &[Option<&Ident>]| -> Vec { + 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); // For the `apply` method: propagate `default_log_fn` into nesting fields - #[cfg(feature = "nesting")] + #[cfg(all(feature = "log", feature = "simple-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); + self.#nesting_field_names.apply_with_log(filler.#nesting_field_names, |_prefix: &str, field: &str| { + #f(stringify!(#nesting_field_names), field); }); )* } @@ -288,10 +296,41 @@ impl Filler { )* } }; + #[cfg(all(feature = "log", feature = "nesting", not(feature = "simple-nesting")))] + let nesting_apply_section: TokenStream = if let Some(ref f) = default_log_fn { + quote! { + #( + 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 = struct_patch::__vec![nesting_field_name]; + new_prefixes.extend_from_slice(prefixes); + #f(&new_prefixes , field); + }); + )* + } + } else { + quote! { + #( + self.#nesting_field_names.apply(filler.#nesting_field_names); + )* + } + }; + #[cfg(all(feature = "simple-nesting", not(feature = "log")))] + let nesting_apply_section: TokenStream = quote! { + #( + self.#nesting_field_names.apply(filler.#nesting_field_names); + )* + }; + #[cfg(all(feature = "nesting", not(feature = "simple-nesting"), not(feature = "log")))] + let nesting_apply_section: TokenStream = quote! { + #( + self.#nesting_field_names.apply(filler.#nesting_field_names); + )* + }; #[cfg(not(feature = "nesting"))] let nesting_apply_section: TokenStream = quote! {}; - #[cfg(not(feature = "nesting"))] + #[cfg(all(feature = "log", not(feature = "nesting")))] let apply_with_log_impl = quote! { fn apply_with_log<__L: FnMut(&str)>(&mut self, filler: #name #generics, mut log: __L) { #( @@ -316,7 +355,37 @@ impl Filler { )* } }; - #[cfg(feature = "nesting")] + #[cfg(all(feature = "log", feature = "simple-nesting"))] + let apply_with_log_impl = quote! { + 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); + } + } + )* + #( + self.#nesting_field_names.apply_with_log(filler.#nesting_field_names, |prefixes: &[&str], field: &str| { + log(stringify!(#nesting_field_names), field); + }); + )* + } + }; + #[cfg(all(feature = "log", feature = "nesting", not(feature = "simple-nesting")))] let apply_with_log_impl = quote! { fn apply_with_log<__L: FnMut(&[&str], &str)>(&mut self, filler: #name #generics, mut log: __L) { #( @@ -342,13 +411,15 @@ impl Filler { #( 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); + let mut new_prefixes = struct_patch::__vec![nesting_field_name]; + new_prefixes.extend_from_slice(prefixes); log(&new_prefixes, field); }); )* } }; + #[cfg(not(feature = "log"))] + let apply_with_log_impl = quote! {}; let filler_impl = quote! { #[automatically_derived] @@ -419,6 +490,7 @@ impl Filler { let mut attributes = vec![]; let mut fields = vec![]; + #[cfg(feature = "log")] let mut default_log_fn: Option = None; for attr in attrs { @@ -443,12 +515,17 @@ impl Filler { let attribute: TokenStream = content.parse()?; attributes.push(attribute); } + #[cfg(feature = "log")] DEFAULT_LOG => { // #[filler(default_log(path::to::fn))] let content; parenthesized!(content in meta.input); default_log_fn = Some(content.parse()?); } + #[cfg(not(feature = "log"))] + DEFAULT_LOG => { + return Err(meta.error("`default_log` attribute requires the `log` feature")); + } _ => { return Err(meta.error(format_args!( "unknown filler container attribute `{}`", @@ -476,6 +553,7 @@ impl Filler { generics, attributes, fields, + #[cfg(feature = "log")] default_log_fn, }) } diff --git a/derive/src/patch.rs b/derive/src/patch.rs index 0e4ffaf..9a3d580 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -28,6 +28,7 @@ pub(crate) struct Patch { generics: syn::Generics, attributes: Vec, fields: Vec, + #[cfg(feature = "log")] default_log_fn: Option, } @@ -108,6 +109,7 @@ impl Patch { generics, attributes, fields, + #[cfg(feature = "log")] default_log_fn, } = self; @@ -765,6 +767,7 @@ impl Patch { let op_impl = quote!(); // Per-field log-call token streams, parallel with each field-name vec. + #[cfg(feature = "log")] let make_log_calls = |names: &[Option<&Ident>]| -> Vec { if let Some(f) = default_log_fn { #[cfg(feature = "nesting")] @@ -784,6 +787,11 @@ impl Patch { } names.iter().map(|_| quote! {}).collect() }; + #[cfg(not(feature = "log"))] + let make_log_calls = |names: &[Option<&Ident>]| -> Vec { + 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); @@ -796,13 +804,29 @@ impl Patch { let apply_by_log_calls = make_log_calls(&apply_by_field_names); // For the `apply` method: propagate `default_log_fn` into nesting fields. - #[cfg(feature = "nesting")] + #[cfg(all(feature = "log", feature = "simple-nesting"))] + 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, |prefix: &str, field: &str| { + #f(stringify!(#nesting_field_names), field); + }); + )* + } + } else { + quote! { + #( + self.#nesting_field_names.apply(patch.#nesting_field_names); + )* + } + }; + #[cfg(all(feature = "log", feature = "nesting", not(feature = "simple-nesting")))] let nesting_apply_section: TokenStream = if let Some(ref f) = default_log_fn { quote! { #( 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![nesting_field_name]; + let mut new_prefixes = struct_patch::__vec![nesting_field_name]; new_prefixes.extend_from_slice(prefixes); #f(&new_prefixes, field); }); @@ -815,10 +839,22 @@ impl Patch { )* } }; + #[cfg(all(feature = "simple-nesting", not(feature = "log")))] + let nesting_apply_section: TokenStream = quote! { + #( + self.#nesting_field_names.apply(patch.#nesting_field_names); + )* + }; + #[cfg(all(feature = "nesting", not(feature = "simple-nesting"), not(feature = "log")))] + let nesting_apply_section: TokenStream = quote! { + #( + self.#nesting_field_names.apply(patch.#nesting_field_names); + )* + }; #[cfg(not(feature = "nesting"))] let nesting_apply_section: TokenStream = quote! {}; - #[cfg(not(feature = "nesting"))] + #[cfg(all(feature = "log", not(feature = "nesting")))] let apply_with_log_impl = quote! { fn apply_with_log(&mut self, patch: #name #generics, mut log: F) { #( @@ -873,7 +909,67 @@ impl Patch { )* } }; - #[cfg(feature = "nesting")] + #[cfg(all(feature = "log", feature = "simple-nesting"))] + let apply_with_log_impl = quote! { + 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, |prefix: &str, field: &str| { + log(stringify!(#nesting_field_names), field); + }); + )* + } + }; + #[cfg(all(feature = "log", feature = "nesting", not(feature = "simple-nesting")))] let apply_with_log_impl = quote! { fn apply_with_log(&mut self, patch: #name #generics, mut log: F) { #( @@ -929,13 +1025,15 @@ impl Patch { #( 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![nesting_field_name]; + let mut new_prefixes = struct_patch::__vec![nesting_field_name]; new_prefixes.extend_from_slice(prefixes); log(&new_prefixes, field); }); )* } }; + #[cfg(not(feature = "log"))] + let apply_with_log_impl = quote! {}; let patch_impl = quote! { #[automatically_derived] @@ -1130,7 +1228,7 @@ impl Patch { // chain when `Filler` (which shares the method name `apply`) is used in // the same crate, triggering a recursion-limit overflow. A concrete // impl per derived type terminates the solver immediately. - #[cfg(all(feature = "box", feature = "nesting"))] + #[cfg(all(feature = "box", feature = "log", feature = "nesting"))] let box_apply_with_log_impl = quote! { fn apply_with_log<__F: ::core::ops::FnMut(&[&str], &str)>( &mut self, @@ -1140,7 +1238,7 @@ impl Patch { struct_patch::traits::Patch::apply_with_log(self, *patch, log); } }; - #[cfg(all(feature = "box", not(feature = "nesting")))] + #[cfg(all(feature = "box", feature = "log", not(feature = "nesting")))] let box_apply_with_log_impl = quote! { fn apply_with_log<__F: ::core::ops::FnMut(&str)>( &mut self, @@ -1150,6 +1248,8 @@ impl Patch { struct_patch::traits::Patch::apply_with_log(self, *patch, log); } }; + #[cfg(all(feature = "box", not(feature = "log")))] + let box_apply_with_log_impl = quote! {}; #[cfg(feature = "box")] let box_impl = quote! { #[automatically_derived] @@ -1224,6 +1324,7 @@ impl Patch { let mut name = None; let mut attributes = vec![]; let mut fields = vec![]; + #[cfg(feature = "log")] let mut default_log_fn: Option = None; for attr in attrs { @@ -1258,12 +1359,17 @@ impl Patch { let attribute: TokenStream = content.parse()?; attributes.push(attribute); } + #[cfg(feature = "log")] DEFAULT_LOG => { // #[patch(default_log(path::to::fn))] let content; parenthesized!(content in meta.input); default_log_fn = Some(content.parse()?); } + #[cfg(not(feature = "log"))] + DEFAULT_LOG => { + return Err(meta.error("`default_log` attribute requires the `log` feature")); + } _ => { return Err(meta.error(format_args!( "unknown patch container attribute `{}`", @@ -1292,6 +1398,7 @@ impl Patch { generics, attributes, fields, + #[cfg(feature = "log")] default_log_fn, }) } @@ -1612,6 +1719,7 @@ mod tests { patch_struct_name: syn::Ident::new("MyPatch", Span::call_site()), generics: syn::Generics::default(), attributes: vec![quote! { derive(Debug, PartialEq, Clone, Serialize, Deserialize) }], + #[cfg(feature = "log")] default_log_fn: None, fields: vec![ Field { diff --git a/docs/logs.md b/docs/logs.md index cbe1ff4..7702237 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -1,12 +1,21 @@ # Case Study - Log which fields were patched or filled -Both `Patch` and `Filler` support two ways to observe which fields are changed: +Both `Patch` and `Filler` support two ways to observe which fields are changed. +**Requires the `log` feature.** default enabled. + +**Note:** If the `log` feature is not enabled, there is no difference between the `nesting` +and `simple-nesting` features—both lack the ability to observe field changes. + +**Error:** `nesting` and `simple-nesting` features cannot be enabled at the same time. +They are mutually exclusive. Choose one or the other based on your use case: +- Use `nesting` for full multi-level nesting support with complete path visibility +- Use `simple-nesting` for lightweight single-level nesting in `no_std` environments **Ad-hoc at the call site** — use `apply_with_log`, which takes a closure that is called with each patched/filled field name: ```rust -use struct_patch::{Filler, Patch}; +use struct_patch::Patch; #[derive(Default, Patch)] struct Item { @@ -18,32 +27,26 @@ 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); - -#[derive(Default, Filler)] -struct Settings { - theme: Option, -} - -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()) -); -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. -- **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. +**Function Signature for logging functions:** +- **Without `nesting` or `simple-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. +- **With `simple-nesting` feature enabled**: Define your logging function as + `fn(&str, &str)` where the first parameter is a single prefix string for one + level of nesting (e.g., `"config"` for nested fields, `""` for top-level), and + the second parameter is the field name. This allows you to observe the path + through single-level nested structures without heap allocation, making it ideal + for `no_std` environments. **Always-on via struct attribute** — use `#[patch(default_log(fn_path))]` or `#[filler(default_log(fn_path))]` to wire a specific function into `apply` @@ -51,13 +54,13 @@ 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: +Example without `nesting` or `simple-nesting` feature: ```rust use struct_patch::Filler; // Your clean logging function that takes only the field name -#[cfg(not(feature = "nesting"))] +#[cfg(not(any(feature = "nesting", feature = "simple-nesting")))] fn my_filler_log(field: &str) { println!("filled: {field}"); } @@ -77,7 +80,7 @@ settings.apply(SettingsFiller { theme: Some("dark".into()) }); Example with `nesting` feature: ```rust -use struct_patch::{Filler, Patch}; +use struct_patch::Patch; fn my_log(prefixes: &[&str], field: &str) { let path = if prefixes.is_empty() { @@ -98,26 +101,75 @@ struct Config { let mut cfg = Config::default(); cfg.apply(ConfigPatch { retries: Some(3), timeout: None }); // prints: patched: retries +``` -fn my_filler_log(prefixes: &[&str], field: &str) { - let path = if prefixes.is_empty() { +The path may be any item path (`crate::logging::log_field`, +`tracing::debug!` wrapped in a thin function, etc.). + +Example with `simple-nesting` feature: + +The `simple-nesting` feature provides lightweight nesting support for +single-level nested patches without heap allocation. It's ideal for `no_std` +environments where you want nesting but cannot afford the allocation overhead +of the full `nesting` feature. + +** Note: ** `simple-nesting` still works for multi-level nesting, but the +logging function only works well with one layer. + +```rust +use struct_patch::Patch; + +fn log_field(prefix: &str, field: &str) { + // prefix is a single string like "inner" or empty "" + // field is the current field name like "value" + let path = if prefix.is_empty() { field.to_string() } else { - format!("{}.{}", prefixes.join("."), field) + format!("{}.{}", prefix, field) }; - println!("filled: {path}"); + println!("patched: {path}"); } -#[derive(Default, Filler)] -#[filler(default_log(my_filler_log))] -struct Settings { - theme: Option, +#[derive(Clone, Debug, Default, Patch)] +#[patch(attribute(derive(Debug, Default)))] +struct Item { + value: u32, + #[patch(nesting)] + config: Config, } -let mut settings = Settings::default(); -settings.apply(SettingsFiller { theme: Some("dark".into()) }); -// prints: filled: theme -``` +#[derive(Clone, Debug, Default, Patch)] +#[patch(attribute(derive(Debug, Default)))] +struct Config { + timeout: u32, + retries: u32, +} -The path may be any item path (`crate::logging::log_field`, -`tracing::debug!` wrapped in a thin function, etc.). +// Creating a nested patch +let item_a = Item::default(); +let item_b = Item { + value: 42, + config: Config { + timeout: 5000, + retries: 3, + }, +}; + +let patch: ItemPatch = item_b.clone().into_patch_by_diff(item_a); + +// Applying with logging +let mut item = Item::default(); +item.apply_with_log(patch, |prefix, field| { + let path = if prefix.is_empty() { + field.to_string() + } else { + format!("{}.{}", prefix, field) + }; + println!("patched: {path}"); +}); + +// Output: +// patched: value +// patched: config.timeout +// patched: config.retries +``` diff --git a/examples/complex-examples/Cargo.toml b/examples/complex-examples/Cargo.toml index 79e7d15..3972a05 100644 --- a/examples/complex-examples/Cargo.toml +++ b/examples/complex-examples/Cargo.toml @@ -10,7 +10,7 @@ struct-patch = { path = "../../lib" } [workspace.package] authors = ["Antonio Yang "] -version = "0.14.3" +version = "0.14.4" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/examples/filler-examples/Cargo.toml b/examples/filler-examples/Cargo.toml index 0417376..eb8ec78 100644 --- a/examples/filler-examples/Cargo.toml +++ b/examples/filler-examples/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "filler-examples" authors = ["Antonio Yang "] -version = "0.14.3" +version = "0.14.4" edition = "2021" license = "MIT" diff --git a/examples/no-std-examples/Cargo.toml b/examples/no-std-examples/Cargo.toml index 774d933..bd62206 100644 --- a/examples/no-std-examples/Cargo.toml +++ b/examples/no-std-examples/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "no-std-examples" authors = ["Antonio Yang "] -version = "0.14.3" +version = "0.14.4" edition = "2021" license = "MIT" [dependencies] -struct-patch = { path = "../../lib" } +struct-patch = { path = "../../lib", default-features = false } linked_list_allocator = "0.10" cortex-m-rt = "0.7" cortex-m-semihosting = "0.5" @@ -16,6 +16,9 @@ panic-semihosting = "0.7" default = [] box = ["struct-patch/box"] option = ["struct-patch/option"] +nesting = ["struct-patch/nesting", "struct-patch/log"] +pure-nesting = ["struct-patch/nesting"] +simple-nesting = ["struct-patch/simple-nesting", "struct-patch/log"] [[bin]] name = "no-std-box" @@ -24,3 +27,7 @@ path = "src/box.rs" [[bin]] name = "no-std-option" path = "src/option.rs" + +[[bin]] +name = "no-std-nesting" +path = "src/nesting.rs" diff --git a/examples/no-std-examples/src/nesting.rs b/examples/no-std-examples/src/nesting.rs new file mode 100644 index 0000000..950e7d8 --- /dev/null +++ b/examples/no-std-examples/src/nesting.rs @@ -0,0 +1,84 @@ +#![no_std] +#![no_main] +#![allow(clippy::empty_loop)] +#![allow(unused_imports)] +#![allow(dead_code)] +extern crate alloc; + +use core::mem::MaybeUninit; +use cortex_m_rt::entry; +use cortex_m_semihosting::debug; +use cortex_m_semihosting::hprintln; +use linked_list_allocator::LockedHeap; +use panic_semihosting as _; + +#[global_allocator] +static ALLOCATOR: LockedHeap = LockedHeap::empty(); + +const HEAP_SIZE: usize = 1024; +static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit(); + +#[entry] +#[cfg(all(not(feature = "nesting"), not(feature = "simple-nesting"), not(feature = "pure-nesting")))] +fn main() -> ! { + debug::exit(debug::EXIT_FAILURE); + loop {} +} + +#[entry] +#[cfg(any(feature = "nesting", feature = "simple-nesting", feature = "pure-nesting"))] +fn main() -> ! { + unsafe { + ALLOCATOR + .lock() + .init(core::ptr::addr_of_mut!(HEAP) as *mut u8, HEAP_SIZE); + } + + use struct_patch::Patch; + + #[derive(Clone, Debug, Default, Patch, PartialEq)] + #[patch(attribute(derive(Debug, PartialEq)))] + struct Item { + field_int: u32, + #[patch(nesting)] + inner: Nesting, + } + + #[derive(Clone, Debug, Default, Patch, PartialEq)] + #[patch(attribute(derive(Debug, PartialEq)))] + struct Nesting { + inner_int: u32, + } + + + let item_a = Item::default(); + let item_b = Item { + field_int: 7, + inner: Nesting { + inner_int: 100, + }, + }; + + let patch: ItemPatch = item_b.clone().into_patch_by_diff(item_a); + assert_eq!(patch.field_int, Some(7)); + assert_eq!(patch.inner.inner_int, Some(100)); + + + let mut item = Item::default(); + + #[cfg(feature = "nesting")] + item.apply_with_log(patch, |_prefixes: &[&str], _field: &str| { + // log if you want + }); + #[cfg(feature = "pure-nesting")] + item.apply(patch); + #[cfg(feature = "simple-nesting")] + item.apply_with_log(patch, |_prefix: &str, _field: &str| { + // log if you want + }); + + assert_eq!(item, item_b); + + debug::exit(Ok(())); + loop {} +} diff --git a/examples/patch-examples/Cargo.toml b/examples/patch-examples/Cargo.toml index 80b28f9..0dfcda9 100644 --- a/examples/patch-examples/Cargo.toml +++ b/examples/patch-examples/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "patch-examples" authors = ["Antonio Yang "] -version = "0.14.3" +version = "0.14.4" edition = "2021" license = "MIT" diff --git a/examples/patch-examples/examples/log.rs b/examples/patch-examples/examples/log.rs index 69c1c7a..2a9f34d 100644 --- a/examples/patch-examples/examples/log.rs +++ b/examples/patch-examples/examples/log.rs @@ -72,7 +72,7 @@ fn main() { }, ); // Prints: - // [custom_log] patch field 'debug' was updated + // [custom_log] patch field port was updated println!( "host={}, port={}", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 5026405..8bd65eb 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,14 +12,14 @@ readme.workspace = true rust-version.workspace = true [dependencies] -struct-patch-derive = { version = "=0.14.3", path = "../derive" } +struct-patch-derive = { version = "=0.14.4", path = "../derive" } [dev-dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1.0" [features] -default = ["status", "op"] +default = ["status", "op", "log"] status = [ "struct-patch-derive/status" ] @@ -30,6 +30,9 @@ merge = [ "struct-patch-derive/merge" ] +log = [ + "struct-patch-derive/log" +] alloc = [] std = ["box", "option"] box = ["alloc", "struct-patch-derive/box"] @@ -37,6 +40,9 @@ option = [] nesting = [ "struct-patch-derive/nesting" ] +simple-nesting = [ + "struct-patch-derive/simple-nesting" +] none_as_default = ["option"] keep_none = ["option"] substrate = [ diff --git a/lib/build.rs b/lib/build.rs new file mode 100644 index 0000000..b60dcc8 --- /dev/null +++ b/lib/build.rs @@ -0,0 +1,9 @@ +fn main() { + println!("cargo:rustc-check-cfg=cfg(use_alloc)"); + + let on = |name: &str| std::env::var_os(format!("CARGO_FEATURE_{name}")).is_some(); + + if on("ALLOC") || (on("NESTING") && on("LOG")) { + println!("cargo:rustc-cfg=use_alloc"); + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index ad9d3f3..ea2ee22 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -75,9 +75,13 @@ //! ``` #![no_std] -#[cfg(feature = "alloc")] +#[cfg(use_alloc)] extern crate alloc; +#[cfg(use_alloc)] +#[doc(hidden)] +pub use alloc::vec as __vec; + #[cfg(feature = "catalyst")] #[doc(hidden)] pub use struct_patch_derive::Catalyst; diff --git a/lib/src/traits.rs b/lib/src/traits.rs index eff8270..b553d6d 100644 --- a/lib/src/traits.rs +++ b/lib/src/traits.rs @@ -81,6 +81,7 @@ /// /// #[cfg(feature = "nesting")] /// { +/// #[cfg(not(feature = "simple-nesting"))] /// fn log_field(prefixes: &[&str], field: &str) { /// let path = if prefixes.is_empty() { /// field.to_string() @@ -89,6 +90,15 @@ /// }; /// println!("patched: {path}"); /// } +/// #[cfg(feature = "simple-nesting")] +/// fn log_field(prefix: &str, field: &str) { +/// let path = if prefix.is_empty() { +/// field.to_string() +/// } else { +/// format!("{}.{}", prefix, field) +/// }; +/// println!("patched: {path}"); +/// } /// /// #[derive(Default, Patch)] /// #[patch(default_log(log_field))] @@ -98,10 +108,11 @@ /// } /// let mut config = Config::default(); /// config.apply(ConfigPatch { field_int: Some(1), field_string: None }); -/// // log_field(&[], "field_int") is called automatically +/// // log_field(&[], "field_int") is called automatically without simple-nesting +/// // log_field("", "field_int") is called automatically with simple-nesting /// } /// -/// #[cfg(not(feature = "nesting"))] +/// #[cfg(all(feature = "log", not(feature = "nesting")))] /// { /// fn log_field(field: &str) { /// println!("patched: {field}"); @@ -192,7 +203,16 @@ pub trait Patch

{ /// let patch = ItemPatch { field_int: Some(42), field_string: None }; /// /// let mut patched_fields = Vec::new(); - /// #[cfg(feature = "nesting")] + /// #[cfg(feature = "simple-nesting")] + /// item.apply_with_log(patch, |prefix, field| { + /// let path = if prefix.is_empty() { + /// field.to_string() + /// } else { + /// format!("{}.{}", prefix, field) + /// }; + /// patched_fields.push(path); + /// }); + /// #[cfg(all(feature = "nesting", not(feature = "simple-nesting")))] /// item.apply_with_log(patch, |prefixes, field| { /// let path = if prefixes.is_empty() { /// field.to_string() @@ -209,12 +229,16 @@ pub trait Patch

{ /// /// assert_eq!(patched_fields, vec!["field_int"]); /// ``` - #[cfg(feature = "nesting")] + #[cfg(all(feature = "log", feature = "simple-nesting"))] + fn apply_with_log(&mut self, patch: P, _log: F) { + self.apply(patch); + } + #[cfg(all(feature = "log", feature = "nesting", not(feature = "simple-nesting")))] fn apply_with_log(&mut self, patch: P, _log: F) { self.apply(patch); } - #[cfg(not(feature = "nesting"))] + #[cfg(all(feature = "log", not(feature = "nesting"), not(feature = "simple-nesting")))] fn apply_with_log(&mut self, patch: P, _log: F) { self.apply(patch); } @@ -270,12 +294,17 @@ pub trait Filler { /// /// assert_eq!(filled_fields, vec!["value"]); /// ``` - #[cfg(feature = "nesting")] + #[cfg(all(feature = "log", feature = "simple-nesting"))] + fn apply_with_log(&mut self, filler: F, _log: L) { + self.apply(filler); + } + + #[cfg(all(feature = "log", feature = "nesting", not(feature = "simple-nesting")))] fn apply_with_log(&mut self, filler: F, _log: L) { self.apply(filler); } - #[cfg(not(feature = "nesting"))] + #[cfg(all(feature = "log", not(feature = "nesting"), not(feature = "simple-nesting")))] fn apply_with_log(&mut self, filler: F, _log: L) { self.apply(filler); } diff --git a/nix/scripts/check-no-std.sh b/nix/scripts/check-no-std.sh index 68210f8..2eee75e 100644 --- a/nix/scripts/check-no-std.sh +++ b/nix/scripts/check-no-std.sh @@ -3,3 +3,6 @@ cd $(git rev-parse --show-toplevel 2>/dev/null) cd examples/no-std-examples cargo run --quiet --features=box --bin no-std-box cargo run --quiet --features=option --bin no-std-option +cargo run --quiet --features=nesting --bin no-std-nesting +cargo run --quiet --features=simple-nesting --bin no-std-nesting +cargo run --quiet --features=pure-nesting --bin no-std-nesting