Skip to content

Add custom = <function> container attribute and allow optional ignore = "<reason>" for fields #6

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

Merged
merged 6 commits into from
Jan 29, 2025
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## Unreleased

## 1.1.0

- Add `#[cache_diff(custom = <function>)]` to containers (structs) to allow for customizing + deriving diffs. (https://github.com/heroku-buildpacks/cache_diff/pull/6)
- Add: Allow annotating ignored fields with `#[cache_diff(ignore = "<reason>")]`. Using `ignore = "custom"` requires the container (struct) to implement `custom = <function>`. (https://github.com/heroku-buildpacks/cache_diff/pull/6)

## 1.0.1

- Fix: Multiple `#[derive(CachDiff)]` calls in the same file now work (https://github.com/heroku-buildpacks/cache_diff/pull/4)

## 1.0.0
Expand Down
37 changes: 35 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ members = [
]

[workspace.package]
version = "1.0.1"
version = "1.1.0"
rust-version = "1.82"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/heroku-buildpacks/cache_diff"
documentation = "https://docs.rs/cache_diff"

[workspace.dependencies]
pretty_assertions = "1.4.1"
indoc = "2"
serde = {version = "1.0", features = ["derive"] }
65 changes: 61 additions & 4 deletions cache_diff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ When it returns an empty list, the two structs are identical.

You can manually implement the trait, or you can use the `#[derive(CacheDiff)]` macro to automatically generate the implementation.

Attributes are:
Top level struct configuration (Container attributes):

- `cache_diff(rename = "<new name>")` Specify custom name for the field
- `cache_diff(ignore)` Ignores the given field
- `cache_diff(display = <function>)` Specify a function to call to display the field
- `#[cache_diff(custom = <function>)]` Specify a function that receives references to both current and old values and returns a Vec of strings if there are any differences. This function is only called once. It can be in combination with `#[cache_diff(custom)]` on fields to combine multiple related fields into one diff (for example OS distribution and version) or to split apart a monolithic field into multiple differences (for example an "inventory" struct that contains a version and CPU architecture information).

Attributes for fields are:

- `#[cache_diff(rename = "<new name>")]` Specify custom name for the field
- `#[cache_diff(ignore)]` or `#[cache_diff(ignore = "<reason>")]` Ignores the given field with an optional comment string.
If the field is ignored because you're using a custom diff function (see container attributes) you can use
`cache_diff(ignore = "custom")` which will check that the container implements a custom function.

### Why

Expand Down Expand Up @@ -189,6 +194,58 @@ let diff = now.diff(&Metadata { version: NoDisplay("3.3.0".to_string())});
assert_eq!(diff.join(" "), "version (`custom 3.3.0` to `custom 3.4.0`)");
```

### Customize one or more field differences

You can provide a custom implementation for a diffing a subset of fields without having to roll your own implementation.

#### Custom logic for one field example

Here's an example where someone wants to bust the cache after N cache calls. Everything else other than `cache_usage_count` can be derived. If you want to keep the existing derived difference checks, but add on a custom one you can do it like this:

```rust
use cache_diff::CacheDiff;
const MAX: f32 = 200.0;

#[derive(Debug, CacheDiff)]
#[cache_diff(custom = diff_cache_usage_count)]
pub(crate) struct Metadata {
#[cache_diff(ignore = "custom")]
cache_usage_count: f32,

binary_version: String,
target_arch: String,
os_distribution: String,
os_version: String,
}

fn diff_cache_usage_count(_old: &Metadata, now: &Metadata) -> Vec<String> {
let Metadata {
cache_usage_count,
binary_version: _,
target_arch: _,
os_distribution: _,
os_version: _,
} = now;

if cache_usage_count > &MAX {
vec![format!("Cache count ({}) exceeded limit {MAX}", cache_usage_count)]
} else {
Vec::new()
}
}
```

In this example, four fields are derived automatically, saving us time, while one field is custom
using the `#[cache_diff(custom = diff_cache_usage_count)]` attribute on the struct. This tells
[CacheDiff] to call this function and pass in the old and current values. It expects a vector
with some strings if there is a difference and an empty vector if there are none.

Don't forget to "ignore" any fields you're implementing yourself. You can also use this feature to
combine several fields into a single diff output, for example using the previous struct, if
you only wanted to have one output for a combined `os_distribution` and `os_version` in one output
like "OS (ubuntu-22 to ubuntu-24)". Alternatively, you can use <https://github.com/schneems/magic_migrate> to
re-arrange your struct to only have one field with a custom display.

<!-- cargo-rdme end -->

## Releasing
Expand Down
65 changes: 61 additions & 4 deletions cache_diff/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
//!
//! You can manually implement the trait, or you can use the `#[derive(CacheDiff)]` macro to automatically generate the implementation.
//!
//! Attributes are:
//! Top level struct configuration (Container attributes):
//!
//! - `cache_diff(rename = "<new name>")` Specify custom name for the field
//! - `cache_diff(ignore)` Ignores the given field
//! - `cache_diff(display = <function>)` Specify a function to call to display the field
//! - `#[cache_diff(custom = <function>)]` Specify a function that receives references to both current and old values and returns a Vec of strings if there are any differences. This function is only called once. It can be in combination with `#[cache_diff(custom)]` on fields to combine multiple related fields into one diff (for example OS distribution and version) or to split apart a monolithic field into multiple differences (for example an "inventory" struct that contains a version and CPU architecture information).
//!
//! Attributes for fields are:
//!
//! - `#[cache_diff(rename = "<new name>")]` Specify custom name for the field
//! - `#[cache_diff(ignore)]` or `#[cache_diff(ignore = "<reason>")]` Ignores the given field with an optional comment string.
//! If the field is ignored because you're using a custom diff function (see container attributes) you can use
//! `cache_diff(ignore = "custom")` which will check that the container implements a custom function.
//!
//! ## Why
//!
Expand Down Expand Up @@ -169,6 +174,58 @@
//!
//! assert_eq!(diff.join(" "), "version (`custom 3.3.0` to `custom 3.4.0`)");
//! ```
//!
//! ## Customize one or more field differences
//!
//! You can provide a custom implementation for a diffing a subset of fields without having to roll your own implementation.
//!
//! ### Custom logic for one field example
//!
//! Here's an example where someone wants to bust the cache after N cache calls. Everything else other than `cache_usage_count` can be derived. If you want to keep the existing derived difference checks, but add on a custom one you can do it like this:
//!
//! ```rust
//! use cache_diff::CacheDiff;
//! const MAX: f32 = 200.0;
//!
//! #[derive(Debug, CacheDiff)]
//! #[cache_diff(custom = diff_cache_usage_count)]
//! pub(crate) struct Metadata {
//! #[cache_diff(ignore = "custom")]
//! cache_usage_count: f32,
//!
//! binary_version: String,
//! target_arch: String,
//! os_distribution: String,
//! os_version: String,
//! }
//!
//! fn diff_cache_usage_count(_old: &Metadata, now: &Metadata) -> Vec<String> {
//! let Metadata {
//! cache_usage_count,
//! binary_version: _,
//! target_arch: _,
//! os_distribution: _,
//! os_version: _,
//! } = now;
//!
//! if cache_usage_count > &MAX {
//! vec![format!("Cache count ({}) exceeded limit {MAX}", cache_usage_count)]
//! } else {
//! Vec::new()
//! }
//! }
//! ```
//!
//! In this example, four fields are derived automatically, saving us time, while one field is custom
//! using the `#[cache_diff(custom = diff_cache_usage_count)]` attribute on the struct. This tells
//! [CacheDiff] to call this function and pass in the old and current values. It expects a vector
//! with some strings if there is a difference and an empty vector if there are none.
//!
//! Don't forget to "ignore" any fields you're implementing yourself. You can also use this feature to
//! combine several fields into a single diff output, for example using the previous struct, if
//! you only wanted to have one output for a combined `os_distribution` and `os_version` in one output
//! like "OS (ubuntu-22 to ubuntu-24)". Alternatively, you can use <https://github.com/schneems/magic_migrate> to
//! re-arrange your struct to only have one field with a custom display.

/// Centralized cache invalidation logic with human readable differences
///
Expand Down
4 changes: 4 additions & 0 deletions cache_diff_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ strum = {version = "0.26", features = ["derive"] }

[lib]
proc-macro = true

[dev-dependencies]
pretty_assertions.workspace = true
indoc.workspace = true
57 changes: 49 additions & 8 deletions cache_diff_derive/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub(crate) struct CacheDiffAttributes {
pub(crate) display: Option<syn::Path>,

/// When `Some` indicates the field should be ignored in the diff comparison
pub(crate) ignore: Option<()>,
pub(crate) ignore: Option<String>,
}

impl CacheDiffAttributes {
Expand Down Expand Up @@ -89,14 +89,19 @@ impl syn::parse::Parse for CacheDiffAttributes {
let name_str = name.to_string();
let mut attribute = CacheDiffAttributes::default();
match Key::from_str(&name_str).map_err(|_| {
let extra = match name_str.as_ref() {
"custom" => "\nThe cache_diff attribute `custom` is available on the struct, not the field",
_ => ""
};

syn::Error::new(
name.span(),
format!(
"Unknown cache_diff attribute: `{name_str}`. Must be one of {}",
Key::iter()
.map(|k| format!("`{k}`"))
.collect::<Vec<String>>()
.join(", ")
"Unknown cache_diff attribute: `{name_str}`. Must be one of {valid_keys}{extra}",
valid_keys = Key::iter()
.map(|k| format!("`{k}`"))
.collect::<Vec<String>>()
.join(", ")
),
)
})? {
Expand All @@ -110,7 +115,12 @@ impl syn::parse::Parse for CacheDiffAttributes {
attribute.display = Some(input.parse()?);
}
Key::ignore => {
attribute.ignore = Some(());
if input.peek(syn::Token![=]) {
input.parse::<syn::Token![=]>()?;
attribute.ignore = Some(input.parse::<syn::LitStr>()?.value());
} else {
attribute.ignore = Some("_ignored".to_string());
}
}
}
Ok(attribute)
Expand All @@ -120,6 +130,8 @@ impl syn::parse::Parse for CacheDiffAttributes {
#[cfg(test)]
mod test {
use super::*;
use indoc::formatdoc;
use pretty_assertions::assert_eq;

#[test]
fn test_parse_all_rename() {
Expand All @@ -145,18 +157,47 @@ mod test {
assert_eq!(CacheDiffAttributes::parse_all(&input).unwrap(), expected);
}

#[test]
fn test_ignore_with_value() {
let input = syn::parse_quote! {
#[cache_diff(ignore = "value")]
};
let expected = CacheDiffAttributes {
ignore: Some("value".to_string()),
..Default::default()
};
assert_eq!(CacheDiffAttributes::parse_all(&input).unwrap(), expected);
}

#[test]
fn test_parse_all_ignore() {
let input = syn::parse_quote! {
#[cache_diff(ignore)]
};
let expected = CacheDiffAttributes {
ignore: Some(()),
ignore: Some("_ignored".to_string()),
..Default::default()
};
assert_eq!(CacheDiffAttributes::parse_all(&input).unwrap(), expected);
}

#[test]
fn test_parse_accidental_custom() {
let input = syn::parse_quote! {
#[cache_diff(custom = "IDK")]
};
let result = CacheDiffAttributes::parse_all(&input);
assert!(result.is_err(), "Expected an error, got {:?}", result);
assert_eq!(
format!("{}", result.err().unwrap()).trim(),
formatdoc! {"
Unknown cache_diff attribute: `custom`. Must be one of `rename`, `display`, `ignore`
The cache_diff attribute `custom` is available on the struct, not the field
"}
.trim()
);
}

#[test]
fn test_parse_all_unknown() {
let input = syn::parse_quote! {
Expand Down
Loading