delta-struct 0.2.0

Delta struct provides a rust-lang Deriveable trait, Delta, that can be used to compute the difference (aka delta) between two instances of a type.
Documentation

Compute the difference (delta) between two instances of a type, and apply that difference to a third.

Deriving [Delta] on a struct generates a companion "delta struct" holding only what changed, plus an implementation of the [Delta] trait that knows how to produce one and how to apply it. Pair it with serde and you can send updates over the wire without resending state that both sides already agree on.

Quick start

use delta_struct::Delta;

#[derive(Delta)]
struct Config {
    host: String,
    port: u16,
}

let old = Config { host: "localhost".to_string(), port: 80 };
let new = Config { host: "localhost".to_string(), port: 8080 };

// `Config` gained a companion struct named `ConfigDelta`.
let delta = Delta::delta(old, new).expect("the port changed");
assert_eq!(delta.host, None);          // unchanged fields are `None`
assert_eq!(delta.port, Some(8080));

// Applying the delta to an older copy brings it up to date.
let mut current = Config { host: "localhost".to_string(), port: 80 };
current.apply_delta(delta);
assert_eq!(current.port, 8080);

Note that a single use delta_struct::Delta; imports both the trait and the derive macro. The trait has to be in scope wherever you derive it — the generated code refers to Delta by that name.

[Delta::delta] returns [None] when nothing changed, so if let Some(delta) = Delta::delta(old, new) is the usual way to skip sending an empty update.

Field types

Every field is diffed according to a field type, chosen with #[delta_struct(field_type = "...")]. The default is "scalar", which can be changed per struct — see Container attributes.

scalar (the default)

The field is compared with != and replaced wholesale. In the delta struct it becomes Option<T>: Some(new_value) when the two differ, [None] when they don't. Requires T: PartialEq.

unordered

The field is treated as a bag of items whose order carries no meaning, so the delta records only which items came and went. One field becomes two: {field}_add and {field}_remove, both Vec<Item>.

use delta_struct::Delta;

#[derive(Delta)]
struct Device {
    #[delta_struct(field_type = "unordered")]
    services: Vec<String>,
}

let old = Device { services: vec!["ssh".to_string(), "http".to_string()] };
let new = Device { services: vec!["http".to_string(), "mqtt".to_string()] };

let delta = Delta::delta(old, new).unwrap();
assert_eq!(delta.services_add, vec!["mqtt".to_string()]);
assert_eq!(delta.services_remove, vec!["ssh".to_string()]);

Duplicates are counted rather than deduplicated: diffing [3, 3] against [3] removes a single 3. Any collection works as long as it satisfies IntoIterator (to compute the delta) plus FromIterator and Extend (to apply one) and its items are PartialEq — [Vec] and HashSet both qualify.

Because matching items are found by linear search, computing a delta over an unordered field costs O(n * m) comparisons. That is fine for the handful-of-elements collections this is aimed at, and something to keep in mind for large ones.

apply_delta removes then appends, so for an ordered collection like [Vec] the result is a permutation of the new value, not necessarily the new value itself. Use ordered where that matters.

ordered

The field is diffed positionally with Myers' algorithm, and the delta is a minimal edit script: a [SeqDelta] holding [Splice]s that each say "at this index, drop this many items and put these in their place". Unlike unordered, one source field stays one delta field.

use delta_struct::{Delta, Splice};

#[derive(Delta)]
struct Playlist {
    #[delta_struct(field_type = "ordered")]
    tracks: Vec<String>,
}

let old = Playlist { tracks: vec!["intro".to_string(), "b".to_string(), "outro".to_string()] };
let new = Playlist { tracks: vec!["intro".to_string(), "x".to_string(), "outro".to_string()] };

let delta = Delta::delta(old, new).unwrap();
assert_eq!(
    delta.tracks.splices,
    vec![Splice { at: 1, remove: 1, insert: vec!["x".to_string()] }],
);

Splice positions index the old sequence and arrive sorted and non-overlapping, so applying one is a single forward pass. Reordering is a real change here where unordered would see none, and applying a delta reproduces the new sequence exactly, position included.

The collection needs IntoIterator and FromIterator, and its items need Hash + Eq — a stricter bar than the PartialEq the other field types ask for, because that is what indexing the sequences for Myers requires. The practical consequence is that a Vec<f64> cannot be an ordered field, though it can be unordered.

Turn on the serde feature to get Serialize and Deserialize on [SeqDelta] and [Splice]; without it, a delta struct containing an ordered field cannot derive them.

delta

The field is itself diffed recursively, which keeps a nested change from resending the whole subtree. Requires the field's type to implement [Delta]; the delta struct holds Option<<T as Delta>::Output>.

use delta_struct::Delta;

#[derive(Delta)]
struct Inner {
    a: i32,
    b: i32,
}

