From c879361b47eff321bd3a6d6c51b5bffca750914b Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 24 Mar 2026 22:18:17 +0800 Subject: [PATCH 1/5] use HashSet for fields --- derive/src/patch.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/derive/src/patch.rs b/derive/src/patch.rs index e365a64..7595fa9 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -1,6 +1,7 @@ extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; +use std::collections::HashSet; use std::str::FromStr; use syn::{ meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, LitStr, Result, @@ -56,58 +57,61 @@ impl Patch { .collect::>>()?; #[cfg(not(feature = "nesting"))] - let field_names = fields.iter().map(|f| f.ident.as_ref()).collect::>(); + let field_names = fields + .iter() + .map(|f| f.ident.as_ref()) + .collect::>(); #[cfg(feature = "nesting")] let field_names = fields .iter() .filter(|f| !f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(not(feature = "nesting"))] let renamed_field_names = fields .iter() .filter(|f| f.retyped) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(feature = "nesting")] let renamed_field_names = fields .iter() .filter(|f| f.retyped && !f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(not(feature = "nesting"))] let original_field_names = fields .iter() .filter(|f| !f.retyped) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(feature = "nesting")] let original_field_names = fields .iter() .filter(|f| !f.retyped && !f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(not(feature = "nesting"))] - let nesting_field_names: Vec = Vec::new(); + let nesting_field_names: HashSet = HashSet::new(); #[cfg(not(feature = "nesting"))] - let nesting_field_types: Vec = Vec::new(); + let nesting_field_types: HashSet = HashSet::new(); #[cfg(feature = "nesting")] let nesting_field_names = fields .iter() .filter(|f| f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(feature = "nesting")] let nesting_field_types = fields .iter() .filter(|f| f.nesting) .map(|f| f.ty.clone()) - .collect::>(); + .collect::>(); let mapped_attributes = attributes .iter() From 3ece55ec2d7bfaef4da75ebf599394aa45a36548 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Tue, 24 Mar 2026 22:26:01 +0800 Subject: [PATCH 2/5] Revert "use HashSet for fields" This reverts commit c879361b47eff321bd3a6d6c51b5bffca750914b. --- derive/src/patch.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/derive/src/patch.rs b/derive/src/patch.rs index 7595fa9..e365a64 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -1,7 +1,6 @@ extern crate proc_macro; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; -use std::collections::HashSet; use std::str::FromStr; use syn::{ meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, LitStr, Result, @@ -57,61 +56,58 @@ impl Patch { .collect::>>()?; #[cfg(not(feature = "nesting"))] - let field_names = fields - .iter() - .map(|f| f.ident.as_ref()) - .collect::>(); + let field_names = fields.iter().map(|f| f.ident.as_ref()).collect::>(); #[cfg(feature = "nesting")] let field_names = fields .iter() .filter(|f| !f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(not(feature = "nesting"))] let renamed_field_names = fields .iter() .filter(|f| f.retyped) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(feature = "nesting")] let renamed_field_names = fields .iter() .filter(|f| f.retyped && !f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(not(feature = "nesting"))] let original_field_names = fields .iter() .filter(|f| !f.retyped) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(feature = "nesting")] let original_field_names = fields .iter() .filter(|f| !f.retyped && !f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(not(feature = "nesting"))] - let nesting_field_names: HashSet = HashSet::new(); + let nesting_field_names: Vec = Vec::new(); #[cfg(not(feature = "nesting"))] - let nesting_field_types: HashSet = HashSet::new(); + let nesting_field_types: Vec = Vec::new(); #[cfg(feature = "nesting")] let nesting_field_names = fields .iter() .filter(|f| f.nesting) .map(|f| f.ident.as_ref()) - .collect::>(); + .collect::>(); #[cfg(feature = "nesting")] let nesting_field_types = fields .iter() .filter(|f| f.nesting) .map(|f| f.ty.clone()) - .collect::>(); + .collect::>(); let mapped_attributes = attributes .iter() From 81d5b9f0bb01aa1d68b1c6ee3900543b9b2dcfdb Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Wed, 25 Mar 2026 00:06:58 +0800 Subject: [PATCH 3/5] add `#[patch(by_is_emtpy)] --- derive/src/patch.rs | 273 ++++++++++++++++++++++++++++++++++++++----- lib/examples/clap.rs | 10 +- 2 files changed, 250 insertions(+), 33 deletions(-) diff --git a/derive/src/patch.rs b/derive/src/patch.rs index e365a64..116d2b6 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -17,6 +17,7 @@ const SKIP: &str = "skip"; const ADDABLE: &str = "addable"; const ADD: &str = "add"; const NESTING: &str = "nesting"; +const BY_IS_EMPTY: &str = "by_is_empty"; pub(crate) struct Patch { visibility: syn::Visibility, @@ -36,6 +37,7 @@ struct Field { addable: Addable, #[cfg(feature = "nesting")] nesting: bool, + by_is_empty: bool, } impl Patch { @@ -55,42 +57,85 @@ impl Patch { .map(|f| f.to_token_stream()) .collect::>>()?; + // Field names #[cfg(not(feature = "nesting"))] - let field_names = fields.iter().map(|f| f.ident.as_ref()).collect::>(); + let field_names = fields + .iter() + .filter(|f| !f.by_is_empty) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(not(feature = "nesting"))] + let field_names_by_is_empty = fields + .iter() + .filter(|f| f.by_is_empty) + .map(|f| f.ident.as_ref()) + .collect::>(); #[cfg(feature = "nesting")] let field_names = fields .iter() - .filter(|f| !f.nesting) + .filter(|f| !f.nesting && !f.by_is_empty) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(feature = "nesting")] + let field_names_by_is_empty = fields + .iter() + .filter(|f| !f.nesting && f.by_is_empty) .map(|f| f.ident.as_ref()) .collect::>(); + // Rename fields #[cfg(not(feature = "nesting"))] let renamed_field_names = fields .iter() - .filter(|f| f.retyped) + .filter(|f| f.retyped && !f.by_is_empty) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(not(feature = "nesting"))] + let renamed_field_names_by_is_empty = fields + .iter() + .filter(|f| f.retyped && f.by_is_empty) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(feature = "nesting")] let renamed_field_names = fields .iter() - .filter(|f| f.retyped && !f.nesting) + .filter(|f| f.retyped && !f.nesting && !f.by_is_empty) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(feature = "nesting")] + let renamed_field_names_by_is_empty = fields + .iter() + .filter(|f| f.retyped && !f.nesting && f.by_is_empty) .map(|f| f.ident.as_ref()) .collect::>(); + // Original fields #[cfg(not(feature = "nesting"))] let original_field_names = fields .iter() - .filter(|f| !f.retyped) + .filter(|f| !f.retyped && !f.by_is_empty) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(not(feature = "nesting"))] + let original_field_names_by_is_empty = fields + .iter() + .filter(|f| !f.retyped && f.by_is_empty) .map(|f| f.ident.as_ref()) .collect::>(); - #[cfg(feature = "nesting")] let original_field_names = fields .iter() - .filter(|f| !f.retyped && !f.nesting) + .filter(|f| !f.retyped && !f.nesting && !f.by_is_empty) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(feature = "nesting")] + let original_field_names_by_is_empty = fields + .iter() + .filter(|f| !f.retyped && !f.nesting && f.by_is_empty) .map(|f| f.ident.as_ref()) .collect::>(); + // Nesting fields #[cfg(not(feature = "nesting"))] let nesting_field_names: Vec = Vec::new(); #[cfg(not(feature = "nesting"))] @@ -135,6 +180,11 @@ impl Patch { return false } )* + #( + if !self.#field_names_by_is_empty.is_empty() { + return false + } + )* #( if !self.#nesting_field_names.is_empty() { return false @@ -160,9 +210,25 @@ impl Patch { (None, None) => None, }, )* + #( + #renamed_field_names_by_is_empty: match (self.#renamed_field_names_by_is_empty.is_empty(), other.#renamed_field_names_by_is_empty.is_empty()) { + (false, false) => self.#renamed_field_names_by_is_empty.merge(other.#renamed_field_names_by_is_empty), + (false, true) => self.#renamed_field_names_by_is_empty, + (true, false) => other.#renamed_field_names_by_is_empty, + (true, true) => self.#renamed_field_names_by_is_empty, + }, + )* #( #original_field_names: other.#original_field_names.or(self.#original_field_names), )* + #( + #original_field_names_by_is_empty: match (self.#original_field_names_by_is_empty.is_empty(), other.#original_field_names_by_is_empty.is_empty()) { + (false, false) => self.#original_field_names_by_is_empty.merge(other.#original_field_names_by_is_empty), + (false, true) => self.#original_field_names_by_is_empty, + (true, false) => other.#original_field_names_by_is_empty, + (true, true) => self.#original_field_names_by_is_empty, + }, + )* #( #nesting_field_names: other.#nesting_field_names.merge(self.#nesting_field_names), )* @@ -177,16 +243,24 @@ impl Patch { let addable_handles = fields .iter() .map(|f| { - match &f.addable { - Addable::AddTrait => quote!( + match (&f.addable, f.by_is_empty) { + (Addable::AddTrait, true) => quote!( + a + &b + ), + (Addable::AddTrait, false) => quote!( Some(a + &b) ), - Addable::AddFn(f) => { + (Addable::AddFn(f), true) => { + quote!( + #f(a, b) + ) + }, + (Addable::AddFn(f), false) => { quote!( Some(#f(a, b)) ) - } , - Addable::Disable => quote!( + }, + (Addable::Disable, _) => quote!( panic!("There are conflict patches, please use `#[patch(addable)]` if you want to add these values.") ) } @@ -219,6 +293,18 @@ impl Patch { (None, None) => None, }, )* + #( + #renamed_field_names_by_is_empty: match (self.#renamed_field_names_by_is_empty.is_empty(), rhs.#renamed_field_names_by_is_empty.is_empty()) { + (false, false) => { + let a = self.#renamed_field_names_by_is_empty; + let b = rhs.#renamed_field_names_by_is_empty; + #addable_handles + }, + (false, true) => self.#renamed_field_names_by_is_empty, + (true, false) => rhs.#renamed_field_names_by_is_empty, + (true, true) => self.#renamed_field_names_by_is_empty, + }, + )* #( #original_field_names: match (self.#original_field_names, rhs.#original_field_names) { (Some(a), Some(b)) => { @@ -229,6 +315,18 @@ impl Patch { (None, None) => None, }, )* + #( + #original_field_names_by_is_empty: match (self.#original_field_names_by_is_empty.is_empty(), rhs.#original_field_names_by_is_empty.is_empty()) { + (false, false) => { + let a = self.#original_field_names_by_is_empty; + let b = rhs.#original_field_names_by_is_empty; + #addable_handles + }, + (false, true) => self.#original_field_names_by_is_empty, + (true, false) => rhs.#original_field_names_by_is_empty, + (true, true) => self.#original_field_names_by_is_empty, + }, + )* #( #nesting_field_names: self.#nesting_field_names + rhs.#nesting_field_names, )* @@ -271,6 +369,18 @@ impl Patch { (None, None) => None, }, )* + #( + #renamed_field_names_by_is_empty: match (self.#renamed_field_names_by_is_empty.is_empty(), rhs.#renamed_field_names_by_is_empty.is_empty()) { + (false, false) => { + let a = self.#renamed_field_names_by_is_empty; + let b = rhs.#renamed_field_names_by_is_empty; + #addable_handles + }, + (false, true) => self.#renamed_field_names_by_is_empty, + (true, false) => rhs.#renamed_field_names_by_is_empty, + (true, true) => self.#renamed_field_names_by_is_empty, + }, + )* #( #original_field_names: match (self.#original_field_names, rhs.#original_field_names) { (Some(a), Some(b)) => { @@ -281,6 +391,18 @@ impl Patch { (None, None) => None, }, )* + #( + #original_field_names_by_is_empty: match (self.#original_field_names_by_is_empty.is_empty(), rhs.#original_field_names_by_is_empty.by_is_empty()) { + (false, false) => { + let a = self.#original_field_names_by_is_empty; + let b = rhs.#original_field_names_by_is_empty; + #addable_handles + }, + (false, true) => self.#original_field_names_by_is_empty, + (true, false) => rhs.#original_field_names_by_is_empty, + (true, true) => self.#original_field_names_by_is_empty, + }, + )* #( #nesting_field_names: self.#nesting_field_names + rhs.#nesting_field_names, )* @@ -300,11 +422,21 @@ impl Patch { self.#renamed_field_names.apply(v); } )* + #( + if !patch.#renamed_field_names_by_is_empty.is_empty() { + self.#renamed_field_names.apply(patch.#renamed_field_names_by_is_empty); + } + )* #( if let Some(v) = patch.#original_field_names { self.#original_field_names = v; } )* + #( + if !patch.#original_field_names_by_is_empty.is_empty() { + self.#original_field_names = patch.#original_field_names; + } + )* #( self.#nesting_field_names.apply(patch.#nesting_field_names); )* @@ -315,9 +447,15 @@ impl Patch { #( #renamed_field_names: Some(self.#renamed_field_names.into_patch()), )* + #( + #renamed_field_names_by_is_empty: self.#renamed_field_names_by_is_empty.into_patch(), + )* #( #original_field_names: Some(self.#original_field_names), )* + #( + #original_field_names_by_is_empty: self.#original_field_names_by_is_empty, + )* #( #nesting_field_names: self.#nesting_field_names.into_patch(), )* @@ -334,6 +472,14 @@ impl Patch { None }, )* + #( + #renamed_field_names_by_is_empty: if self.#renamed_field_names_by_is_empty != previous_struct.#renamed_field_names_by_is_empty { + self.#renamed_field_names_by_is_empty.into_patch_by_diff(previous_struct.#renamed_field_names_by_is_empty) + } + else { + Default::default() + }, + )* #( #original_field_names: if self.#original_field_names != previous_struct.#original_field_names { Some(self.#original_field_names) @@ -342,6 +488,14 @@ impl Patch { None }, )* + #( + #original_field_names_by_is_empty: if self.#original_field_names_by_is_empty != previous_struct.#original_field_names_by_is_empty { + self.#original_field_names_by_is_empty + } + else { + Default::default() + }, + )* #( #nesting_field_names: self.#nesting_field_names.into_patch_by_diff(previous_struct.#nesting_field_names), )* @@ -353,6 +507,9 @@ impl Patch { #( #field_names: None, )* + #( + #field_names_by_is_empty: Default::default(), + )* #( #nesting_field_names: #nesting_field_types::new_empty_patch(), )* @@ -470,6 +627,7 @@ impl Field { attributes, #[cfg(feature = "nesting")] nesting, + by_is_empty, .. } = self; @@ -483,10 +641,19 @@ impl Field { .collect::>(); match ident { #[cfg(not(feature = "nesting"))] - Some(ident) => Ok(quote! { - #(#attributes)* - pub #ident: Option<#ty>, - }), + Some(ident) => { + if *by_is_empty { + Ok(quote! { + #(#attributes)* + pub #ident: #ty, + }) + } else { + Ok(quote! { + #(#attributes)* + pub #ident: Option<#ty>, + }) + } + } #[cfg(feature = "nesting")] Some(ident) => { if *nesting { @@ -499,6 +666,11 @@ impl Field { #(#attributes)* pub #ident: #patch_type, }) + } else if *by_is_empty { + Ok(quote! { + #(#attributes)* + pub #ident: #ty, + }) } else { Ok(quote! { #(#attributes)* @@ -507,10 +679,19 @@ impl Field { } } #[cfg(not(feature = "nesting"))] - None => Ok(quote! { - #(#attributes)* - pub Option<#ty>, - }), + None => { + if *by_is_empty { + Ok(quote! { + #(#attributes)* + pub #ty, + }) + } else { + Ok(quote! { + #(#attributes)* + pub Option<#ty>, + }) + } + } #[cfg(feature = "nesting")] None => { if *nesting { @@ -523,6 +704,11 @@ impl Field { #(#attributes)* pub #patch_type, }) + } else if *by_is_empty { + Ok(quote! { + #(#attributes)* + pub #ty, + }) } else { Ok(quote! { #(#attributes)* @@ -542,6 +728,7 @@ impl Field { let mut attributes = vec![]; let mut field_type = None; let mut skip = false; + let mut by_is_empty = false; #[cfg(feature = "op")] let mut addable = Addable::Disable; @@ -611,6 +798,17 @@ impl Field { meta.error("#[patch(nesting)] only work with `nesting` feature") ); } + #[cfg(not(feature = "op"))] + BY_IS_EMPTY => { + // #[patch(by_is_empty)] + by_is_empty = true; + } + #[cfg(feature = "op")] + BY_IS_EMPTY => { + return Err( + meta.error("#[patch(by_is_empty)] only work without `op` feature now") + ); + } _ => { return Err(meta.error(format_args!( "unknown patch field attribute `{}`", @@ -634,6 +832,7 @@ impl Field { addable, #[cfg(feature = "nesting")] nesting, + by_is_empty, })) } } @@ -696,6 +895,8 @@ mod tests { pub field1: SubItem, #[patch(skip)] pub field2: Option, + #[patch(by_is_empty)] + pub field3: bool, } }; let expected = Patch { @@ -704,16 +905,28 @@ 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) }], - fields: vec![Field { - ident: Some(syn::Ident::new("field1", Span::call_site())), - ty: LitStr::new("SubItemPatch", Span::call_site()) - .parse() - .unwrap(), - attributes: vec![], - retyped: true, - #[cfg(feature = "op")] - addable: Addable::Disable, - }], + fields: vec![ + Field { + ident: Some(syn::Ident::new("field1", Span::call_site())), + ty: LitStr::new("SubItemPatch", Span::call_site()) + .parse() + .unwrap(), + attributes: vec![], + retyped: true, + #[cfg(feature = "op")] + addable: Addable::Disable, + by_is_empty: false, + }, + Field { + ident: Some(syn::Ident::new("field3", Span::call_site())), + ty: LitStr::new("bool", Span::call_site()).parse().unwrap(), + attributes: vec![], + retyped: false, + #[cfg(feature = "op")] + addable: Addable::Disable, + by_is_empty: true, + }, + ], }; let result = Patch::from_ast(syn::parse2(input).unwrap()).unwrap(); assert_eq_sorted!( diff --git a/lib/examples/clap.rs b/lib/examples/clap.rs index 2230b72..07623f0 100644 --- a/lib/examples/clap.rs +++ b/lib/examples/clap.rs @@ -6,6 +6,11 @@ use struct_patch::Patch; struct Config { #[patch(attribute(arg(short, long)))] log_level: u8, + + // NOTE: + // with by_is_empty, the debug will keep in bool in ConfigPath + // such that we can pass `--debug` not `--debug=true` as cli convention + #[patch(by_is_empty)] #[patch(attribute(arg(short, long)))] debug: bool, } @@ -20,12 +25,11 @@ impl Default for Config { } fn main() { - - // NOTE: + // NOTE: // We patch from the patch instance, so the config can easily follow // Rust Default Trait by avoiding to set default from the clap macro // we can easily have the single source of default values - + let mut config = Config::default(); config.apply(ConfigPatch::parse()); From 4b9e4810134e91e2d04fb466b5a7b7820ccd47b1 Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Wed, 25 Mar 2026 20:01:12 +0800 Subject: [PATCH 4/5] Add `#[patch(empty_value = ...)]` --- .github/workflows/test.yml | 2 + Cargo.toml | 2 +- README.md | 3 +- derive/src/filler.rs | 26 +---- derive/src/lib.rs | 24 +++++ derive/src/patch.rs | 203 +++++++++++++++++++++---------------- lib/Cargo.toml | 2 +- lib/examples/clap.rs | 7 +- 8 files changed, 151 insertions(+), 118 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e969640..bd8a838 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,5 +62,7 @@ jobs: run: | nix develop .#ci -c cargo run --example status nix develop .#ci -c cargo run --example op + nix develop .#ci -c cargo run --example clap nix develop .#ci -c cargo run --features=nesting --example nesting + nix develop .#ci -c cargo run --features=nesting --example clap nix develop .#ci -c cargo test diff --git a/Cargo.toml b/Cargo.toml index f64a8ee..3f43d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ [workspace.package] authors = ["Antonio Yang "] -version = "0.10.4" +version = "0.10.5" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/README.md b/README.md index 3fc9b37..190bbf2 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,9 @@ Field attributes: - `#[patch(name = "...")]`: change the type of the field in the generated patch struct. - `#[patch(attribute(...))]`: add attributes to the field in the generated patch struct. - `#[patch(attribute(derive(...)))]`: add derives to the field in the generated patch struct. +- `#[patch(empty_value = ...)]`: define a value as empty, so the corresponding field of patch will not wrapped by Option, and apply patch when the field is empty. - `#[filler(extendable)]`: use the struct of field for filler, the struct needs implement `Default`, `Extend`, `IntoIterator` and `is_empty`. -- `#[filler(empty_value)]`: define a value as empty, so the corresponding field of Filler will be applied, even the field is not `Option` or `Extendable`. +- `#[filler(empty_value = ...)]`: define a value as empty, so the corresponding field of Filler will be applied, even the field is not `Option` or `Extendable`. Please check the [traits][doc-traits] of document to learn more. diff --git a/derive/src/filler.rs b/derive/src/filler.rs index d15af45..7e1b30d 100644 --- a/derive/src/filler.rs +++ b/derive/src/filler.rs @@ -1,9 +1,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; -use syn::meta::ParseNestedMeta; -use syn::spanned::Spanned; -use syn::{parenthesized, DeriveInput, Error, Lit, LitStr, Result, Type}; +use syn::{parenthesized, DeriveInput, Lit, LitStr, Result, Type}; #[cfg(feature = "op")] use crate::Addable; @@ -415,7 +413,7 @@ impl Field { return Err(meta .error("The field is already the field of filler, we can't defined more than once")); } - if let Some(lit) = get_lit(path, &meta)? { + if let Some(lit) = crate::get_lit(path, &meta)? { fty = Some(FillerType::NativeValue(lit)); } else { return Err(meta @@ -429,6 +427,7 @@ impl Field { } #[cfg(not(feature = "op"))] ADDABLE => { + use syn::spanned::Spanned; return Err(syn::Error::new( ident.span(), "`addable` needs `op` feature", @@ -501,22 +500,3 @@ fn none_option_filler_type(ty: &Type) -> Ident { panic!("#[filler(extendable)] should use on a type") } } - -fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { - let expr: syn::Expr = meta.value()?.parse()?; - let mut value = &expr; - while let syn::Expr::Group(e) = value { - value = &e.expr; - } - if let syn::Expr::Lit(syn::ExprLit { lit, .. }) = value { - Ok(Some(lit.clone())) - } else { - Err(Error::new( - expr.span(), - format!( - "expected serde {} attribute to be lit: `{} = \"...\"`", - attr_name, attr_name - ), - )) - } -} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index ede4f64..3be2b96 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -5,6 +5,10 @@ mod patch; use filler::Filler; use patch::Patch; +use syn::meta::ParseNestedMeta; +use syn::spanned::Spanned; +use syn::Error; + #[cfg(feature = "op")] pub(crate) enum Addable { Disable, @@ -29,3 +33,23 @@ pub fn derive_filler(item: proc_macro::TokenStream) -> proc_macro::TokenStream { .unwrap() .into() } + +fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result> { + let expr: syn::Expr = meta.value()?.parse()?; + let mut value = &expr; + while let syn::Expr::Group(e) = value { + value = &e.expr; + } + if let syn::Expr::Lit(syn::ExprLit { lit, .. }) = value { + Ok(Some(lit.clone())) + } else { + Err(Error::new( + expr.span(), + format!( + "expected serde {} attribute to be lit: `{} = \"...\"`", + attr_name, attr_name + ), + )) + } +} + diff --git a/derive/src/patch.rs b/derive/src/patch.rs index 116d2b6..5943c5c 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -3,6 +3,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use std::str::FromStr; use syn::{ + Lit, meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, LitStr, Result, Type, }; @@ -17,7 +18,7 @@ const SKIP: &str = "skip"; const ADDABLE: &str = "addable"; const ADD: &str = "add"; const NESTING: &str = "nesting"; -const BY_IS_EMPTY: &str = "by_is_empty"; +const EMPTY_VALUE: &str = "empty_value"; pub(crate) struct Patch { visibility: syn::Visibility, @@ -37,7 +38,8 @@ struct Field { addable: Addable, #[cfg(feature = "nesting")] nesting: bool, - by_is_empty: bool, + /// Define which value is empty + empty_value: Option, } impl Patch { @@ -61,79 +63,100 @@ impl Patch { #[cfg(not(feature = "nesting"))] let field_names = fields .iter() - .filter(|f| !f.by_is_empty) + .filter(|f| f.empty_value.is_none()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(not(feature = "nesting"))] - let field_names_by_is_empty = fields + let field_names_by_empty_value = fields .iter() - .filter(|f| f.by_is_empty) + .filter(|f| f.empty_value.is_some()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(feature = "nesting")] let field_names = fields .iter() - .filter(|f| !f.nesting && !f.by_is_empty) + .filter(|f| !f.nesting && f.empty_value.is_none()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(feature = "nesting")] - let field_names_by_is_empty = fields + let field_names_by_empty_value = fields .iter() - .filter(|f| !f.nesting && f.by_is_empty) + .filter(|f| !f.nesting && f.empty_value.is_some()) .map(|f| f.ident.as_ref()) .collect::>(); + let field_name_empty_values = fields + .iter() + .filter_map(|f| f.empty_value.as_ref()) + .collect::>(); // Rename fields #[cfg(not(feature = "nesting"))] let renamed_field_names = fields .iter() - .filter(|f| f.retyped && !f.by_is_empty) + .filter(|f| f.retyped && f.empty_value.is_none()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(not(feature = "nesting"))] - let renamed_field_names_by_is_empty = fields + let renamed_field_names_by_empty_value = fields .iter() - .filter(|f| f.retyped && f.by_is_empty) + .filter(|f| f.retyped && f.empty_value.is_some()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(feature = "nesting")] let renamed_field_names = fields .iter() - .filter(|f| f.retyped && !f.nesting && !f.by_is_empty) + .filter(|f| f.retyped && !f.nesting && f.empty_value.is_none()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(feature = "nesting")] - let renamed_field_names_by_is_empty = fields + let renamed_field_names_by_empty_value = fields .iter() - .filter(|f| f.retyped && !f.nesting && f.by_is_empty) + .filter(|f| f.retyped && !f.nesting && f.empty_value.is_some()) .map(|f| f.ident.as_ref()) .collect::>(); + let renamed_field_name_empty_values = fields + .iter() + .filter(|f| f.retyped) + .filter_map(|f| f.empty_value.as_ref()) + .collect::>(); // Original fields #[cfg(not(feature = "nesting"))] let original_field_names = fields .iter() - .filter(|f| !f.retyped && !f.by_is_empty) + .filter(|f| !f.retyped && f.empty_value.is_none()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(not(feature = "nesting"))] - let original_field_names_by_is_empty = fields + let original_field_names_by_empty_value = fields .iter() - .filter(|f| !f.retyped && f.by_is_empty) + .filter(|f| !f.retyped && f.empty_value.is_some()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(feature = "nesting")] let original_field_names = fields .iter() - .filter(|f| !f.retyped && !f.nesting && !f.by_is_empty) + .filter(|f| !f.retyped && !f.nesting && f.empty_value.is_none()) .map(|f| f.ident.as_ref()) .collect::>(); #[cfg(feature = "nesting")] - let original_field_names_by_is_empty = fields + let original_field_names_by_empty_value = fields .iter() - .filter(|f| !f.retyped && !f.nesting && f.by_is_empty) + .filter(|f| !f.retyped && !f.nesting && f.empty_value.is_some()) .map(|f| f.ident.as_ref()) .collect::>(); + #[cfg(not(feature = "nesting"))] + let original_field_name_empty_values = fields + .iter() + .filter(|f| !f.retyped ) + .filter_map(|f| f.empty_value.as_ref()) + .collect::>(); + #[cfg(feature = "nesting")] + let original_field_name_empty_values = fields + .iter() + .filter(|f| !f.retyped && !f.nesting ) + .filter_map(|f| f.empty_value.as_ref()) + .collect::>(); // Nesting fields #[cfg(not(feature = "nesting"))] @@ -181,7 +204,7 @@ impl Patch { } )* #( - if !self.#field_names_by_is_empty.is_empty() { + if self.#field_names_by_empty_value == #field_name_empty_values { return false } )* @@ -211,22 +234,22 @@ impl Patch { }, )* #( - #renamed_field_names_by_is_empty: match (self.#renamed_field_names_by_is_empty.is_empty(), other.#renamed_field_names_by_is_empty.is_empty()) { - (false, false) => self.#renamed_field_names_by_is_empty.merge(other.#renamed_field_names_by_is_empty), - (false, true) => self.#renamed_field_names_by_is_empty, - (true, false) => other.#renamed_field_names_by_is_empty, - (true, true) => self.#renamed_field_names_by_is_empty, + #renamed_field_names_by_empty_value: match (self.#renamed_field_names_by_empty_value == #renamed_field_name_empty_values, other.#renamed_field_names_by_empty_value == #renamed_field_name_empty_values) { + (false, false) => self.#renamed_field_names_by_empty_value.merge(other.#renamed_field_names_by_empty_value), + (false, true) => self.#renamed_field_names_by_empty_value, + (true, false) => other.#renamed_field_names_by_empty_value, + (true, true) => #renamed_field_name_empty_values, }, )* #( #original_field_names: other.#original_field_names.or(self.#original_field_names), )* #( - #original_field_names_by_is_empty: match (self.#original_field_names_by_is_empty.is_empty(), other.#original_field_names_by_is_empty.is_empty()) { - (false, false) => self.#original_field_names_by_is_empty.merge(other.#original_field_names_by_is_empty), - (false, true) => self.#original_field_names_by_is_empty, - (true, false) => other.#original_field_names_by_is_empty, - (true, true) => self.#original_field_names_by_is_empty, + #original_field_names_by_empty_value: match (self.#original_field_names_by_empty_value == #original_field_name_empty_values, other.#original_field_names_by_empty_value == #original_field_name_empty_values) { + (false, false) => self.#original_field_names_by_empty_value.merge(other.#original_field_names_by_empty_value), + (false, true) => self.#original_field_names_by_empty_value, + (true, false) => other.#original_field_names_by_empty_value, + (true, true) => #original_field_name_empty_values, }, )* #( @@ -243,7 +266,7 @@ impl Patch { let addable_handles = fields .iter() .map(|f| { - match (&f.addable, f.by_is_empty) { + match (&f.addable, f.empty_value.is_some()) { (Addable::AddTrait, true) => quote!( a + &b ), @@ -294,15 +317,15 @@ impl Patch { }, )* #( - #renamed_field_names_by_is_empty: match (self.#renamed_field_names_by_is_empty.is_empty(), rhs.#renamed_field_names_by_is_empty.is_empty()) { + #renamed_field_names_by_empty_value: match (self.#renamed_field_names_by_empty_value == #renamed_field_name_empty_values, rhs.#renamed_field_names_by_empty_value == #renamed_field_name_empty_values) { (false, false) => { - let a = self.#renamed_field_names_by_is_empty; - let b = rhs.#renamed_field_names_by_is_empty; + let a = self.#renamed_field_names_by_empty_value; + let b = rhs.#renamed_field_names_by_empty_value; #addable_handles }, - (false, true) => self.#renamed_field_names_by_is_empty, - (true, false) => rhs.#renamed_field_names_by_is_empty, - (true, true) => self.#renamed_field_names_by_is_empty, + (false, true) => self.#renamed_field_names_by_empty_value, + (true, false) => rhs.#renamed_field_names_by_empty_value, + (true, true) => #renamed_field_name_empty_values, }, )* #( @@ -316,15 +339,15 @@ impl Patch { }, )* #( - #original_field_names_by_is_empty: match (self.#original_field_names_by_is_empty.is_empty(), rhs.#original_field_names_by_is_empty.is_empty()) { + #original_field_names_by_empty_value: match (self.#original_field_names_by_empty_value == #original_field_name_empty_values , rhs.#original_field_names_by_empty_value == #original_field_name_empty_values) { (false, false) => { - let a = self.#original_field_names_by_is_empty; - let b = rhs.#original_field_names_by_is_empty; + let a = self.#original_field_names_by_empty_value; + let b = rhs.#original_field_names_by_empty_value; #addable_handles }, - (false, true) => self.#original_field_names_by_is_empty, - (true, false) => rhs.#original_field_names_by_is_empty, - (true, true) => self.#original_field_names_by_is_empty, + (false, true) => self.#original_field_names_by_empty_value, + (true, false) => rhs.#original_field_names_by_empty_value, + (true, true) => #original_field_name_empty_values, }, )* #( @@ -370,15 +393,15 @@ impl Patch { }, )* #( - #renamed_field_names_by_is_empty: match (self.#renamed_field_names_by_is_empty.is_empty(), rhs.#renamed_field_names_by_is_empty.is_empty()) { + #renamed_field_names_by_empty_value: match (self.#renamed_field_names_by_empty_value == #renamed_field_name_empty_values, rhs.#renamed_field_names_by_empty_value == #renamed_field_name_empty_values) { (false, false) => { - let a = self.#renamed_field_names_by_is_empty; - let b = rhs.#renamed_field_names_by_is_empty; + let a = self.#renamed_field_names_by_empty_value; + let b = rhs.#renamed_field_names_by_empty_value; #addable_handles }, - (false, true) => self.#renamed_field_names_by_is_empty, - (true, false) => rhs.#renamed_field_names_by_is_empty, - (true, true) => self.#renamed_field_names_by_is_empty, + (false, true) => self.#renamed_field_names_by_empty_value, + (true, false) => rhs.#renamed_field_names_by_empty_value, + (true, true) => #renamed_field_name_empty_values, }, )* #( @@ -392,15 +415,15 @@ impl Patch { }, )* #( - #original_field_names_by_is_empty: match (self.#original_field_names_by_is_empty.is_empty(), rhs.#original_field_names_by_is_empty.by_is_empty()) { + #original_field_names_by_empty_value: match (self.#original_field_names_by_empty_value == #original_field_name_empty_values , rhs.#original_field_names_by_empty_value == #original_field_name_empty_values) { (false, false) => { - let a = self.#original_field_names_by_is_empty; - let b = rhs.#original_field_names_by_is_empty; + let a = self.#original_field_names_by_empty_value; + let b = rhs.#original_field_names_by_empty_value; #addable_handles }, - (false, true) => self.#original_field_names_by_is_empty, - (true, false) => rhs.#original_field_names_by_is_empty, - (true, true) => self.#original_field_names_by_is_empty, + (false, true) => self.#original_field_names_by_empty_value, + (true, false) => rhs.#original_field_names_by_empty_value, + (true, true) => #original_field_name_empty_values, }, )* #( @@ -423,8 +446,8 @@ impl Patch { } )* #( - if !patch.#renamed_field_names_by_is_empty.is_empty() { - self.#renamed_field_names.apply(patch.#renamed_field_names_by_is_empty); + if patch.#renamed_field_names_by_empty_value != #renamed_field_name_empty_values { + self.#renamed_field_names_by_empty_value.apply(patch.#renamed_field_names_by_empty_value); } )* #( @@ -433,8 +456,8 @@ impl Patch { } )* #( - if !patch.#original_field_names_by_is_empty.is_empty() { - self.#original_field_names = patch.#original_field_names; + if patch.#original_field_names_by_empty_value != #original_field_name_empty_values { + self.#original_field_names_by_empty_value = patch.#original_field_names_by_empty_value ; } )* #( @@ -448,13 +471,13 @@ impl Patch { #renamed_field_names: Some(self.#renamed_field_names.into_patch()), )* #( - #renamed_field_names_by_is_empty: self.#renamed_field_names_by_is_empty.into_patch(), + #renamed_field_names_by_empty_value: self.#renamed_field_names_by_empty_value.into_patch(), )* #( #original_field_names: Some(self.#original_field_names), )* #( - #original_field_names_by_is_empty: self.#original_field_names_by_is_empty, + #original_field_names_by_empty_value: self.#original_field_names_by_empty_value, )* #( #nesting_field_names: self.#nesting_field_names.into_patch(), @@ -473,11 +496,11 @@ impl Patch { }, )* #( - #renamed_field_names_by_is_empty: if self.#renamed_field_names_by_is_empty != previous_struct.#renamed_field_names_by_is_empty { - self.#renamed_field_names_by_is_empty.into_patch_by_diff(previous_struct.#renamed_field_names_by_is_empty) + #renamed_field_names_by_empty_value: if self.#renamed_field_names_by_empty_value != previous_struct.#renamed_field_names_by_empty_value { + self.#renamed_field_names_by_empty_value.into_patch_by_diff(previous_struct.#renamed_field_names_by_empty_value) } else { - Default::default() + #renamed_field_name_empty_values }, )* #( @@ -489,11 +512,11 @@ impl Patch { }, )* #( - #original_field_names_by_is_empty: if self.#original_field_names_by_is_empty != previous_struct.#original_field_names_by_is_empty { - self.#original_field_names_by_is_empty + #original_field_names_by_empty_value: if self.#original_field_names_by_empty_value != previous_struct.#original_field_names_by_empty_value { + self.#original_field_names_by_empty_value } else { - Default::default() + #original_field_name_empty_values }, )* #( @@ -508,7 +531,7 @@ impl Patch { #field_names: None, )* #( - #field_names_by_is_empty: Default::default(), + #field_names_by_empty_value: #field_name_empty_values, )* #( #nesting_field_names: #nesting_field_types::new_empty_patch(), @@ -627,7 +650,7 @@ impl Field { attributes, #[cfg(feature = "nesting")] nesting, - by_is_empty, + empty_value, .. } = self; @@ -642,7 +665,7 @@ impl Field { match ident { #[cfg(not(feature = "nesting"))] Some(ident) => { - if *by_is_empty { + if empty_value.is_some() { Ok(quote! { #(#attributes)* pub #ident: #ty, @@ -666,7 +689,7 @@ impl Field { #(#attributes)* pub #ident: #patch_type, }) - } else if *by_is_empty { + } else if empty_value.is_some() { Ok(quote! { #(#attributes)* pub #ident: #ty, @@ -680,7 +703,7 @@ impl Field { } #[cfg(not(feature = "nesting"))] None => { - if *by_is_empty { + if empty_value.is_some() { Ok(quote! { #(#attributes)* pub #ty, @@ -704,7 +727,7 @@ impl Field { #(#attributes)* pub #patch_type, }) - } else if *by_is_empty { + } else if empty_value.is_some() { Ok(quote! { #(#attributes)* pub #ty, @@ -728,7 +751,7 @@ impl Field { let mut attributes = vec![]; let mut field_type = None; let mut skip = false; - let mut by_is_empty = false; + let mut empty_value = None; #[cfg(feature = "op")] let mut addable = Addable::Disable; @@ -798,16 +821,18 @@ impl Field { meta.error("#[patch(nesting)] only work with `nesting` feature") ); } - #[cfg(not(feature = "op"))] - BY_IS_EMPTY => { - // #[patch(by_is_empty)] - by_is_empty = true; - } - #[cfg(feature = "op")] - BY_IS_EMPTY => { - return Err( - meta.error("#[patch(by_is_empty)] only work without `op` feature now") - ); + EMPTY_VALUE => { + // #[patch(empty_value = ...)] + if empty_value.is_some() { + return Err(meta + .error("The empty value is already set, we can't defined more than once")); + } + if let Some(lit) = crate::get_lit(path, &meta)? { + empty_value= Some(lit); + } else { + return Err(meta + .error("empty_value needs a clear value to define what is empty")); + } } _ => { return Err(meta.error(format_args!( @@ -832,7 +857,7 @@ impl Field { addable, #[cfg(feature = "nesting")] nesting, - by_is_empty, + empty_value, })) } } @@ -895,7 +920,7 @@ mod tests { pub field1: SubItem, #[patch(skip)] pub field2: Option, - #[patch(by_is_empty)] + #[patch(empty_value = false)] pub field3: bool, } }; @@ -915,7 +940,7 @@ mod tests { retyped: true, #[cfg(feature = "op")] addable: Addable::Disable, - by_is_empty: false, + empty_value: None, }, Field { ident: Some(syn::Ident::new("field3", Span::call_site())), @@ -924,7 +949,7 @@ mod tests { retyped: false, #[cfg(feature = "op")] addable: Addable::Disable, - by_is_empty: true, + empty_value: Some(Lit::Bool(syn::LitBool::new(false, Span::call_site()))), }, ], }; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index fcf021c..286a605 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,7 +12,7 @@ readme.workspace = true rust-version.workspace = true [dependencies] -struct-patch-derive = { version = "=0.10.4", path = "../derive" } +struct-patch-derive = { version = "=0.10.5", path = "../derive" } [dev-dependencies] serde_json = "1.0" diff --git a/lib/examples/clap.rs b/lib/examples/clap.rs index 07623f0..de58bb0 100644 --- a/lib/examples/clap.rs +++ b/lib/examples/clap.rs @@ -8,9 +8,10 @@ struct Config { log_level: u8, // NOTE: - // with by_is_empty, the debug will keep in bool in ConfigPath - // such that we can pass `--debug` not `--debug=true` as cli convention - #[patch(by_is_empty)] + // with `empty_value`, the debug will keep in bool without Option wrapper + // in ConfigPath, such that we can pass `--debug` not `--debug=true` which + // is the same as cli convention + #[patch(empty_value = false)] #[patch(attribute(arg(short, long)))] debug: bool, } From fc26d9cf180dd4c533fd9b96d4c4f07925f605fc Mon Sep 17 00:00:00 2001 From: Antonio Yang Date: Wed, 25 Mar 2026 20:30:10 +0800 Subject: [PATCH 5/5] fix clap example without merge feature --- lib/examples/clap.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/examples/clap.rs b/lib/examples/clap.rs index de58bb0..5d61a7b 100644 --- a/lib/examples/clap.rs +++ b/lib/examples/clap.rs @@ -13,6 +13,7 @@ struct Config { // is the same as cli convention #[patch(empty_value = false)] #[patch(attribute(arg(short, long)))] + #[cfg(not(feature = "merge"))] debug: bool, } @@ -20,6 +21,7 @@ impl Default for Config { fn default() -> Config { Config { log_level: 10, + #[cfg(not(feature = "merge"))] debug: false, } }