Skip to content

Commit

Permalink
feat: Implement patch for Box and Option
Browse files Browse the repository at this point in the history
  • Loading branch information
taorepoara committed Aug 9, 2024
1 parent fa92ce6 commit a773abb
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 8 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Box<P>>` trait for `T` where `T` implements `Patch<P>`.
This let you patch a boxed (or not) struct with a boxed patch.
- `option`: implements the `Patch<Option<P>>` trait for `Option<T>` where `T` implements `Patch<P>`.
`T` also needs to implement `From<P>`.
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
Expand Down
4 changes: 3 additions & 1 deletion struct-patch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ serde_with = "3.9.0"

[features]
default = ["status"]
std = ["box", "option"]
box = []
option = []
status = [
"struct-patch-derive/status"
]

2 changes: 1 addition & 1 deletion struct-patch/examples/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}"),
Expand Down
2 changes: 1 addition & 1 deletion struct-patch/examples/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions struct-patch/examples/patch-attr.rs
Original file line number Diff line number Diff line change
@@ -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)))]
Expand All @@ -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:?}"),
Expand Down
2 changes: 1 addition & 1 deletion struct-patch/examples/rename-patch-struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}"),
Expand Down
2 changes: 1 addition & 1 deletion struct-patch/examples/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion struct-patch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down
246 changes: 246 additions & 0 deletions struct-patch/src/std.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
#[cfg(any(feature = "box", feature = "option"))]
use crate::Patch;
#[cfg(feature = "box")]
use std::boxed::Box;

#[cfg(feature = "box")]
impl<T, P> Patch<Box<P>> for T
where
T: Patch<P>,
{
fn apply(&mut self, patch: Box<P>) {
self.apply(*patch);
}

fn into_patch(self) -> Box<P> {
Box::new(self.into_patch())
}

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

fn new_empty_patch() -> Box<P> {
Box::new(T::new_empty_patch())
}
}

#[cfg(feature = "option")]
/// Patch implementation for Option<T>
/// 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<T, P> Patch<Option<P>> for Option<T>
where
T: Patch<P> + From<P>,
{
fn apply(&mut self, patch: Option<P>) {
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<P> {
self.map(|x| x.into_patch())
}

fn into_patch_by_diff(self, previous_struct: Self) -> Option<P> {
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<P> {
Some(T::new_empty_patch())
}
}

#[cfg(test)]
mod tests {
use crate as struct_patch;
use crate::Patch;

// Tests for Patch<Box<P>> 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<Option<P>> 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<ItemPatch> 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<BPatch>",
attribute(serde(deserialize_with = "deserialize_optional_field", default))
)]
b: Option<B>,
}

impl From<BPatch> 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<Option<Option<T>>, 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 });
}
}
}
}

0 comments on commit a773abb

Please sign in to comment.