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 e365a64..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,6 +18,7 @@ const SKIP: &str = "skip"; const ADDABLE: &str = "addable"; const ADD: &str = "add"; const NESTING: &str = "nesting"; +const EMPTY_VALUE: &str = "empty_value"; pub(crate) struct Patch { visibility: syn::Visibility, @@ -36,6 +38,8 @@ struct Field { addable: Addable, #[cfg(feature = "nesting")] nesting: bool, + /// Define which value is empty + empty_value: Option, } impl Patch { @@ -55,42 +59,106 @@ 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.empty_value.is_none()) + .map(|f| f.ident.as_ref()) + .collect::>(); + #[cfg(not(feature = "nesting"))] + let field_names_by_empty_value = fields + .iter() + .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) + .filter(|f| !f.nesting && f.empty_value.is_none()) .map(|f| f.ident.as_ref()) .collect::>(); + #[cfg(feature = "nesting")] + let field_names_by_empty_value = fields + .iter() + .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) + .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_empty_value = fields + .iter() + .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) + .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_empty_value = fields + .iter() + .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) + .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_empty_value = fields + .iter() + .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) + .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_empty_value = fields + .iter() + .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"))] let nesting_field_names: Vec = Vec::new(); #[cfg(not(feature = "nesting"))] @@ -135,6 +203,11 @@ impl Patch { return false } )* + #( + if self.#field_names_by_empty_value == #field_name_empty_values { + return false + } + )* #( if !self.#nesting_field_names.is_empty() { return false @@ -160,9 +233,25 @@ impl Patch { (None, None) => None, }, )* + #( + #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_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, + }, + )* #( #nesting_field_names: other.#nesting_field_names.merge(self.#nesting_field_names), )* @@ -177,16 +266,24 @@ impl Patch { let addable_handles = fields .iter() .map(|f| { - match &f.addable { - Addable::AddTrait => quote!( + match (&f.addable, f.empty_value.is_some()) { + (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 +316,18 @@ impl Patch { (None, None) => None, }, )* + #( + #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_empty_value; + let b = rhs.#renamed_field_names_by_empty_value; + #addable_handles + }, + (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, + }, + )* #( #original_field_names: match (self.#original_field_names, rhs.#original_field_names) { (Some(a), Some(b)) => { @@ -229,6 +338,18 @@ impl Patch { (None, None) => None, }, )* + #( + #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_empty_value; + let b = rhs.#original_field_names_by_empty_value; + #addable_handles + }, + (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, + }, + )* #( #nesting_field_names: self.#nesting_field_names + rhs.#nesting_field_names, )* @@ -271,6 +392,18 @@ impl Patch { (None, None) => None, }, )* + #( + #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_empty_value; + let b = rhs.#renamed_field_names_by_empty_value; + #addable_handles + }, + (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, + }, + )* #( #original_field_names: match (self.#original_field_names, rhs.#original_field_names) { (Some(a), Some(b)) => { @@ -281,6 +414,18 @@ impl Patch { (None, None) => None, }, )* + #( + #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_empty_value; + let b = rhs.#original_field_names_by_empty_value; + #addable_handles + }, + (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, + }, + )* #( #nesting_field_names: self.#nesting_field_names + rhs.#nesting_field_names, )* @@ -300,11 +445,21 @@ impl Patch { self.#renamed_field_names.apply(v); } )* + #( + 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); + } + )* #( if let Some(v) = patch.#original_field_names { self.#original_field_names = v; } )* + #( + 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 ; + } + )* #( self.#nesting_field_names.apply(patch.#nesting_field_names); )* @@ -315,9 +470,15 @@ impl Patch { #( #renamed_field_names: Some(self.#renamed_field_names.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_empty_value: self.#original_field_names_by_empty_value, + )* #( #nesting_field_names: self.#nesting_field_names.into_patch(), )* @@ -334,6 +495,14 @@ impl Patch { None }, )* + #( + #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 { + #renamed_field_name_empty_values + }, + )* #( #original_field_names: if self.#original_field_names != previous_struct.#original_field_names { Some(self.#original_field_names) @@ -342,6 +511,14 @@ impl Patch { None }, )* + #( + #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 { + #original_field_name_empty_values + }, + )* #( #nesting_field_names: self.#nesting_field_names.into_patch_by_diff(previous_struct.#nesting_field_names), )* @@ -353,6 +530,9 @@ impl Patch { #( #field_names: None, )* + #( + #field_names_by_empty_value: #field_name_empty_values, + )* #( #nesting_field_names: #nesting_field_types::new_empty_patch(), )* @@ -470,6 +650,7 @@ impl Field { attributes, #[cfg(feature = "nesting")] nesting, + empty_value, .. } = self; @@ -483,10 +664,19 @@ impl Field { .collect::>(); match ident { #[cfg(not(feature = "nesting"))] - Some(ident) => Ok(quote! { - #(#attributes)* - pub #ident: Option<#ty>, - }), + Some(ident) => { + if empty_value.is_some() { + Ok(quote! { + #(#attributes)* + pub #ident: #ty, + }) + } else { + Ok(quote! { + #(#attributes)* + pub #ident: Option<#ty>, + }) + } + } #[cfg(feature = "nesting")] Some(ident) => { if *nesting { @@ -499,6 +689,11 @@ impl Field { #(#attributes)* pub #ident: #patch_type, }) + } else if empty_value.is_some() { + Ok(quote! { + #(#attributes)* + pub #ident: #ty, + }) } else { Ok(quote! { #(#attributes)* @@ -507,10 +702,19 @@ impl Field { } } #[cfg(not(feature = "nesting"))] - None => Ok(quote! { - #(#attributes)* - pub Option<#ty>, - }), + None => { + if empty_value.is_some() { + Ok(quote! { + #(#attributes)* + pub #ty, + }) + } else { + Ok(quote! { + #(#attributes)* + pub Option<#ty>, + }) + } + } #[cfg(feature = "nesting")] None => { if *nesting { @@ -523,6 +727,11 @@ impl Field { #(#attributes)* pub #patch_type, }) + } else if empty_value.is_some() { + Ok(quote! { + #(#attributes)* + pub #ty, + }) } else { Ok(quote! { #(#attributes)* @@ -542,6 +751,7 @@ impl Field { let mut attributes = vec![]; let mut field_type = None; let mut skip = false; + let mut empty_value = None; #[cfg(feature = "op")] let mut addable = Addable::Disable; @@ -611,6 +821,19 @@ impl Field { meta.error("#[patch(nesting)] only work with `nesting` feature") ); } + 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!( "unknown patch field attribute `{}`", @@ -634,6 +857,7 @@ impl Field { addable, #[cfg(feature = "nesting")] nesting, + empty_value, })) } } @@ -696,6 +920,8 @@ mod tests { pub field1: SubItem, #[patch(skip)] pub field2: Option, + #[patch(empty_value = false)] + pub field3: bool, } }; let expected = Patch { @@ -704,16 +930,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, + empty_value: None, + }, + 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, + empty_value: Some(Lit::Bool(syn::LitBool::new(false, Span::call_site()))), + }, + ], }; let result = Patch::from_ast(syn::parse2(input).unwrap()).unwrap(); assert_eq_sorted!( 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 2230b72..5d61a7b 100644 --- a/lib/examples/clap.rs +++ b/lib/examples/clap.rs @@ -6,7 +6,14 @@ use struct_patch::Patch; struct Config { #[patch(attribute(arg(short, long)))] log_level: u8, + + // NOTE: + // 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)))] + #[cfg(not(feature = "merge"))] debug: bool, } @@ -14,18 +21,18 @@ impl Default for Config { fn default() -> Config { Config { log_level: 10, + #[cfg(not(feature = "merge"))] debug: false, } } } 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());