Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
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 --example log
nix develop .#ci -c cargo test --no-default-features

- name: Test with std features
Expand Down Expand Up @@ -66,6 +67,7 @@ jobs:
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 --example log
nix develop .#ci -c cargo test

- name: Test in no std
Expand Down
89 changes: 88 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A library to help you modify config structs. It provides:

This crate provides the `Patch`, `Filler`, `Substrate`, `Catalyst` and `Complex` traits with accompanying derive macros in the following three use cases.

- If any field in a `Patch` is `Some`, it overwrites the corresponding field when applied.
- If any field in a `Patch` is `Some`, it overwrites the corresponding field when applied. Use `apply_with_log` to receive a callback for each field that is actually changed.
- If any field in the instance is empty (`None` or an empty collection), `Filler` will try to fill it. It supports `Option`, `Vec`, `VecDeque`, `LinkedList`, `HashMap`, `BTreeMap`, `HashSet`, `BTreeSet`, `BinaryHeap` fields, as well as custom types via `#[filler(extendable)]` and any type via `#[filler(empty_value = ...)]`.
- With the `catalyst` feature, `Substrate`, `Catalyst` and `Complex` traits with accompanying derive macros help you extend a struct with extra fields from another crate.

Expand Down Expand Up @@ -156,6 +156,90 @@ struct Amyloid {
// }
```

#### Case 5 - Log which fields were patched or filled

Both `Patch` and `Filler` support two ways to observe which fields are changed:

**Ad-hoc at the call site** — use `apply_with_log`, which takes a closure that
is called with each patched/filled field name:

```rust
use struct_patch::{Filler, Patch};

#[derive(Default, Patch)]
struct Item {
field_int: usize,
field_string: String,
}

let mut item = Item::default();
let patch = ItemPatch { field_int: Some(42), field_string: None };

let mut patched_fields = Vec::new();
item.apply_with_log(patch, |field| patched_fields.push(field.to_string()));

assert_eq!(patched_fields, vec!["field_int"]);
assert_eq!(item.field_int, 42);

#[derive(Default, Filler)]
struct Settings {
theme: Option<String>,
}

let mut settings = Settings::default();
let mut filled_fields = Vec::new();
settings.apply_with_log(
SettingsFiller { theme: Some("dark".into()) },
|field| filled_fields.push(field.to_string()),
);
assert_eq!(filled_fields, vec!["theme"]);
```

For structs using `#[patch(nesting)]`, the log closure is threaded into nested
patches so you receive field names from all levels of nesting.

**Always-on via struct attribute** — use `#[patch(default_log(fn_path))]` or
`#[filler(default_log(fn_path))]` to wire a specific function into `apply`
itself. Every call to `apply` on that struct will automatically invoke the
function for each field that is changed, with no extra effort at call sites.
Has no effect on `apply_with_log`.

```rust
use struct_patch::{Filler, Patch};

fn my_log(field: &str) {
println!("patched: {field}");
}

#[derive(Default, Patch)]
#[patch(default_log(my_log))]
struct Config {
retries: usize,
timeout: u64,
}

let mut cfg = Config::default();
cfg.apply(ConfigPatch { retries: Some(3), timeout: None });
// prints: patched: retries

fn my_filler_log(field: &str) {
println!("filled: {field}");
}

#[derive(Default, Filler)]
#[filler(default_log(my_filler_log))]
struct Settings {
theme: Option<String>,
}

let mut settings = Settings::default();
settings.apply(SettingsFiller { theme: Some("dark".into()) });
// prints: filled: theme
```

The path may be any item path (`crate::logging::log_field`,
`tracing::debug!` wrapped in a thin function, etc.).

#### Case 4 - Avoid double-`Option` for `Option<Vec<_>>` fields
By default, deriving `Patch` wraps every field in an `Option`, so a field typed
`Option<Vec<T>>` becomes `Option<Option<Vec<T>>>` in the generated patch. When
Expand Down Expand Up @@ -199,7 +283,9 @@ Two attribute namespaces are provided for the catalyst feature because we need t
- `#[patch(name = "...")]`: change the name of the generated patch struct.
- `#[patch(attribute(...))]`: add attributes to the generated patch struct.
- `#[patch(attribute(derive(...)))]`: add derives to the generated patch struct.
- `#[patch(default_log(fn_path))]`: call `fn_path` with each patched field name on every `apply` call. Has no effect on `apply_with_log`. The function must accept `&str`.
- `#[filler(attribute(...))]`: add attributes to the generated filler struct.
- `#[filler(default_log(fn_path))]`: call `fn_path` with each filled field name on every `apply` call. Has no effect on `apply_with_log`. The function must accept `&str`.
- `#[catalyst(bind = "...")]`: specify the base (substrate) structure. (catalyst feature)
- `#[catalyst(keep_field_attribute)]`: pass all field attributes from a substrate or catalyst through to the complex, unless an override is explicitly specified for that field. (catalyst feature)
- `#[catalyst(exclude_field_attributes = ["..."])]`: when `keep_field_attribute` is used, specifies attribute names to exclude from being passed through to the complex struct fields. For example, `exclude_field_attributes = ["serde"]` strips all `#[serde(...)]` field attributes from the substrate before they reach the complex. (catalyst feature)
Expand Down Expand Up @@ -241,6 +327,7 @@ The [examples][examples] demonstrate the following scenarios:
- show operators on fillers (`filler-op.rs`)
- 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`)

## Features

Expand Down
8 changes: 7 additions & 1 deletion derive/src/catalyst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,13 @@ impl Catalyst {

let complex_fields = raw_complex_fields
.iter()
.map(|f| f.to_token_stream(*keep_field_attribute, override_field_attributes, exclude_field_attributes))
.map(|f| {
f.to_token_stream(
*keep_field_attribute,
override_field_attributes,
exclude_field_attributes,
)
})
.collect::<Result<Vec<_>>>()?;

#[cfg(not(feature = "unsafe"))]
Expand Down
48 changes: 48 additions & 0 deletions derive/src/filler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const ATTRIBUTE: &str = "attribute";
const EXTENDABLE: &str = "extendable";
const EMPTY_VALUE: &str = "empty_value";
const ADDABLE: &str = "addable";
const DEFAULT_LOG: &str = "default_log";

pub(crate) struct Filler {
visibility: syn::Visibility,
Expand All @@ -19,6 +20,7 @@ pub(crate) struct Filler {
generics: syn::Generics,
attributes: Vec<TokenStream>,
fields: Vec<Field>,
default_log_fn: Option<syn::Path>,
}

enum FillerType {
Expand Down Expand Up @@ -65,6 +67,7 @@ impl Filler {
generics,
attributes,
fields,
default_log_fn,
} = self;

let filler_struct_fields = fields
Expand Down Expand Up @@ -225,23 +228,60 @@ impl Filler {
#[cfg(not(feature = "op"))]
let op_impl = quote!();

let make_log_calls = |names: &[Option<&Ident>]| -> Vec<TokenStream> {
if let Some(f) = default_log_fn {
names.iter().map(|n| quote! { #f(stringify!(#n)); }).collect()
} else {
names.iter().map(|_| quote! {}).collect()
}
};
let native_value_log_calls = make_log_calls(&native_value_field_names);
let extendable_log_calls = make_log_calls(&extendable_field_names);
let option_log_calls = make_log_calls(&option_field_names);

let filler_impl = quote! {
#[automatically_derived]
impl #generics struct_patch::traits::Filler< #name #generics > for #struct_name #generics #where_clause {
fn apply(&mut self, filler: #name #generics) {
#(
if self.#native_value_field_names == #native_value_field_empty_values {
#native_value_log_calls
self.#native_value_field_names = filler.#native_value_field_names;
}
)*
#(
if self.#extendable_field_names.is_empty() {
#extendable_log_calls
self.#extendable_field_names.extend(filler.#extendable_field_names.into_iter());
}
)*
#(
if let Some(v) = filler.#option_field_names {
if self.#option_field_names.is_none() {
#option_log_calls
self.#option_field_names = Some(v);
}
}
)*
}

fn apply_with_log<__L: FnMut(&str)>(&mut self, filler: #name #generics, mut log: __L) {
#(
if self.#native_value_field_names == #native_value_field_empty_values {
log(stringify!(#native_value_field_names));
self.#native_value_field_names = filler.#native_value_field_names;
}
)*
#(
if self.#extendable_field_names.is_empty() {
log(stringify!(#extendable_field_names));
self.#extendable_field_names.extend(filler.#extendable_field_names.into_iter());
}
)*
#(
if let Some(v) = filler.#option_field_names {
if self.#option_field_names.is_none() {
log(stringify!(#option_field_names));
self.#option_field_names = Some(v);
}
}
Expand Down Expand Up @@ -287,6 +327,7 @@ impl Filler {

let mut attributes = vec![];
let mut fields = vec![];
let mut default_log_fn: Option<syn::Path> = None;

for attr in attrs {
if attr.path().to_string().as_str() != FILLER {
Expand All @@ -310,6 +351,12 @@ impl Filler {
let attribute: TokenStream = content.parse()?;
attributes.push(attribute);
}
DEFAULT_LOG => {
// #[filler(default_log(path::to::fn))]
let content;
parenthesized!(content in meta.input);
default_log_fn = Some(content.parse()?);
}
_ => {
return Err(meta.error(format_args!(
"unknown filler container attribute `{}`",
Expand Down Expand Up @@ -337,6 +384,7 @@ impl Filler {
generics,
attributes,
fields,
default_log_fn,
})
}
}
Expand Down
Loading
Loading