diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 399c010..2226bdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,62 +19,65 @@ jobs: - name: Test no default features run: | - nix develop .#ci -c cargo run --no-default-features --example instance - nix develop .#ci -c cargo run --no-default-features --example filler - nix develop .#ci -c cargo run --no-default-features --example diff - nix develop .#ci -c cargo run --no-default-features --example json - nix develop .#ci -c cargo run --no-default-features --example rename-patch-struct - nix develop .#ci -c cargo run --no-default-features --example patch-attr - nix develop .#ci -c cargo run --no-default-features --example time - nix develop .#ci -c cargo run --no-default-features --example clap - nix develop .#ci -c cargo run --no-default-features --features=nesting --example nesting - nix develop .#ci -c cargo run --no-default-features --features=option --example option - nix develop .#ci -c cargo run --no-default-features --features=log --example log - nix develop .#ci -c cargo test --no-default-features + nix develop .#ci -c cargo run --quiet --no-default-features --example instance + nix develop .#ci -c cargo run --quiet --no-default-features --example filler + nix develop .#ci -c cargo run --quiet --no-default-features --example diff + nix develop .#ci -c cargo run --quiet --no-default-features --example json + nix develop .#ci -c cargo run --quiet --no-default-features --example rename-patch-struct + nix develop .#ci -c cargo run --quiet --no-default-features --example patch-attr + nix develop .#ci -c cargo run --quiet --no-default-features --example time + nix develop .#ci -c cargo run --quiet --no-default-features --example clap + nix develop .#ci -c cargo run --quiet --no-default-features --features=nesting --example nesting + nix develop .#ci -c cargo run --quiet --no-default-features --features=option --example option + nix develop .#ci -c cargo run --quiet --no-default-features --example=log + nix develop .#ci -c cargo run --quiet --no-default-features --features=box --example box + nix develop .#ci -c cargo test --quiet --no-default-features - name: Test with std features run: | - nix develop .#ci -c cargo run --features=std --example instance - nix develop .#ci -c cargo run --features=std --example filler - nix develop .#ci -c cargo run --features=std --example filler-op - nix develop .#ci -c cargo run --features=std --example diff - nix develop .#ci -c cargo run --features=std --example json - nix develop .#ci -c cargo run --features=std --example rename-patch-struct - nix develop .#ci -c cargo run --features=std --example patch-attr - nix develop .#ci -c cargo run --features=std --example option - nix develop .#ci -c cargo run --features=std,nesting --example nesting - nix develop .#ci -c cargo test --features=std + nix develop .#ci -c cargo run --quiet --features=std --example instance + nix develop .#ci -c cargo run --quiet --features=std --example filler + nix develop .#ci -c cargo run --quiet --features=std --example filler-op + nix develop .#ci -c cargo run --quiet --features=std --example diff + nix develop .#ci -c cargo run --quiet --features=std --example json + nix develop .#ci -c cargo run --quiet --features=std --example rename-patch-struct + nix develop .#ci -c cargo run --quiet --features=std --example patch-attr + nix develop .#ci -c cargo run --quiet --features=std --example option + nix develop .#ci -c cargo run --quiet --features=std --example box + nix develop .#ci -c cargo run --quiet --features=std,nesting --example nesting + nix develop .#ci -c cargo test --quiet --features=std - name: Test with merge features run: | - nix develop .#ci -c cargo run --features=option,merge --example option - nix develop .#ci -c cargo run --features=merge --example op - nix develop .#ci -c cargo run --features=merge,nesting --example nesting - nix develop .#ci -c cargo test --features=merge --no-default-features - nix develop .#ci -c cargo test --features=merge + nix develop .#ci -c cargo run --quiet --features=option,merge --example option + nix develop .#ci -c cargo run --quiet --features=merge --example op + nix develop .#ci -c cargo run --quiet --features=merge,nesting --example nesting + nix develop .#ci -c cargo test --quiet --features=merge --no-default-features + nix develop .#ci -c cargo test --quiet --features=merge - name: Test with option features run: | - nix develop .#ci -c cargo run --features=none_as_default --example option - nix develop .#ci -c cargo run --features=none_as_default,nesting --example nesting - nix develop .#ci -c cargo run --features=keep_none --example option - nix develop .#ci -c cargo run --features=keep_none,nesting --example nesting + nix develop .#ci -c cargo run --quiet --features=none_as_default --example option + nix develop .#ci -c cargo run --quiet --features=none_as_default,nesting --example nesting + nix develop .#ci -c cargo run --quiet --features=keep_none --example option + nix develop .#ci -c cargo run --quiet --features=keep_none,nesting --example nesting - name: Test with default features run: | - nix develop .#ci -c cargo run --example status - nix develop .#ci -c cargo run --example op - nix develop .#ci -c cargo run --example clap - nix develop .#ci -c cargo run --features=nesting --example nesting - nix develop .#ci -c cargo run --features=nesting --example clap - nix develop .#ci -c cargo run --features=log --example log - nix develop .#ci -c cargo test + nix develop .#ci -c cargo run --quiet --example status + nix develop .#ci -c cargo run --quiet --example op + nix develop .#ci -c cargo run --quiet --example clap + nix develop .#ci -c cargo run --quiet --features=nesting --example nesting + nix develop .#ci -c cargo run --quiet --features=nesting --example clap + nix develop .#ci -c cargo run --quiet --example=log + nix develop .#ci -c cargo run --quiet --features=box --example box + nix develop .#ci -c cargo test --quiet - name: Test in no std run: | cd no-std-examples - nix develop .#no-std -c cargo run --features=box --bin no-std-box - nix develop .#no-std -c cargo run --features=option --bin no-std-option + nix develop .#no-std -c cargo run --quiet --features=box --bin no-std-box + nix develop .#no-std -c cargo run --quiet --features=option --bin no-std-option - name: Test with catalyst run: nix develop .#ci -c check-catalyst diff --git a/README.md b/README.md index 0ee942e..f27cd85 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,7 @@ The [examples][examples] demonstrate the following scenarios: - show `skip_wrap` field behavior (`instance.rs`) - use `Patch` with `clap` for command-line config (`clap.rs`) - demonstrate `default_log` and `apply_with_log` for both `Patch` and `Filler` (`log.rs`) +- apply a heap-allocated (boxed) patch and produce a boxed diff (`box.rs`) ## Features @@ -337,9 +338,8 @@ This crate includes the following optional features: - By default, when there is a field conflict between patches/fillers, `+` will add them together if `#[patch(addable)]`, `#[patch(add = fn)]`, or `#[filler(addable)]` is provided; otherwise it will panic. - `merge` *(optional)*: implements the `Merge` trait for the patch struct, which provides the `merge` method, and `<<` (if `op` is enabled) between patches. - `alloc` *(optional)*: enables `alloc` support for `no_std` + alloc environments. -- `std` *(optional)*: enables `std`-dependent features (implies `box` and `option`). Note: the `log` example is incompatible with this feature. -- `box` *(optional)*: implements the `Patch>` trait for `T` where `T` implements `Patch

