diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7aec8f..c2e18a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,7 @@ jobs: run: | nix develop .#ci -c cargo run --features=std --example instance nix develop .#ci -c cargo run --features=std --example filler + nix develop .#ci -c cargo run --features=std --example filler-op nix develop .#ci -c cargo run --features=std --example diff nix develop .#ci -c cargo run --features=std --example json nix develop .#ci -c cargo run --features=std --example rename-patch-struct diff --git a/Cargo.toml b/Cargo.toml index b457492..f64a8ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ [workspace.package] authors = ["Antonio Yang "] -version = "0.10.3" +version = "0.10.4" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/README.md b/README.md index d9e379d..729343e 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,8 @@ The [examples][examples] demo following scenarios. ## Features This crate also includes the following optional features: - `status`(default): implements the `Status` trait for the patch struct, which provides the `is_empty` method. -- `op` (default): provide operators `<<` between instance and patch, and `+` for patches - - default: when there is a field conflict between patches, `+` will add together if the `#[patch(addable)]` or `#[patch(add=fn)]` is provided, else it will panic. +- `op` (default): provide operators `<<` between instance and patch/filler, and `+` for patches/fillers + - default: when there is a field conflict between patches/fillers, `+` will add together if the `#[patch(addable)]`, `#[patch(add=fn)]` or `#[filler(addable)]` is provided, else it will panic. - `merge` (optional): implements the `Merge` trait for the patch struct, which provides the `merge` method, and `<<` (if `op` feature enabled) between patches. - `std`(optional): - `box`: implements the `Patch>` trait for `T` where `T` implements `Patch

