From a773abb4773f5c33350eed0da30548502f886fbc Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Fri, 9 Aug 2024 12:29:56 +0200 Subject: [PATCH] feat: Implement patch for Box and Option --- README.md | 9 + struct-patch/Cargo.toml | 4 +- struct-patch/examples/diff.rs | 2 +- struct-patch/examples/instance.rs | 2 +- struct-patch/examples/patch-attr.rs | 4 +- struct-patch/examples/rename-patch-struct.rs | 2 +- struct-patch/examples/status.rs | 2 +- struct-patch/src/lib.rs | 4 +- struct-patch/src/std.rs | 246 +++++++++++++++++++ 9 files changed, 267 insertions(+), 8 deletions(-) create mode 100644 struct-patch/src/std.rs diff --git a/README.md b/README.md index 2e18fc0..1617962 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,15 @@ The [examples][examples] demo following scenarios. - check a patch is empty or not - add attribute to patch struct +## Features + +This crate also includes the following optional features: +- `status`: implements the `PatchStatus` trait for the patch struct, which provides the `is_empty` method. +- `box`: implements the `Patch>` trait for `T` where `T` implements `Patch

`. + This let you patch a boxed (or not) struct with a boxed patch. +- `option`: implements the `Patch>` trait for `Option` where `T` implements `Patch

`. + `T` also needs to implement `From

`. + This let you patch structs containing fields with optional values. [crates-badge]: https://img.shields.io/crates/v/struct-patch.svg [crate-url]: https://crates.io/crates/struct-patch diff --git a/struct-patch/Cargo.toml b/struct-patch/Cargo.toml index 488598b..c32e120 100644 --- a/struct-patch/Cargo.toml +++ b/struct-patch/Cargo.toml @@ -20,7 +20,9 @@ serde_with = "3.9.0" [features] default = ["status"] +std = ["box", "option"] +box = [] +option = [] status = [ "struct-patch-derive/status" ] - diff --git a/struct-patch/examples/diff.rs b/struct-patch/examples/diff.rs index 69a8683..e1fb9e0 100644 --- a/struct-patch/examples/diff.rs +++ b/struct-patch/examples/diff.rs @@ -16,7 +16,7 @@ fn main() { }; // Diff on two items to get the patch - let patch = new_item.into_patch_by_diff(item); + let patch: ItemPatch = new_item.into_patch_by_diff(item); assert_eq!( format!("{patch:?}"), diff --git a/struct-patch/examples/instance.rs b/struct-patch/examples/instance.rs index 6b1a43e..8f3dc36 100644 --- a/struct-patch/examples/instance.rs +++ b/struct-patch/examples/instance.rs @@ -20,7 +20,7 @@ struct Item { fn main() { let mut item = Item::default(); - let mut patch = Item::new_empty_patch(); + let mut patch: ItemPatch = Item::new_empty_patch(); patch.field_int = Some(7); diff --git a/struct-patch/examples/patch-attr.rs b/struct-patch/examples/patch-attr.rs index 9dbeabd..caab9ee 100644 --- a/struct-patch/examples/patch-attr.rs +++ b/struct-patch/examples/patch-attr.rs @@ -1,5 +1,5 @@ -use struct_patch::Patch; use serde_with::skip_serializing_none; +use struct_patch::Patch; #[derive(Default, Patch)] #[patch(attribute(derive(serde::Serialize, Debug, Default)))] @@ -21,7 +21,7 @@ struct Item { // } fn main() { - let patch = Item::new_empty_patch(); + let patch: ItemPatch = Item::new_empty_patch(); assert_eq!( format!("{patch:?}"), diff --git a/struct-patch/examples/rename-patch-struct.rs b/struct-patch/examples/rename-patch-struct.rs index ded16ee..3468e84 100644 --- a/struct-patch/examples/rename-patch-struct.rs +++ b/struct-patch/examples/rename-patch-struct.rs @@ -19,7 +19,7 @@ struct Item { // } fn main() { - let patch = Item::new_empty_patch(); + let patch: ItemOverlay = Item::new_empty_patch(); assert_eq!( format!("{patch:?}"), diff --git a/struct-patch/examples/status.rs b/struct-patch/examples/status.rs index 01ac70a..6a508f1 100644 --- a/struct-patch/examples/status.rs +++ b/struct-patch/examples/status.rs @@ -11,7 +11,7 @@ struct Item { } fn main() { - let mut patch = Item::new_empty_patch(); + let mut patch: ItemPatch = Item::new_empty_patch(); #[cfg(feature = "status")] assert!(patch.is_empty()); // provided by PatchStatus diff --git a/struct-patch/src/lib.rs b/struct-patch/src/lib.rs index 1a4fcaa..9fd6c03 100644 --- a/struct-patch/src/lib.rs +++ b/struct-patch/src/lib.rs @@ -43,10 +43,12 @@ //! ``` //! //! More details on how to use the the derive macro, including what attributes are available, are available under [`Patch`] -#![cfg_attr(not(test), no_std)] +#![cfg_attr(not(any(test, feature = "box", feature = "option")), no_std)] #[doc(hidden)] pub use struct_patch_derive::Patch; +#[cfg(any(feature = "box", feature = "option"))] +pub mod std; pub mod traits; pub use traits::*; diff --git a/struct-patch/src/std.rs b/struct-patch/src/std.rs new file mode 100644 index 0000000..1b00459 --- /dev/null +++ b/struct-patch/src/std.rs @@ -0,0 +1,246 @@ +#[cfg(any(feature = "box", feature = "option"))] +use crate::Patch; +#[cfg(feature = "box")] +use std::boxed::Box; + +#[cfg(feature = "box")] +impl Patch> for T +where + T: Patch