`. - This lets you patch a boxed (or unboxed) struct with a boxed patch. +- `std` *(optional)*: enables `std`-dependent features (implies `box` and `option`). +- `box` *(optional)*: for every struct that derives `Patch`, also generates `impl Patch> for Struct`, letting you apply a heap-allocated (boxed) patch directly via `item.apply(Box::new(patch))`. - `option` *(optional)*: implements the `Patch>` trait for `Option` where `T` implements `Patch

`. Please take a look at the example to learn more. - default behavior: `T` needs to implement `From

`. When patching on `None`, it converts the patch into `T` via `From

`, letting you patch structs containing fields with optional values. - `none_as_default` *(optional)*: `T` needs to implement `Default`. When patching on `None`, it patches on a default instance. Mutually exclusive with `keep_none`. diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 8a79f0d..b16fada 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -27,6 +27,7 @@ merge = [] nesting = [] catalyst = [ "syn-serde" ] unsafe = [] +box = [] [dev-dependencies] pretty_assertions_sorted = "1.2.3" diff --git a/derive/src/patch.rs b/derive/src/patch.rs index bbb49eb..1b35e8e 100644 --- a/derive/src/patch.rs +++ b/derive/src/patch.rs @@ -717,6 +717,54 @@ impl Patch { } }; + // A blanket `impl Patch> for T where T: Patch