`. diff --git a/derive/src/filler.rs b/derive/src/filler.rs index 28b336d..d15af45 100644 --- a/derive/src/filler.rs +++ b/derive/src/filler.rs @@ -5,10 +5,14 @@ use syn::meta::ParseNestedMeta; use syn::spanned::Spanned; use syn::{parenthesized, DeriveInput, Error, Lit, LitStr, Result, Type}; +#[cfg(feature = "op")] +use crate::Addable; + const FILLER: &str = "filler"; const ATTRIBUTE: &str = "attribute"; const EXTENDABLE: &str = "extendable"; const EMPTY_VALUE: &str = "empty_value"; +const ADDABLE: &str = "addable"; pub(crate) struct Filler { visibility: syn::Visibility, @@ -49,6 +53,8 @@ struct Field { ty: Type, attributes: Vec, fty: FillerType, + #[cfg(feature = "op")] + addable: Addable, } impl Filler { @@ -74,6 +80,13 @@ impl Filler { .map(|f| f.ident.as_ref()) .collect::>(); + #[cfg(feature = "op")] + let option_field_names_addable = fields + .iter() + .filter(|f| matches!(f.fty, FillerType::Option)) + .map(|f| !matches!(f.addable, Addable::Disable)) + .collect::>(); + let extendable_field_names = fields .iter() .filter(|f| matches!(f.fty, FillerType::Extendable(_))) @@ -86,18 +99,32 @@ impl Filler { .map(|f| f.fty.inner()) .collect::>(); + #[cfg(feature = "op")] + let extendable_field_addable = fields + .iter() + .filter(|f| matches!(f.fty, FillerType::Extendable(_))) + .map(|f| !matches!(f.addable, Addable::Disable)) + .collect::>(); + let native_value_field_names = fields .iter() .filter(|f| matches!(f.fty, FillerType::NativeValue(_))) .map(|f| f.ident.as_ref()) .collect::>(); - let native_value_field_values = fields + let native_value_field_empty_values = fields .iter() .filter(|f| matches!(f.fty, FillerType::NativeValue(_))) .map(|f| f.fty.value()) .collect::>(); + #[cfg(feature = "op")] + let native_value_field_addable = fields + .iter() + .filter(|f| matches!(f.fty, FillerType::NativeValue(_))) + .map(|f| !matches!(f.addable, Addable::Disable)) + .collect::>(); + let mapped_attributes = attributes .iter() .map(|a| { @@ -130,7 +157,7 @@ impl Filler { } )* #( - if self.#native_value_field_names != #native_value_field_values { + if self.#native_value_field_names != #native_value_field_empty_values { return false } )* @@ -141,11 +168,67 @@ impl Filler { #[cfg(not(feature = "status"))] let status_impl = quote!(); + #[cfg(feature = "op")] + let op_impl = quote! { + impl #generics core::ops::Shl<#name #generics> for #struct_name #generics #where_clause { + type Output = Self; + + fn shl(mut self, rhs: #name #generics) -> Self { + struct_patch::traits::Filler::apply(&mut self, rhs); + self + } + } + + impl #generics core::ops::Add for #name #generics #where_clause { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self { + #( + if self.#native_value_field_names == #native_value_field_empty_values { + self.#native_value_field_names = rhs.#native_value_field_names; + } else if #native_value_field_addable { + self.#native_value_field_names = self.#native_value_field_names + rhs.#native_value_field_names; + } else if rhs.#native_value_field_names != #native_value_field_empty_values { + panic!("`{}` conflict in fillers, please use `#[filler(addable)]`", stringify!(#native_value_field_names)) + } + )* + #( + if self.#extendable_field_names.is_empty() { + self.#extendable_field_names = rhs.#extendable_field_names; + } else if #extendable_field_addable { + self.#extendable_field_names.extend(rhs.#extendable_field_names.into_iter()); + } else if !rhs.#extendable_field_names.is_empty() { + panic!("`{}` conflict in fillers, please use `#[filler(addable)]`", stringify!(#extendable_field_names)) + } + )* + #( + if let Some(b) = self.#option_field_names { + if let Some(a) = rhs.#option_field_names { + if #option_field_names_addable { + self.#option_field_names = Some(a + &b); + } else { + panic!("`{}` conflict in fillers, please use `#[filler(addable)]`", stringify!(#option_field_names)) + } + } else { + self.#option_field_names = Some(b); + } + } else { + self.#option_field_names = rhs.#option_field_names; + } + )* + self + } + } + }; + + #[cfg(not(feature = "op"))] + let op_impl = quote!(); + let filler_impl = quote! { impl #generics struct_patch::traits::Filler< #name #generics > for #struct_name #generics #where_clause { fn apply(&mut self, filler: #name #generics) { #( - if self.#native_value_field_names == #native_value_field_values { + if self.#native_value_field_names == #native_value_field_empty_values { self.#native_value_field_names = filler.#native_value_field_names; } )* @@ -167,7 +250,7 @@ impl Filler { #name { #(#option_field_names: None,)* #(#extendable_field_names: #extendable_field_types::default(),)* - #(#native_value_field_names: #native_value_field_values,)* + #(#native_value_field_names: #native_value_field_empty_values,)* } } } @@ -177,6 +260,7 @@ impl Filler { #filler_struct #status_impl #filler_impl + #op_impl }) } @@ -293,6 +377,8 @@ impl Field { ) -> Result> { let mut fty = filler_type(&ty); let mut attributes = vec![]; + #[cfg(feature = "op")] + let mut addable = Addable::Disable; for attr in attrs { if attr.path().to_string().as_str() != FILLER { @@ -336,9 +422,21 @@ impl Field { .error("empty_value needs a clear value to define empty")); } } + #[cfg(feature = "op")] + ADDABLE => { + // #[filler(addable)] + addable = Addable::AddTrait; + } + #[cfg(not(feature = "op"))] + ADDABLE => { + return Err(syn::Error::new( + ident.span(), + "`addable` needs `op` feature", + )); + }, _ => { return Err(meta.error(format_args!( - "unknown filler field attribute `{}`", + "unknown patch field attribute `{}`", path.replace(' ', "") ))); } @@ -352,6 +450,8 @@ impl Field { ty, attributes, fty, + #[cfg(feature = "op")] + addable, })) } } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 7667566..ede4f64 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -5,6 +5,13 @@ mod patch; use filler::Filler; use patch::Patch; +#[cfg(feature = "op")] +pub(crate) enum Addable { + Disable, + AddTrait, + AddFn(proc_macro2::Ident), +} + #[proc_macro_derive(Patch, attributes(patch))] pub fn derive_patch(item: proc_macro::TokenStream) -> proc_macro::TokenStream { Patch::from_ast(syn::parse_macro_input!(item as syn::DeriveInput)) diff --git a/derive/src/patch.rs b/derive/src/patch.rs index cffda8d..e365a64 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -7,6 +7,9 @@ use syn::{ Type, }; +#[cfg(feature = "op")] +use crate::Addable; + const PATCH: &str = "patch"; const NAME: &str = "name"; const ATTRIBUTE: &str = "attribute"; @@ -24,14 +27,6 @@ pub(crate) struct Patch { fields: Vec, } -#[cfg(feature = "op")] -pub(crate) enum Addable { - Disable, - AddTriat, - #[cfg(feature = "op")] - AddFn(Ident), -} - struct Field { ident: Option, ty: Type, @@ -66,7 +61,8 @@ impl Patch { let field_names = fields .iter() .filter(|f| !f.nesting) - .map(|f| f.ident.as_ref()).collect::>(); + .map(|f| f.ident.as_ref()) + .collect::>(); #[cfg(not(feature = "nesting"))] let renamed_field_names = fields @@ -182,8 +178,8 @@ impl Patch { .iter() .map(|f| { match &f.addable { - Addable::AddTriat => quote!( - Some(a + b) + Addable::AddTrait => quote!( + Some(a + &b) ), Addable::AddFn(f) => { quote!( @@ -495,7 +491,10 @@ impl Field { Some(ident) => { if *nesting { // TODO handle rename - let patch_type = syn::Ident::new(&format!("{}Patch", &ty.to_token_stream()), Span::call_site()); + let patch_type = syn::Ident::new( + &format!("{}Patch", &ty.to_token_stream()), + Span::call_site(), + ); Ok(quote! { #(#attributes)* pub #ident: #patch_type, @@ -506,7 +505,7 @@ impl Field { pub #ident: Option<#ty>, }) } - }, + } #[cfg(not(feature = "nesting"))] None => Ok(quote! { #(#attributes)* @@ -516,7 +515,10 @@ impl Field { None => { if *nesting { // TODO handle rename - let patch_type = syn::Ident::new(&format!("{}Patch", &ty.to_token_stream()), Span::call_site()); + let patch_type = syn::Ident::new( + &format!("{}Patch", &ty.to_token_stream()), + Span::call_site(), + ); Ok(quote! { #(#attributes)* pub #patch_type, @@ -527,7 +529,7 @@ impl Field { pub Option<#ty>, }) } - }, + } } } @@ -579,7 +581,7 @@ impl Field { #[cfg(feature = "op")] ADDABLE => { // #[patch(addable)] - addable = Addable::AddTriat; + addable = Addable::AddTrait; } #[cfg(not(feature = "op"))] ADDABLE => { @@ -606,7 +608,8 @@ impl Field { #[cfg(not(feature = "nesting"))] NESTING => { return Err( - meta.error("#[patch(nesting)] only work with `nesting` feature")); + meta.error("#[patch(nesting)] only work with `nesting` feature") + ); } _ => { return Err(meta.error(format_args!( diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 5dc84a2..3a3cae6 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.3", path = "../derive" } +struct-patch-derive = { version = "=0.10.4", path = "../derive" } [dev-dependencies] serde_json = "1.0" diff --git a/lib/examples/filler-op.rs b/lib/examples/filler-op.rs new file mode 100644 index 0000000..358d714 --- /dev/null +++ b/lib/examples/filler-op.rs @@ -0,0 +1,64 @@ +#[cfg(feature = "op")] +fn main() { + use std::collections::{ + BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque, + }; + use std::iter::{Extend, IntoIterator}; + use struct_patch::Filler; + + #[derive(Clone, Default, Filler)] + #[filler(attribute(derive(Clone, Debug, Default)))] + struct Item { + _field_complete: bool, + // Will check the field is equal to the value to define the field is empty or not + #[filler(empty_value = 0)] + // Will allow field add each other when `+` on fillers + #[filler(addable)] + field_int: usize, + _field_string: String, + maybe_field_int: Option, + maybe_field_string: Option, + // Will allow field extend each other when `+` on fillers + #[filler(addable)] + list: Vec, + _deque: VecDeque, + _linked_list: LinkedList, + _map: HashMap, + _bmap: BTreeMap, + _set: HashSet, + _bset: BTreeSet, + _heap: BinaryHeap, + } + + let item = Item::default(); + + let mut filler1: ItemFiller = Item::new_empty_filler(); + filler1.field_int = 7; + filler1.list = vec![1, 2]; + + let mut filler2: ItemFiller = Item::new_empty_filler(); + filler2.field_int = 8; + filler2.list = vec![3, 4]; + filler2.maybe_field_string = Some("Something".into()); + + let final_item_from_added_fillers = item.clone() << (filler1.clone() + filler2.clone()); + + assert_eq!(final_item_from_added_fillers.field_int, 15); + assert_eq!(final_item_from_added_fillers.list, vec![1, 2, 3, 4]); + assert_eq!( + final_item_from_added_fillers.maybe_field_string, + Some("Something".into()) + ); + + let final_item_after_fillers_applied = item << filler1 << filler2; + + assert_eq!(final_item_after_fillers_applied.field_int, 7); + assert_eq!(final_item_after_fillers_applied.list, vec![1, 2]); + assert_eq!( + final_item_after_fillers_applied.maybe_field_string, + Some("Something".into()) + ); +} + +#[cfg(not(feature = "op"))] +fn main() {} diff --git a/lib/examples/filler.rs b/lib/examples/filler.rs index b6e924f..06d2f9e 100644 --- a/lib/examples/filler.rs +++ b/lib/examples/filler.rs @@ -5,7 +5,7 @@ use struct_patch::Filler; use struct_patch::Status; // NOTE: Default, Extend, IntoIterator, is_empty are required for extendable type -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] struct WrapVec { inner: Vec, } @@ -30,8 +30,8 @@ impl WrapVec { } } -#[derive(Default, Filler)] -#[filler(attribute(derive(Debug, Default)))] +#[derive(Clone, Default, Filler)] +#[filler(attribute(derive(Clone, Debug, Default)))] struct Item { field_complete: bool, // Will check the field is equal to the value to define the field is empty or not