Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 43 additions & 40 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<Box<P>>` trait for `T` where `T` implements `Patch<P>`.
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<Box<PatchStruct>> for Struct`, letting you apply a heap-allocated (boxed) patch directly via `item.apply(Box::new(patch))`.
- `option` *(optional)*: implements the `Patch<Option<P>>` trait for `Option<T>` where `T` implements `Patch<P>`. Please take a look at the example to learn more.
- default behavior: `T` needs to implement `From<P>`. When patching on `None`, it converts the patch into `T` via `From<P>`, 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`.
Expand Down
1 change: 1 addition & 0 deletions derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ merge = []
nesting = []
catalyst = [ "syn-serde" ]
unsafe = []
box = []

[dev-dependencies]
pretty_assertions_sorted = "1.2.3"
50 changes: 50 additions & 0 deletions derive/src/patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,54 @@ impl Patch {
}
};

// A blanket `impl<T,P> Patch<Box<P>> for T where T: Patch<P>` causes
// the Rust trait solver to build an infinite `Box<Box<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<struct_patch::__Box< #name #generics >>
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

Expand All @@ -727,6 +775,8 @@ impl Patch {
#patch_impl

#op_impl

#box_impl
})
}

Expand Down
8 changes: 3 additions & 5 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ merge = [

alloc = []
std = ["box", "option"]
box = ["alloc"]
box = ["alloc", "struct-patch-derive/box"]
option = []
nesting = [
"struct-patch-derive/nesting"
Expand All @@ -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"]
79 changes: 79 additions & 0 deletions lib/examples/box.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
// port: Option<u16>,
// debug: Option<bool>,
// }
//
// With the `box` feature enabled, the following impl is also generated:
//
// impl Patch<Box<ConfigPatch>> for Config { ... }

fn main() {
// --- apply a boxed patch ---

let mut config = Config::default();

let boxed: Box<ConfigPatch> = 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<ConfigPatch> ---

let snapshot = Config {
host: "localhost".into(),
port: 8080,
debug: false,
};
let current = Config {
host: "localhost".into(),
port: 9090,
debug: true,
};

let diff: Box<ConfigPatch> = current.into_patch_by_diff(snapshot);

assert_eq!(diff.host, None);
assert_eq!(diff.port, Some(9090));
assert_eq!(diff.debug, Some(true));
}
58 changes: 1 addition & 57 deletions lib/src/box.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,5 @@
#![cfg(feature = "box")]
use crate::Patch;

extern crate alloc;
use alloc::boxed::Box;
pub use alloc::boxed::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(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")
}
);
}
}
3 changes: 3 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down