diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f2c08e..361df55 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,12 @@ jobs: nix develop -c cargo run --features=std --example option nix develop -c cargo test --features=std + - name: Test with merge features + run: | + nix develop -c cargo run --features=option --features=merge --example option + nix develop -c cargo run --features=merge --example op + nix develop -c cargo test --features=merge + - name: Test with option features run: | nix develop -c cargo run --features=none_as_default --example option diff --git a/README.md b/README.md index 06073d9..87d7b99 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ The [examples][examples] demo following scenarios. This crate also includes the following optional features: - `status`(default): implements the `PatchStatus` trait for the patch struct, which provides the `is_empty` method. - `op` (default): provide operators `<<` for instance and patch, and `+` for patches +- `merge` (optional): implements the `Merge` trait for the patch struct, which provides the `merge` method. - `std`(optional): - `box`: implements the `Patch>` trait for `T` where `T` implements `Patch

`. This let you patch a boxed (or not) struct with a boxed patch. diff --git a/struct-patch-derive/Cargo.toml b/struct-patch-derive/Cargo.toml index 5a65c7e..402d8eb 100644 --- a/struct-patch-derive/Cargo.toml +++ b/struct-patch-derive/Cargo.toml @@ -21,6 +21,7 @@ syn = { version = "2.0", features = ["parsing"] } [features] status = [] op = [] +merge = [] [dev-dependencies] pretty_assertions_sorted = "1.2.3" diff --git a/struct-patch-derive/src/lib.rs b/struct-patch-derive/src/lib.rs index b08704c..0c56a1f 100644 --- a/struct-patch-derive/src/lib.rs +++ b/struct-patch-derive/src/lib.rs @@ -100,6 +100,29 @@ impl Patch { #[cfg(not(feature = "status"))] let patch_status_impl = quote!(); + #[cfg(feature = "merge")] + let patch_merge_impl = quote!( + impl #generics struct_patch::traits::Merge for #name #generics #where_clause { + fn merge(self, other: Self) -> Self { + Self { + #( + #renamed_field_names: match (self.#renamed_field_names, other.#renamed_field_names) { + (Some(a), Some(b)) => Some(a.merge(b)), + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + (None, None) => None, + }, + )* + #( + #original_field_names: other.#original_field_names.or(self.#original_field_names), + )* + } + } + } + ); + #[cfg(not(feature = "merge"))] + let patch_merge_impl = quote!(); + #[cfg(feature = "op")] let op_impl = quote! { impl #generics core::ops::Shl<#name #generics> for #struct_name #generics #where_clause { @@ -111,18 +134,12 @@ impl Patch { } } + #[cfg(feature = "merge")] impl #generics core::ops::Shl<#name #generics> for #name #generics #where_clause { type Output = Self; - fn shl(mut self, rhs: #name #generics) -> Self { - Self { - #( - #renamed_field_names: rhs.#renamed_field_names.or(self.#renamed_field_names), - )* - #( - #original_field_names: rhs.#original_field_names.or(self.#original_field_names), - )* - } + fn shl(mut self, rhs: Self) -> Self { + struct_patch::traits::Merge::merge(self, rhs) } } @@ -222,6 +239,8 @@ impl Patch { #patch_status_impl + #patch_merge_impl + #patch_impl #op_impl diff --git a/struct-patch/Cargo.toml b/struct-patch/Cargo.toml index 4b1d4ea..2813b46 100644 --- a/struct-patch/Cargo.toml +++ b/struct-patch/Cargo.toml @@ -26,6 +26,9 @@ status = [ op = [ "struct-patch-derive/op" ] +merge = [ + "struct-patch-derive/merge" +] std = ["box", "option"] box = [] diff --git a/struct-patch/examples/op.rs b/struct-patch/examples/op.rs index 7fe1742..af1d441 100644 --- a/struct-patch/examples/op.rs +++ b/struct-patch/examples/op.rs @@ -52,12 +52,18 @@ fn main() { // Will be handdled as the discussion // https://github.com/yanganto/struct-patch/pull/32#issuecomment-2283154990 - let final_item_with_bracket = item.clone() << (conflict_patch.clone() << the_other_patch.clone()); - let final_item_without_bracket = item.clone() << conflict_patch << the_other_patch.clone(); - assert_eq!(final_item_with_bracket, final_item_without_bracket); - assert_eq!(final_item_with_bracket.field_int, 2); + let final_item_without_bracket = + item.clone() << conflict_patch.clone() << the_other_patch.clone(); assert_eq!(final_item_without_bracket.field_int, 2); + #[cfg(feature = "merge")] + { + let final_item_with_bracket = + item.clone() << (conflict_patch.clone() << the_other_patch.clone()); + assert_eq!(final_item_with_bracket, final_item_without_bracket); + assert_eq!(final_item_with_bracket.field_int, 2); + } + let final_item_from_merge = item.clone() << (another_patch.clone() + the_other_patch.clone()); assert_eq!(final_item_from_merge.field_string, "from another patch"); assert_eq!(final_item_from_merge.field_complete, true); diff --git a/struct-patch/src/lib.rs b/struct-patch/src/lib.rs index e8972af..a6fb3f4 100644 --- a/struct-patch/src/lib.rs +++ b/struct-patch/src/lib.rs @@ -55,6 +55,8 @@ pub use traits::*; #[cfg(test)] mod tests { use serde::Deserialize; + #[cfg(feature = "merge")] + use struct_patch::Merge; use struct_patch::Patch; #[cfg(feature = "status")] use struct_patch::PatchStatus; @@ -310,7 +312,7 @@ mod tests { ); } - #[cfg(feature = "op")] + #[cfg(all(feature = "op", feature = "merge"))] #[test] fn test_shl_on_patch() { #[derive(Patch, Debug, PartialEq)] @@ -385,4 +387,98 @@ mod tests { let patch2 = ItemPatch { field: Some(2) }; let _overall_patch = patch + patch2; } + + #[cfg(feature = "merge")] + #[test] + fn test_merge() { + #[derive(Patch)] + #[patch(attribute(derive(PartialEq, Debug)))] + struct Item { + a: u32, + b: u32, + c: u32, + d: u32, + } + + let patch = ItemPatch { + a: None, + b: Some(2), + c: Some(0), + d: None, + }; + let patch2 = ItemPatch { + a: Some(1), + b: None, + c: Some(3), + d: None, + }; + + let merged_patch = patch.merge(patch2); + assert_eq!( + merged_patch, + ItemPatch { + a: Some(1), + b: Some(2), + c: Some(3), + d: None, + } + ); + } + + #[cfg(feature = "merge")] + #[test] + fn test_merge_nested() { + #[derive(Patch, PartialEq, Debug)] + #[patch(attribute(derive(PartialEq, Debug, Clone)))] + struct B { + c: u32, + d: u32, + e: u32, + f: u32, + } + + #[derive(Patch)] + #[patch(attribute(derive(PartialEq, Debug)))] + struct A { + a: u32, + #[patch(name = "BPatch")] + b: B, + } + + let patches = vec![ + APatch { + a: Some(1), + b: Some(BPatch { + c: None, + d: Some(2), + e: Some(0), + f: None, + }), + }, + APatch { + a: Some(0), + b: Some(BPatch { + c: Some(1), + d: None, + e: Some(3), + f: None, + }), + }, + ]; + + let merged_patch = patches.into_iter().reduce(Merge::merge).unwrap(); + + assert_eq!( + merged_patch, + APatch { + a: Some(0), + b: Some(BPatch { + c: Some(1), + d: Some(2), + e: Some(3), + f: None, + }), + } + ); + } } diff --git a/struct-patch/src/std.rs b/struct-patch/src/std.rs index db866f1..99a590b 100644 --- a/struct-patch/src/std.rs +++ b/struct-patch/src/std.rs @@ -1,3 +1,5 @@ +#[cfg(all(feature = "merge", feature = "option"))] +use crate::Merge; #[cfg(any(feature = "box", feature = "option"))] use crate::Patch; #[cfg(feature = "box")] @@ -139,6 +141,24 @@ where } } +#[cfg(all(feature = "option", feature = "merge"))] +impl Merge for Option +where + T: Merge, +{ + fn merge(self, other: Self) -> Self { + if let Some(other) = other { + if self.is_some() { + Some(self.unwrap().merge(other)) + } else { + Some(other) + } + } else { + None + } + } +} + #[cfg(test)] mod tests { use crate as struct_patch; diff --git a/struct-patch/src/traits.rs b/struct-patch/src/traits.rs index 3f481d5..b6e9e69 100644 --- a/struct-patch/src/traits.rs +++ b/struct-patch/src/traits.rs @@ -97,3 +97,9 @@ pub trait PatchStatus { /// Returns `true` if all fields are `None`, `false` otherwise. fn is_empty(&self) -> bool; } + +#[cfg(feature = "merge")] +/// A patch struct that can be merged to another one +pub trait Merge { + fn merge(self, other: Self) -> Self; +}