Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Shl between instance and patch #32

Merged
merged 15 commits into from
Aug 12, 2024
Merged
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;
}
}