#[derive(Delta)]
struct Outer {
    #[delta_struct(field_type = "delta")]
    inner: Inner,
    name: String,
}

let old = Outer { inner: Inner { a: 1, b: 2 }, name: "x".to_string() };
let new = Outer { inner: Inner { a: 1, b: 3 }, name: "x".to_string() };

let delta = Delta::delta(old, new).unwrap();
let inner_delta = delta.inner.expect("`b` changed");
assert_eq!(inner_delta.a, None);
assert_eq!(inner_delta.b, Some(3));

Container attributes

#[delta_struct(...)] on the struct itself accepts:

  • default = "..." — the field type used for fields without their own field_type. Defaults to "scalar".
  • delta_leader = "..." — tokens to emit immediately above the generated struct. This is how you attach derives, doc comments, or any other attribute to a type you never get to write by hand.
use delta_struct::Delta;

#[derive(Delta)]
#[delta_struct(
    default = "unordered",
    delta_leader = "/// The changes to a `Tags`.\n#[derive(Debug)]"
)]
struct Tags {
    labels: Vec<String>,
    // Opt an individual field back out of the container default.
    #[delta_struct(field_type = "scalar")]
    revision: u32,
}

let old = Tags { labels: vec![], revision: 1 };
let new = Tags { labels: vec!["new".to_string()], revision: 2 };
let delta = Delta::delta(old, new).unwrap();
assert_eq!(format!("{:?}", delta.labels_add), r#"["new"]"#);
assert_eq!(delta.revision, Some(2));

delta_leader also works on individual fields, where it decorates the generated field instead of the generated struct. On an unordered field it is emitted above both the _add and the _remove field, so write it to read sensibly on each.

# use delta_struct::Delta;
#[derive(Delta)]
struct Host {
    #[delta_struct(delta_leader = "/// The new port, if it moved.")]
    port: u16,
}

Working with serde

For everything but ordered fields there is no serde integration to enable; delta_leader is the whole story. Put the derives on the generated struct and it serializes like anything else:

use delta_struct::Delta;

#[derive(Delta)]
#[delta_struct(delta_leader = "#[derive(serde::Serialize, serde::Deserialize)]")]
struct Config {
    host: String,
    port: u16,
}

let old = Config { host: "localhost".to_string(), port: 80 };
let new = Config { host: "localhost".to_string(), port: 8080 };

// Sender: there is no message to send at all when nothing changed.
let payload = Delta::delta(old, new).map(|delta| serde_json::to_string(&delta).unwrap());
assert_eq!(payload.as_deref(), Some(r#"{"host":null,"port":8080}"#));

// Receiver applies it to whatever it already had.
let mut config = Config { host: "localhost".to_string(), port: 80 };
config.apply_delta(serde_json::from_str::<ConfigDelta>(&payload.unwrap()).unwrap());
assert_eq!(config.port, 8080);

Field-level delta_leader carries serde attributes just as well, so skip_serializing_if can keep unchanged fields out of the payload entirely rather than sending them as null:

use delta_struct::Delta;

#[derive(Delta)]
#[delta_struct(delta_leader = "#[derive(serde::Serialize)]")]
struct Config {
    #[delta_struct(delta_leader = "#[serde(skip_serializing_if = \"Option::is_none\")]")]
    host: String,
    #[delta_struct(delta_leader = "#[serde(skip_serializing_if = \"Option::is_none\")]")]
    port: u16,
}

let old = Config { host: "localhost".to_string(), port: 80 };
let new = Config { host: "localhost".to_string(), port: 8080 };

let delta = Delta::delta(old, new).unwrap();
assert_eq!(serde_json::to_string(&delta).unwrap(), r#"{"port":8080}"#);

What gets generated

For struct Foo, deriving [Delta] emits struct FooDelta with the same visibility as Foo and the same generic parameters, carrying over their bounds and where clause as written. All of its fields are pub, and by default it derives nothing at all — reach for delta_leader whenever you need Debug, Clone, or serde on it. (Likewise if your crate sets #![deny(missing_docs)]: the generated struct and its fields need doc comments supplied through delta_leader.)

Tuple structs are supported; their delta fields are named field_0, field_1, and so on, since tuple-struct syntax has nowhere to hang the _add/_remove pairs an unordered field needs.

use delta_struct::Delta;

#[derive(Delta)]
struct Meters(i32);

let delta = Delta::delta(Meters(3), Meters(4)).unwrap();
assert_eq!(delta.field_0, Some(4));

Limitations

  • Structs only. Enums and unions are rejected; there is no obvious delta for a value that changed variant.
  • Every type parameter gets a PartialEq bound on the generated impl, whether or not the field that uses it needs one.
  • A unit struct's delta is always [None], as is that of a struct with no fields — there is nothing that could differ.
  • ordered items need Hash + Eq, so float sequences are out. See that section above.