Skip to content

Commit

Permalink
Add Shl and Add (#32)
Browse files Browse the repository at this point in the history
* Add `Add` between instance and patch

* update testcase

* Move `Add` to `add` feature

* Update struct-patch-derive/src/lib.rs

Co-authored-by: Thomas DA ROCHA <thomas.darocha@lenra.io>

* Add add example

* Reuse instance test case with add feature

* Fix Add with generics

* feat: Implement Add<patch> for patch (#33)

* feat: Implement Add<patch> for patch

* Use `Shl` as default feature and remove `add`

* Allow patch + patch if no conflict

* Setup `op` feature

* add more assertions on op example

* fix type specific issue with std feature

* refine panic message

---------

Co-authored-by: Thomas DA ROCHA <thomas.darocha@lenra.io>
  • Loading branch information
yanganto and taorepoara authored Aug 12, 2024
1 parent db68b86 commit 0ee5705
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 12 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ jobs:
- name: Test with default features
run: |
nix develop -c cargo run --example status
nix develop -c cargo run --example op
nix develop -c cargo test
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ fn patch_json() {
let patch: ItemPatch = serde_json::from_str(data).unwrap();

item.apply(patch);
// You can do
// `let new_item = item << patch;`

// For multiple patches,
// you can do this
// `let new_item = item << patch_1 << patch_2;`
// or make an aggregated one, but please make sure the patch fields do not conflict, else will panic
// ```
// let overall_patch = patch_1 + patch_2 + patch_3;
// let new_item = item << overall_patch;
// ```

assert_eq!(
item,
Expand Down Expand Up @@ -74,12 +85,13 @@ The [examples][examples] demo following scenarios.
- 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>`.
- `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
- `std`(optional):
- `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>`.
- `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.

Expand Down
1 change: 1 addition & 0 deletions struct-patch-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ syn = { version = "2.0", features = ["parsing"] }

[features]
status = []
op = []

[dev-dependencies]
pretty_assertions_sorted = "1.2.3"
62 changes: 62 additions & 0 deletions struct-patch-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,66 @@ impl Patch {
#[cfg(not(feature = "status"))]
let patch_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 {
self.apply(rhs);
self
}
}

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),
)*
}
}
}

impl #generics core::ops::Add<Self> for #name #generics #where_clause {
type Output = Self;

fn add(mut self, rhs: Self) -> Self {
Self {
#(
#renamed_field_names: match (self.#renamed_field_names, rhs.#renamed_field_names) {
(Some(a), Some(b)) => {
// TODO handle #[patch(add=)] fields
panic!("There are conflict patches on {}.{}", stringify!(#name), stringify!(#renamed_field_names))
},
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
)*
#(
#original_field_names: match (self.#original_field_names, rhs.#original_field_names) {
(Some(a), Some(b)) => {
// TODO handle #[patch(add=)] fields
panic!("There are conflict patches on {}.{}", stringify!(#name), stringify!(#original_field_names))
},
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
)*
}
}
}
};
#[cfg(not(feature = "op"))]
let op_impl = quote!();

let patch_impl = quote! {
impl #generics struct_patch::traits::Patch< #name #generics > for #struct_name #generics #where_clause {
fn apply(&mut self, patch: #name #generics) {
Expand Down Expand Up @@ -163,6 +223,8 @@ impl Patch {
#patch_status_impl

#patch_impl

#op_impl
})
}

Expand Down
12 changes: 8 additions & 4 deletions struct-patch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ serde = { version = "1", features = ["derive"] }
serde_with = "3.9.0"

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

std = ["box", "option"]
box = []
option = []
32 changes: 28 additions & 4 deletions struct-patch/examples/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use struct_patch::Patch;
#[derive(Default, Patch)]
#[patch(attribute(derive(Debug, Default)))]
struct Item {
field_bool: bool,
field_complete: bool,
field_int: usize,
field_string: String,
}
Expand All @@ -12,7 +12,7 @@ struct Item {
//
// #[derive(Debug, Default)] // pass by patch(attribute(...))
// struct ItemPatch {
// field_bool: Option<bool>,
// field_complete: Option<bool>,
// field_int: Option<usize>,
// field_string: Option<String>,
// }
Expand All @@ -26,12 +26,36 @@ fn main() {

assert_eq!(
format!("{patch:?}"),
"ItemPatch { field_bool: None, field_int: Some(7), field_string: None }"
"ItemPatch { field_complete: None, field_int: Some(7), field_string: None }"
);

item.apply(patch);

assert_eq!(item.field_bool, false);
assert_eq!(item.field_complete, false);
assert_eq!(item.field_int, 7);
assert_eq!(item.field_string, "");

#[cfg(feature = "op")]
{
let another_patch = ItemPatch {
field_complete: None,
field_int: None,
field_string: Some("from another patch".into()),
};
let new_item = item << another_patch;

assert_eq!(new_item.field_complete, false);
assert_eq!(new_item.field_int, 7);
assert_eq!(new_item.field_string, "from another patch");

let the_other_patch = ItemPatch {
field_complete: Some(true),
field_int: None,
field_string: None,
};
let final_item = new_item << the_other_patch;
assert_eq!(final_item.field_complete, true);
assert_eq!(final_item.field_int, 7);
assert_eq!(final_item.field_string, "from another patch");
}
}
65 changes: 65 additions & 0 deletions struct-patch/examples/op.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use struct_patch::Patch;

#[derive(Clone, Default, Patch)]
#[patch(attribute(derive(Clone, Debug, Default)))]
struct Item {
field_complete: bool,
field_int: usize,
field_string: String,
}

#[cfg(feature = "op")]
fn main() {
let mut item = Item::default();

let mut patch: ItemPatch = Item::new_empty_patch();

patch.field_int = Some(7);

assert_eq!(
format!("{patch:?}"),
"ItemPatch { field_complete: None, field_int: Some(7), field_string: None }"
);

item.apply(patch);

assert_eq!(item.field_complete, false);
assert_eq!(item.field_int, 7);
assert_eq!(item.field_string, "");

let another_patch = ItemPatch {
field_complete: None,
field_int: None,
field_string: Some("from another patch".into()),
};

let _conflict_patch = ItemPatch {
field_complete: None,
field_int: Some(1),
field_string: Some("from another patch".into()),
};

let the_other_patch = ItemPatch {
field_complete: Some(true),
field_int: Some(2),
field_string: None,
};

// let final_item_from_merge = item.clone() << (_conflict_patch + the_other_patch.clone());
// Will get panic `There are conflict patches on ItemPatch.field_int`
//
// TODO
// Will be handdled as the discussion
// https://github.com/yanganto/struct-patch/pull/32#issuecomment-2283154990

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);

let final_item_series_patch = item << another_patch << the_other_patch;
assert_eq!(final_item_series_patch.field_string, "from another patch");
assert_eq!(final_item_series_patch.field_complete, true);
}

#[cfg(not(feature = "op"))]
fn main() {}
103 changes: 103 additions & 0 deletions struct-patch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,107 @@ mod tests {
}
);
}

#[cfg(feature = "op")]
#[test]
fn test_shl() {
#[derive(Patch, Debug, PartialEq)]
struct Item {
field: u32,
other: String,
}

let item = Item {
field: 1,
other: String::from("hello"),
};
let patch = ItemPatch {
field: None,
other: Some(String::from("bye")),
};

assert_eq!(
item << patch,
Item {
field: 1,
other: String::from("bye")
}
);
}

#[cfg(feature = "op")]
#[test]
fn test_shl_on_patch() {
#[derive(Patch, Debug, PartialEq)]
struct Item {
field: u32,
other: String,
}

let mut item = Item {
field: 1,
other: String::from("hello"),
};
let patch = ItemPatch {
field: None,
other: Some(String::from("bye")),
};
let patch2 = ItemPatch {
field: Some(2),
other: None,
};

let new_patch = patch << patch2;

item.apply(new_patch);
assert_eq!(
item,
Item {
field: 2,
other: String::from("bye")
}
);
}

#[cfg(feature = "op")]
#[test]
fn test_add_patches() {
#[derive(Patch)]
#[patch(attribute(derive(Debug, PartialEq)))]
struct Item {
field: u32,
other: String,
}

let patch = ItemPatch {
field: Some(1),
other: None,
};
let patch2 = ItemPatch {
field: None,
other: Some(String::from("hello")),
};
let overall_patch = patch + patch2;
assert_eq!(
overall_patch,
ItemPatch {
field: Some(1),
other: Some(String::from("hello")),
}
);
}

#[cfg(feature = "op")]
#[test]
#[should_panic]
fn test_add_conflict_patches_panic() {
#[derive(Patch, Debug, PartialEq)]
struct Item {
field: u32,
}

let patch = ItemPatch { field: Some(1) };
let patch2 = ItemPatch { field: Some(2) };
let _overall_patch = patch + patch2;
}
}

0 comments on commit 0ee5705

Please sign in to comment.