, +{ + fn apply(&mut self, patch: Box

) { + self.apply(*patch); + } + + fn into_patch(self) -> Box

{ + Box::new(self.into_patch()) + } + + fn into_patch_by_diff(self, previous_struct: Self) -> Box

{ + Box::new(self.into_patch_by_diff(previous_struct)) + } + + fn new_empty_patch() -> Box

{ + Box::new(T::new_empty_patch()) + } +} + +#[cfg(feature = "option")] +/// Patch implementation for Option +/// This implementation is used to apply a patch to an optional field +/// The `From` trait is used to convert the patch to the struct type +impl Patch> for Option +where + T: Patch

+ From

, +{ + fn apply(&mut self, patch: Option

) { + if let Some(patch) = patch { + if let Some(self_) = self { + self_.apply(patch); + } else { + *self = Some(patch.into()); + } + } else { + *self = None; + } + } + + fn into_patch(self) -> Option

{ + self.map(|x| x.into_patch()) + } + + fn into_patch_by_diff(self, previous_struct: Self) -> Option

{ + match (self, previous_struct) { + (Some(self_), Some(previous_struct_)) => { + Some(self_.into_patch_by_diff(previous_struct_)) + } + (Some(self_), None) => Some(self_.into_patch()), + (None, _) => None, + } + } + + fn new_empty_patch() -> Option

{ + Some(T::new_empty_patch()) + } +} + +#[cfg(test)] +mod tests { + use crate as struct_patch; + use crate::Patch; + + // Tests for Patch> implementation + #[cfg(feature = "box")] + mod patch_box { + use super::*; + + #[test] + fn test_patch_box_simple() { + #[derive(Patch, Debug, PartialEq)] + struct Item { + field: u32, + other: String, + } + + let mut item = Item { + field: 1, + other: String::from("hello"), + }; + let patch = Box::new(ItemPatch { + field: None, + other: Some(String::from("bye")), + }); + + item.apply(patch); + assert_eq!( + item, + Item { + field: 1, + other: String::from("bye") + } + ); + } + } + + // Test for Patch> implementation + #[cfg(feature = "option")] + mod patch_option { + use super::*; + + #[test] + fn test_patch_option() { + #[derive(Patch, Debug, PartialEq)] + struct Item { + field: u32, + other: String, + } + + impl From for Item { + fn from(patch: ItemPatch) -> Self { + Item { + field: patch.field.unwrap_or_default(), + other: patch.other.unwrap_or_default(), + } + } + } + + let mut item = Some(Item { + field: 1, + other: String::from("hello"), + }); + let patch = Some(ItemPatch { + field: None, + other: Some(String::from("bye")), + }); + + item.apply(patch); + assert_eq!( + item, + Some(Item { + field: 1, + other: String::from("bye") + }) + ); + } + + /// Tests for nested optional fields + /// See https://stackoverflow.com/questions/44331037/how-can-i-distinguish-between-a-deserialized-field-that-is-missing-and-one-that + /// and https://github.com/serde-rs/serde/issues/1042 + /// To understand how to manage optional fields in patch with serde + mod nested { + use super::*; + use serde::Deserializer; + use serde::Deserialize; + + #[derive(PartialEq, Debug, Patch, Deserialize)] + #[patch(attribute(derive(PartialEq, Debug, Deserialize)))] + struct B { + c: u32, + d: u32, + } + + #[derive(PartialEq, Debug, Patch, Deserialize)] + #[patch(attribute(derive(PartialEq, Debug, Deserialize)))] + struct A { + #[patch( + name = "Option", + attribute(serde(deserialize_with = "deserialize_optional_field", default)) + )] + b: Option, + } + + impl From for B { + fn from(patch: BPatch) -> Self { + B { + c: patch.c.unwrap_or_default(), + d: patch.d.unwrap_or_default(), + } + } + } + + fn deserialize_optional_field<'de, T, D>( + deserializer: D, + ) -> Result>, D::Error> + where + D: Deserializer<'de>, + T: Deserialize<'de>, + { + Ok(Some(Option::deserialize(deserializer)?)) + } + + #[test] + fn test_optional_nested_present() { + let mut a = A { + b: Some(B { c: 0, d: 0 }), + }; + let data = r#"{ "b": { "c": 1 } }"#; + let patch: APatch = serde_json::from_str(data).unwrap(); + assert_eq!( + patch, + APatch { + b: Some(Some(BPatch { + c: Some(1), + d: None + })) + } + ); + a.apply(patch); + assert_eq!( + a, + A { + b: Some(B { c: 1, d: 0 }) + } + ); + } + + #[test] + fn test_optional_nested_absent() { + let mut a = A { + b: Some(B { c: 0, d: 0 }), + }; + let data = r#"{ }"#; + let patch: APatch = serde_json::from_str(data).unwrap(); + assert_eq!(patch, APatch { b: None }); + a.apply(patch); + assert_eq!( + a, + A { + b: Some(B { c: 0, d: 0 }) + } + ); + } + + #[test] + fn test_optional_nested_null() { + let mut a = A { + b: Some(B { c: 0, d: 0 }), + }; + let data = r#"{ "b": null }"#; + let patch: APatch = serde_json::from_str(data).unwrap(); + assert_eq!(patch, APatch { b: Some(None) }); + a.apply(patch); + assert_eq!(a, A { b: None }); + } + } + } +}