<!--
This readme is created with https://crates.io/crates/cargo-rdme
To update: edit `cargo_diff/src/lib.rs` then run:
```
$ cargo rdme -w cache_diff
```
Note: All intra-doc links need a certain type of formattting for rdme to expand
them to the correct docs.rs links. More info found here:
https://github.com/orium/cargo-rdme/blob/964a939c8c86a2e6aa3f6a8f89cf75b64ab92f6a/README.md#intralinks
-->
# cache_diff
<!-- cargo-rdme start -->
Generate clean, human readable diffs between two cache structs
### What
The `CacheDiff` trait provides a way to compare two structs and generate a list of differences between them.
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.
Top level struct configuration (Container attributes):
- `#[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
Cloud Native Buildpacks (CNBs) written in Rust using [libcnb.rs](https://github.com/heroku/libcnb.rs) use
a serializable struct to represent the state of a cache. When that data changes, we need to invalidate the
cache, but also report back to the user what changed.
Due to the CNB layer implementation, this struct is often called "metadata".
### Install
```shell
$ cargo add cache_diff
```
For ANSI colored output, add the [`bullet_stream`](https://github.com/heroku-buildpacks/bullet_stream) feature:
```shell
$ cargo add cache_diff --features bullet_stream
```
### Derive usage
By default a `#[derive(CacheDiff)]` will generate a `diff` function that compares each field in the struct.
You can disable this dependency by specifying `features = []`.
```rust
use cache_diff::CacheDiff;
#[derive(CacheDiff)]
struct Metadata {
version: String,
}
let diff = Metadata { version: "3.4.0".to_string() }
.diff(&Metadata { version: "3.3.0".to_string() });
assert_eq!(diff.join(" "), "version (`3.3.0` to `3.4.0`)");
```
Struct fields must implement [`PartialEq`](std::cmp::PartialEq) and [`Display`](std::fmt::Display). Also note that [`PartialEq`](std::cmp::PartialEq) on the top level
cache struct is not used or required. If you want to customize equality logic, you can implement
the `CacheDiff` trait manually:
```rust
use cache_diff::CacheDiff;
#[derive(Debug)]
struct Metadata {
version: String,
}
// Implement the trait manually
impl CacheDiff for Metadata {
fn diff(&self, old: &Self) -> Vec<String> {
let mut diff = vec![];
// This evaluation logic differs from the derive macro
if !self.custom_compare_eq(old) {
diff.push(format!("Cache is different ({old:?} to {self:?})"));
}
diff
}
}
impl Metadata {
fn custom_compare_eq(&self, old: &Self) -> bool {
todo!()
}
}
```
### Ordering
The order of output will match the struct field definition from top to bottom:
```rust
use cache_diff::CacheDiff;
#[derive(CacheDiff)]
struct Metadata {
version: String,
distro: String,
}
let now = Metadata { version: "3.4.0".to_string(), distro: "Ubuntu".to_string() };
let diff = now.diff(&Metadata { version: "3.3.0".to_string(), distro: "Alpine".to_string() });
assert_eq!(diff.join(", "), "version (`3.3.0` to `3.4.0`), distro (`Alpine` to `Ubuntu`)");
```
### Rename attributes
If your field name is not descriptive enough, you can rename it:
```rust
use cache_diff::CacheDiff;
#[derive(CacheDiff)]
struct Metadata {
#[cache_diff(rename="Ruby version")]
version: String,
}
let now = Metadata { version: "3.4.0".to_string() };
let diff = now.diff(&Metadata { version: "3.3.0".to_string() });
assert_eq!(diff.join(" "), "Ruby version (`3.3.0` to `3.4.0`)");
```
### Ignore attributes
If the struct contains fields that should not be included in the diff comparison, you can ignore them:
```rust
use cache_diff::CacheDiff;
#[derive(CacheDiff)]
struct Metadata {
version: String,
#[cache_diff(ignore)]
changed_by: String
}
let now = Metadata { version: "3.4.0".to_string(), changed_by: "Alice".to_string() };
let diff = now.diff(&Metadata { version: now.version.clone(), changed_by: "Bob".to_string() });
assert!(diff.is_empty());
```
### Handle structs missing display
Not all structs implement the [`Display`](std::fmt::Display) trait, for example [`std::path::PathBuf`](std::path::PathBuf) requires that you call `display()` on it.
The `#[derive(CacheDiff)]` macro will automatically handle the following conversions for you:
- `std::path::PathBuf` (via [`std::path::Path::display`](std::path::Path::display))
However, if you have a custom struct that does not implement [`Display`](std::fmt::Display), you can specify a function to call instead:
```rust
use cache_diff::CacheDiff;
#[derive(CacheDiff)]
struct Metadata {
#[cache_diff(display = my_function)]
version: NoDisplay,
}
#[derive(PartialEq)]
struct NoDisplay(String);
fn my_function(s: &NoDisplay) -> String {
format!("custom {}", s.0)
}
let now = Metadata { version: NoDisplay("3.4.0".to_string())};
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
Releases can be performed via `cargo release`:
```
$ cargo install cargo-release
```
Release readiness for all crates can be checked by running:
```
$ cargo release --workspace --exclude usage --dry-run
```
When satisfied, contributors with permissions can release by running:
```
$ cargo release --workspace --exclude usage --execute
```