` causes + // the Rust trait solver to build an infinite `Box>>` proof + // chain when `Filler` (which shares the method name `apply`) is used in + // the same crate, triggering a recursion-limit overflow. A concrete + // impl per derived type terminates the solver immediately. + #[cfg(feature = "box")] + let box_impl = quote! { + #[automatically_derived] + impl #generics struct_patch::traits::Patch> + for #struct_name #generics #where_clause + { + fn apply(&mut self, patch: struct_patch::__Box< #name #generics >) { + struct_patch::traits::Patch::apply(self, *patch); + } + + fn apply_with_log<__F: ::core::ops::FnMut(&str)>( + &mut self, + patch: struct_patch::__Box< #name #generics >, + log: __F, + ) { + struct_patch::traits::Patch::apply_with_log(self, *patch, log); + } + + fn into_patch(self) -> struct_patch::__Box< #name #generics > { + struct_patch::__Box::new( + struct_patch::traits::Patch::into_patch(self) + ) + } + + fn into_patch_by_diff( + self, + previous_struct: Self, + ) -> struct_patch::__Box< #name #generics > { + struct_patch::__Box::new( + struct_patch::traits::Patch::into_patch_by_diff(self, previous_struct) + ) + } + + fn new_empty_patch() -> struct_patch::__Box< #name #generics > { + struct_patch::__Box::new( + <#struct_name #generics as struct_patch::traits::Patch< #name #generics >>::new_empty_patch() + ) + } + } + }; + #[cfg(not(feature = "box"))] + let box_impl = quote! {}; + Ok(quote! { #patch_struct @@ -727,6 +775,8 @@ impl Patch { #patch_impl #op_impl + + #box_impl }) } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d9e73c6..d007b8f 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -36,7 +36,7 @@ merge = [ alloc = [] std = ["box", "option"] -box = ["alloc"] +box = ["alloc", "struct-patch-derive/box"] option = [] nesting = [ "struct-patch-derive/nesting" @@ -50,9 +50,7 @@ unsafe = [ "struct-patch-derive/unsafe" ] -# exmaple features for CI not reall feature -log = [] # avoid running std on log example [[example]] -name = "log" -required-features = ["log"] +name = "box" +required-features = ["box"] diff --git a/lib/examples/box.rs b/lib/examples/box.rs new file mode 100644 index 0000000..c97bcbb --- /dev/null +++ b/lib/examples/box.rs @@ -0,0 +1,79 @@ +use struct_patch::Patch; + +fn log_field(field: &str) { + println!("[default_log] field changed: {field}"); +} + +#[derive(Default, Patch)] +#[patch(attribute(derive(Debug, Default)))] +#[patch(default_log(log_field))] +struct Config { + host: String, + port: u16, + debug: bool, +} + +// Generated by Patch derive macro +// +// #[derive(Debug, Default)] +// struct ConfigPatch { +// host: Option, +// port: Option, +// debug: Option, +// } +// +// With the `box` feature enabled, the following impl is also generated: +// +// impl Patch> for Config { ... } + +fn main() { + // --- apply a boxed patch --- + + let mut config = Config::default(); + + let boxed: Box = Box::new(ConfigPatch { + host: Some("localhost".into()), + port: Some(8080), + debug: None, + }); + + config.apply(boxed); + + assert_eq!(config.host, "localhost"); + assert_eq!(config.port, 8080); + assert!(!config.debug); + + // --- apply_with_log also works through Box --- + + let mut patched_fields = Vec::new(); + config.apply_with_log( + Box::new(ConfigPatch { + host: None, + port: None, + debug: Some(true), + }), + |field| patched_fields.push(field.to_string()), + ); + + assert!(config.debug); + assert_eq!(patched_fields, vec!["debug"]); + + // --- into_patch and into_patch_by_diff return Box --- + + let snapshot = Config { + host: "localhost".into(), + port: 8080, + debug: false, + }; + let current = Config { + host: "localhost".into(), + port: 9090, + debug: true, + }; + + let diff: Box = current.into_patch_by_diff(snapshot); + + assert_eq!(diff.host, None); + assert_eq!(diff.port, Some(9090)); + assert_eq!(diff.debug, Some(true)); +} diff --git a/lib/src/box.rs b/lib/src/box.rs index 59465b3..ee5353f 100644 --- a/lib/src/box.rs +++ b/lib/src/box.rs @@ -1,61 +1,5 @@ #![cfg(feature = "box")] -use crate::Patch; extern crate alloc; -use alloc::boxed::Box; +pub use alloc::boxed::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(test)] -mod tests { - use super::*; - use crate as struct_patch; - use crate::Patch; - use alloc::string::String; - - #[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") - } - ); - } -} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index df03015..8735ea4 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -89,6 +89,9 @@ pub use struct_patch_derive::Patch; #[doc(hidden)] pub use struct_patch_derive::Substrate; pub mod r#box; +#[cfg(feature = "box")] +#[doc(hidden)] +pub use alloc::boxed::Box as __Box; pub mod option; pub mod traits; pub use traits::*;