#![warn(missing_docs)]
extern crate self as delta_struct;
#[cfg(doctest)]
#[doc = include_str!("../../README.md")]
struct ReadmeDoctests;
pub mod bag;
pub mod entry;
pub mod fingerprint;
pub mod index;
pub mod map;
pub mod seq;
pub mod unordered;
pub mod variant;
pub mod version;
pub use bag::BagDelta;
pub use delta_struct_macros::{Delta, Fingerprint};
pub use entry::EntryDelta;
pub use fingerprint::{fingerprint_of, Fingerprint};
pub use index::{TryIndex, TryIndexMut};
pub use map::{KeyedDelta, MapDelta, MapEntry};
pub use seq::{SeqDelta, Splice};
pub use unordered::Unordered;
pub use variant::{EnumDelta, Mismatch};
pub use version::{Applied, Rejected, Versioned, VersionedDelta};
pub trait Delta {
type Output;
fn delta(old: Self, new: Self) -> Option<Self::Output>;
fn apply_delta(&mut self, delta: Self::Output) -> Result<(), Mismatch>;
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "lowercase")
)]
pub enum ScalarDelta<T> {
#[default]
Unchanged,
Changed(T),
}
impl<T> ScalarDelta<T> {
pub fn opt(self) -> Option<T> {
self.into()
}
pub fn is_unchanged(&self) -> bool {
matches!(self, Self::Unchanged)
}
pub fn is_changed(&self) -> bool {
matches!(self, Self::Changed(_))
}
}
impl<T> From<Option<T>> for ScalarDelta<T> {
fn from(value: Option<T>) -> Self {
match value {
Some(v) => Self::Changed(v),
None => Self::Unchanged,
}
}
}
impl<T> From<ScalarDelta<T>> for Option<T> {
fn from(value: ScalarDelta<T>) -> Self {
match value {
ScalarDelta::Changed(v) => Some(v),
ScalarDelta::Unchanged => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
#[derive(Delta)]
#[allow(dead_code)] struct UnitType;
#[derive(Delta, Clone, Debug, PartialEq, Eq)]
#[delta_struct(delta_leader = "#[derive(Clone, Debug, PartialEq, Eq)]")]
struct NewType(i32);
#[derive(Delta)]
#[allow(dead_code)] struct NewTypeWithGeneric<T>(T);
#[derive(Delta)]
#[allow(dead_code)] struct InlineBoundNewType<T: Clone>(T);
#[derive(Delta)]
#[allow(dead_code)] struct WhereClauseNewType<T>(T)
where
T: Clone;
#[derive(Clone, Debug, Delta, PartialEq)]
#[delta_struct(delta_leader = "#[derive(Debug, PartialEq)]")]
struct Reading(
#[delta_struct(field_type = "unordered")] BTreeSet<i32>,
#[delta_struct(field_type = "ordered")] Vec<String>,
#[delta_struct(field_type = "delta")] NewType,
bool,
);
#[test]
fn tuple_struct_delta_keeps_field_positions() {
let old = Reading(
vec![1, 2].into_iter().collect(),
vec!["a".to_string()],
NewType(7),
false,
);
let new = Reading(
vec![2, 3].into_iter().collect(),
vec!["a".to_string(), "b".to_string()],
NewType(8),
true,
);
let delta = Delta::delta(old.clone(), new.clone()).unwrap();
assert_eq!(delta.0.add, vec![3]);
assert_eq!(delta.0.remove, vec![1]);
assert_eq!(
delta.1.splices,
vec![Splice {
at: 1,
remove: 0,
insert: vec!["b".to_string()],
}]
);
assert_eq!(delta.2, Some(NewTypeDelta(ScalarDelta::Changed(8))));
assert_eq!(delta.3, ScalarDelta::Changed(true));
let mut applied = old;
applied.apply_delta(delta).unwrap();
assert_eq!(applied, new);
}
#[cfg(feature = "serde")]
#[test]
fn tuple_struct_delta_serializes_as_a_sequence() {
#[derive(Delta)]
#[delta_struct(delta_leader = "#[derive(serde::Serialize)]")]
struct Meters(i32, i32);
let delta = Delta::delta(Meters(1, 2), Meters(1, 3)).unwrap();
assert_eq!(
serde_json::to_string(&delta).unwrap(),
"[\"unchanged\",{\"changed\":3}]"
);
}
#[derive(Delta)]
struct InlineBoundGeneric<T: Clone> {
foo: T,
bar: bool,
}
#[derive(Delta)]
struct WhereClauseGeneric<T>
where
T: Clone,
{
foo: T,
bar: bool,
}
#[derive(Delta)]
struct InlineBoundDeltaField<T: Delta> {
#[delta_struct(field_type = "delta")]
foo: T,
}
#[derive(Delta)]
struct WhereClauseDeltaField<T>
where
T: Delta,
{
#[delta_struct(field_type = "delta")]
foo: T,
}
#[derive(Delta)]
struct SimpleType {
#[delta_struct(delta_leader = "/// This is foo.")]
foo: i32,
bar: bool,
}
#[derive(Delta)]
#[allow(dead_code)] struct SimpleTypeWithGeneric<T> {
foo: T,
bar: bool,
}
#[derive(Delta)]
struct SimpleCollectionWithGeneric<T: Ord> {
#[delta_struct(
field_type = "unordered",
delta_leader = "/// This the foo type on the delta struct."
)]
foo: BTreeSet<T>,
bar: bool,
}
#[derive(Delta)]
struct DeltaRecursion {
#[delta_struct(field_type = "delta")]
foo: NewType,
bar: bool,
}
#[derive(Delta)]
#[delta_struct(default = "unordered")]
struct AttributeTest {
#[delta_struct(field_type = "scalar")]
foo: i32,
#[delta_struct(field_type = "scalar")]
bar: i32,
baz: BTreeSet<i32>,
}
#[derive(Delta, Clone, Debug, PartialEq, Eq)]
struct AllFieldTypes {
#[delta_struct(field_type = "scalar")]
scalar: i32,
#[delta_struct(field_type = "delta")]
delta: NewType,
#[delta_struct(field_type = "unordered")]
unordered: HashSet<i32>,
}
#[derive(Clone, Debug, Delta, PartialEq)]
#[allow(dead_code)] struct DeviceConfig {
#[delta_struct(field_type = "unordered")]
pub services: HashSet<String>,
#[delta_struct(field_type = "unordered")]
pub settings: HashSet<String>,
pub thumbnail_request: i32,
pub speedtest_request: i32,
#[delta_struct(field_type = "delta")]
pub features: AllFieldTypes,
pub deprovision: bool,
}
#[test]
fn unordered_with_scalar() {
let old = SimpleCollectionWithGeneric {
foo: vec![1, 2, 3].into_iter().collect(),
bar: false,
};
let new = SimpleCollectionWithGeneric {
foo: vec![3, 4, 5].into_iter().collect(),
bar: true,
};
let delta = Delta::delta(old, new).unwrap();
assert_eq!(delta.foo.add, vec![4, 5]);
assert_eq!(delta.foo.remove, vec![1, 2]);
assert_eq!(delta.bar, ScalarDelta::Changed(true));
}
#[test]
fn unordered_apply_round_trips() {
#[derive(Clone, Debug, Delta, PartialEq)]
struct Tags {
#[delta_struct(field_type = "unordered")]
labels: HashSet<i32>,
}
let tags = |labels: &[i32]| Tags {
labels: labels.iter().copied().collect(),
};
let cases: &[(&[i32], &[i32])] = &[
(&[1, 2, 3], &[3, 4, 5]),
(&[1, 2], &[1, 2, 3]),
(&[1, 2, 3], &[1, 2]),
(&[], &[1, 2, 3]),
(&[1, 2, 3], &[]),
(&[1, 2], &[3, 4]),
];
for (old, new) in cases {
let mut applied = tags(old);
let delta = Delta::delta(tags(old), tags(new)).unwrap();
applied.apply_delta(delta).unwrap();
assert_eq!(applied, tags(new), "{:?} -> {:?}", old, new);
}
}
#[test]
fn unordered_apply_ignores_absent_removals() {
let old = AllFieldTypes {
scalar: 1,
delta: NewType(1),
unordered: vec![1, 2].into_iter().collect(),
};
let new = AllFieldTypes {
scalar: 1,
delta: NewType(1),
unordered: vec![2, 3].into_iter().collect(),
};
let mut applied = old.clone();
applied
.apply_delta(Delta::delta(old.clone(), new.clone()).unwrap())
.unwrap();
applied
.apply_delta(Delta::delta(old, new.clone()).unwrap())
.unwrap();
assert_eq!(applied, new);
}
#[cfg(feature = "serde")]
#[test]
fn unordered_delta_serializes_nested() {
#[derive(Delta)]
#[delta_struct(delta_leader = "#[derive(serde::Serialize)]")]
struct Device {
#[delta_struct(field_type = "unordered")]
services: BTreeSet<String>,
}
let device = |services: &[&str]| Device {
services: services.iter().map(|s| s.to_string()).collect(),
};
let delta = Delta::delta(device(&["ssh"]), device(&["mqtt"])).unwrap();
assert_eq!(
serde_json::to_string(&delta).unwrap(),
r#"{"services":{"add":["mqtt"],"remove":["ssh"]}}"#
);
}
#[test]
fn delta_false_positive_check() {
let old = NewType(5);
let new = NewType(5);
let delta = Delta::delta(old, new);
assert!(delta.is_none());
}
#[test]
fn scalar_delta_false_positive_check() {
let old = SimpleType { foo: 5, bar: false };
let new = SimpleType { foo: 5, bar: true };
let delta = Delta::delta(old, new).unwrap();
assert!(delta.foo.is_unchanged());
assert_eq!(delta.bar, ScalarDelta::Changed(true));
}
#[test]
fn delta_field() {
let old = DeltaRecursion {
foo: NewType(5),
bar: false,
};
let new = DeltaRecursion {
foo: NewType(6),
bar: true,
};
let delta = Delta::delta(old, new).unwrap();
assert_eq!(delta.foo, Some(NewTypeDelta(ScalarDelta::Changed(6))));
assert_eq!(delta.bar, ScalarDelta::Changed(true));
}
#[test]
fn default_type_respected() {
let old = AttributeTest {
foo: 5,
bar: 4,
baz: BTreeSet::new(),
};
let new = AttributeTest {
foo: 5,
bar: 4,
baz: vec![9, 4, 5].into_iter().collect(),
};
let delta = Delta::delta(old, new).unwrap();
assert!(delta.foo.is_unchanged());
assert!(delta.bar.is_unchanged());
assert_eq!(delta.baz.add, vec![4, 5, 9]);
assert_eq!(delta.baz.remove, Vec::<i32>::new());
}
#[derive(Clone, Debug, Delta, PartialEq)]
struct Playlist {
#[delta_struct(field_type = "ordered")]
tracks: Vec<String>,
shuffle: bool,
}
#[derive(Delta)]
#[delta_struct(default = "ordered")]
struct OrderedByDefault {
a: Vec<i32>,
b: Vec<i32>,
}
fn playlist(tracks: &[&str], shuffle: bool) -> Playlist {
Playlist {
tracks: tracks.iter().map(|t| t.to_string()).collect(),
shuffle,
}
}
#[test]
fn ordered_records_position() {
let delta = Delta::delta(
playlist(&["a", "b", "c"], false),
playlist(&["a", "x", "c"], false),
)
.unwrap();
assert_eq!(
delta.tracks.splices,
vec![Splice {
at: 1,
remove: 1,
insert: vec!["x".to_string()],
}]
);
assert_eq!(delta.shuffle, ScalarDelta::Unchanged);
}
#[test]
fn ordered_distinguishes_reorder_from_unordered() {
let delta = Delta::delta(playlist(&["a", "b"], false), playlist(&["b", "a"], false));
assert!(delta.is_some());
}
#[test]
fn ordered_false_positive_check() {
let delta = Delta::delta(playlist(&["a", "b"], false), playlist(&["a", "b"], false));
assert!(delta.is_none());
}
#[test]
fn ordered_apply_round_trips() {
let cases: &[(&[&str], &[&str])] = &[
(&["a", "b", "c"], &["a", "x", "c"]),
(&["a", "b"], &["a", "b", "c"]),
(&["b", "c"], &["a", "b", "c"]),
(&["a", "b", "c"], &[]),
(&[], &["a", "b", "c"]),
(&["a", "b", "c", "d", "e"], &["a", "x", "c", "y", "e"]),
(&["a", "a", "a", "b"], &["a", "b", "a", "a"]),
(&["a", "b", "c"], &["c", "b", "a"]),
];
for (old, new) in cases {
let mut applied = playlist(old, true);
let delta = Delta::delta(playlist(old, false), playlist(new, true)).unwrap();
applied.apply_delta(delta).unwrap();
assert_eq!(applied, playlist(new, true), "{:?} -> {:?}", old, new);
}
}
#[cfg(feature = "serde")]
#[test]
fn ordered_delta_serializes() {
let delta =
Delta::delta(playlist(&["a", "b"], false), playlist(&["a", "c"], false)).unwrap();
let json = serde_json::to_string(&delta.tracks).unwrap();
assert_eq!(json, r#"{"splices":[{"at":1,"remove":1,"insert":["c"]}]}"#);
let round_tripped: SeqDelta<String> = serde_json::from_str(&json).unwrap();
let mut target = playlist(&["a", "b"], false);
seq::apply(&mut target.tracks, round_tripped);
assert_eq!(target.tracks, vec!["a".to_string(), "c".to_string()]);
}
#[test]
fn ordered_as_container_default() {
let delta = Delta::delta(
OrderedByDefault {
a: vec![1, 2],
b: vec![3],
},
OrderedByDefault {
a: vec![1, 2],
b: vec![3, 4],
},
)
.unwrap();
assert!(delta.a.is_empty());
assert_eq!(
delta.b.splices,
vec![Splice {
at: 1,
remove: 0,
insert: vec![4],
}]
);
}
#[derive(Clone, Debug, Delta, PartialEq, serde::Serialize)]
#[delta_struct(delta_leader = "#[derive(Debug, serde::Serialize)]")]
struct Service {
port: u16,
healthy: bool,
}
#[derive(Clone, Debug, Delta, PartialEq)]
struct Cluster {
#[delta_struct(field_type = "unordered-delta")]
services: HashMap<String, Service>,
region: String,
}
#[derive(Clone, Debug, Delta, PartialEq)]
#[delta_struct(delta_leader = "#[derive(Clone, Debug)]")]
struct ClusterNoDelta {
#[delta_struct(field_type = "unordered")]
services: HashMap<String, String>,
region: String,
}
#[derive(Delta)]
#[delta_struct(default = "unordered-delta")]
#[allow(dead_code)] struct UnorderedDeltaByDefault {
a: HashMap<u8, NewType>,
b: BTreeMap<u8, NewType>,
}
#[derive(Delta)]
#[allow(dead_code)] struct UnorderedDeltaWithGeneric<K: std::hash::Hash + Eq, V: Delta> {
#[delta_struct(
field_type = "unordered-delta",
delta_leader = "/// One part of the change to `foo`."
)]
foo: HashMap<K, V>,
}
type Services<'a> = &'a [(&'a str, u16, bool)];
fn cluster(services: Services, region: &str) -> Cluster {
Cluster {
services: services
.iter()
.map(|(name, port, healthy)| {
(
name.to_string(),
Service {
port: *port,
healthy: *healthy,
},
)
})
.collect(),
region: region.to_string(),
}
}
#[test]
fn unordered_delta_diffs_values_in_place() {
let delta = Delta::delta(
cluster(&[("web", 80, true), ("db", 5432, true)], "us"),
cluster(&[("web", 8080, true), ("db", 5432, true)], "us"),
)
.unwrap();
assert!(delta.services.add.is_empty());
assert!(delta.services.remove.is_empty());
assert_eq!(delta.services.change.len(), 1);
assert_eq!(delta.services.change[0].key, "web");
assert_eq!(
delta.services.change[0].delta.port,
ScalarDelta::Changed(8080)
);
assert_eq!(
delta.services.change[0].delta.healthy,
ScalarDelta::Unchanged
);
assert_eq!(delta.region, ScalarDelta::Unchanged);
}
#[test]
fn unordered_delta_adds_and_removes_by_key() {
let delta = Delta::delta(
cluster(&[("web", 80, true)], "us"),
cluster(&[("db", 5432, false)], "us"),
)
.unwrap();
assert_eq!(
delta.services.add,
vec![(
"db".to_string(),
Service {
port: 5432,
healthy: false
}
)]
);
assert_eq!(delta.services.remove, vec!["web".to_string()]);
assert!(delta.services.change.is_empty());
}
#[test]
fn unordered_delta_false_positive_check() {
let delta = Delta::delta(
cluster(&[("web", 80, true), ("db", 5432, true)], "us"),
cluster(&[("db", 5432, true), ("web", 80, true)], "us"),
);
assert!(delta.is_none());
}
#[test]
fn unordered_delta_apply_round_trips() {
let cases: &[(Services, Services)] = &[
(&[("web", 80, true)], &[("web", 8080, true)]),
(&[("web", 80, true)], &[("web", 80, true), ("db", 1, false)]),
(&[("web", 80, true), ("db", 1, false)], &[("web", 80, true)]),
(&[("web", 80, true)], &[("db", 1, false)]),
(
&[("web", 80, true), ("db", 1, false), ("gone", 9, true)],
&[("web", 8080, true), ("db", 1, false), ("new", 7, false)],
),
(&[], &[("web", 80, true)]),
(&[("web", 80, true)], &[]),
];
for (old, new) in cases {
let mut applied = cluster(old, "us");
let delta = Delta::delta(cluster(old, "us"), cluster(new, "eu")).unwrap();
applied.apply_delta(delta).unwrap();
assert_eq!(applied, cluster(new, "eu"), "{:?} -> {:?}", old, new);
}
}
#[test]
fn unordered_delta_over_a_btree_map() {
#[derive(Clone, Debug, Delta, PartialEq)]
struct Pairs {
#[delta_struct(field_type = "unordered-delta")]
entries: BTreeMap<u8, NewType>,
}
let pairs = |entries: &[(u8, i32)]| Pairs {
entries: entries.iter().map(|(k, v)| (*k, NewType(*v))).collect(),
};
let mut applied = pairs(&[(1, 10), (2, 20)]);
let delta = Delta::delta(pairs(&[(1, 10), (2, 20)]), pairs(&[(2, 21), (3, 30)])).unwrap();
assert_eq!(delta.entries.add, vec![(3, NewType(30))]);
assert_eq!(delta.entries.remove, vec![1]);
assert_eq!(delta.entries.change.len(), 1);
applied.apply_delta(delta).unwrap();
assert_eq!(applied, pairs(&[(2, 21), (3, 30)]));
}
type Images<'a> = &'a [(&'a str, &'a str)];
fn cluster_no_delta(services: Images, region: &str) -> ClusterNoDelta {
ClusterNoDelta {
services: services
.iter()
.map(|(name, image)| (name.to_string(), image.to_string()))
.collect(),
region: region.to_string(),
}
}
fn sorted<T: Ord>(mut entries: Vec<T>) -> Vec<T> {
entries.sort();
entries
}
#[test]
fn unordered_over_a_map_sends_one_copy_of_a_changed_value() {
let delta = Delta::delta(
cluster_no_delta(&[("web", "nginx:1"), ("db", "pg:14")], "us"),
cluster_no_delta(&[("web", "nginx:2"), ("db", "pg:14")], "us"),
)
.unwrap();
assert_eq!(
sorted(delta.services.add),
vec![("web".to_string(), "nginx:2".to_string())]
);
assert!(delta.services.remove.is_empty());
assert_eq!(delta.region, ScalarDelta::Unchanged);
}
#[test]
fn unordered_over_a_map_removes_by_bare_key() {
let delta = Delta::delta(
cluster_no_delta(&[("web", "nginx:1"), ("db", "pg:14")], "us"),
cluster_no_delta(&[("db", "pg:14")], "us"),
)
.unwrap();
assert!(delta.services.add.is_empty());
assert_eq!(delta.services.remove, vec!["web".to_string()]);
}
#[test]
fn unordered_over_a_map_false_positive_check() {
let delta = Delta::delta(
cluster_no_delta(&[("web", "nginx:1"), ("db", "pg:14")], "us"),
cluster_no_delta(&[("db", "pg:14"), ("web", "nginx:1")], "us"),
);
assert!(delta.is_none());
}
#[test]
fn unordered_over_a_map_apply_round_trips() {
let cases: &[(Images, Images)] = &[
(&[("web", "nginx:1")], &[("web", "nginx:2")]),
(
&[("web", "nginx:1")],
&[("web", "nginx:1"), ("db", "pg:14")],
),
(
&[("web", "nginx:1"), ("db", "pg:14")],
&[("web", "nginx:1")],
),
(&[("web", "nginx:1")], &[("db", "pg:14")]),
(
&[("web", "nginx:1"), ("db", "pg:14"), ("gone", "x:1")],
&[("web", "nginx:2"), ("db", "pg:14"), ("new", "y:1")],
),
(&[], &[("web", "nginx:1")]),
(&[("web", "nginx:1")], &[]),
];
for (old, new) in cases {
let mut applied = cluster_no_delta(old, "us");
let delta =
Delta::delta(cluster_no_delta(old, "us"), cluster_no_delta(new, "eu")).unwrap();
applied.apply_delta(delta).unwrap();
assert_eq!(
applied,
cluster_no_delta(new, "eu"),
"{:?} -> {:?}",
old,
new
);
}
}
#[test]
fn unordered_over_a_map_apply_ignores_absent_removals() {
let delta = Delta::delta(
cluster_no_delta(&[("web", "nginx:1"), ("db", "pg:14")], "us"),
cluster_no_delta(&[("web", "nginx:2")], "us"),
)
.unwrap();
let mut applied = cluster_no_delta(&[("web", "nginx:1"), ("db", "pg:14")], "us");
applied.apply_delta(delta.clone()).unwrap();
applied.apply_delta(delta).unwrap();
assert_eq!(applied, cluster_no_delta(&[("web", "nginx:2")], "us"));
}
#[test]
fn unordered_over_a_btree_map() {
#[derive(Clone, Debug, Delta, PartialEq)]
struct Labels {
#[delta_struct(field_type = "unordered")]
entries: BTreeMap<u8, char>,
}
let labels = |entries: &[(u8, char)]| Labels {
entries: entries.iter().copied().collect(),
};
let mut applied = labels(&[(1, 'a'), (2, 'b')]);
let delta =
Delta::delta(labels(&[(1, 'a'), (2, 'b')]), labels(&[(2, 'c'), (3, 'd')])).unwrap();
assert_eq!(delta.entries.add, vec![(2, 'c'), (3, 'd')]);
assert_eq!(delta.entries.remove, vec![1]);
applied.apply_delta(delta).unwrap();
assert_eq!(applied, labels(&[(2, 'c'), (3, 'd')]));
}
#[test]
fn unordered_over_a_map_with_generics() {
#[derive(Clone, Debug, Delta, PartialEq)]
#[delta_struct(delta_leader = "#[derive(Debug, PartialEq)]")]
struct Tagged<K: std::hash::Hash + Eq, V: PartialEq> {
#[delta_struct(field_type = "unordered")]
tags: HashMap<K, V>,
}
let tagged = |v: u8| Tagged {
tags: vec![("a", v)].into_iter().collect::<HashMap<&str, u8>>(),
};
let delta = Delta::delta(tagged(1), tagged(2)).unwrap();
assert_eq!(
delta.tags,
EntryDelta {
add: vec![("a", 2)],
remove: vec![]
}
);
let mut applied = tagged(1);
applied.apply_delta(delta).unwrap();
assert_eq!(applied, tagged(2));
}
#[cfg(feature = "serde")]
#[test]
fn unordered_over_a_map_serializes() {
#[derive(Delta)]
#[delta_struct(delta_leader = "#[derive(serde::Serialize)]")]
struct Labels {
#[delta_struct(field_type = "unordered")]
entries: BTreeMap<String, String>,
}
let labels = |image: &str| Labels {
entries: vec![("web".to_string(), image.to_string())]
.into_iter()
.collect(),
};
let delta = Delta::delta(labels("nginx:1"), labels("nginx:2")).unwrap();
assert_eq!(
serde_json::to_string(&delta).unwrap(),
r#"{"entries":{"add":[["web","nginx:2"]],"remove":[]}}"#
);
}
#[cfg(feature = "serde")]
#[test]
fn unordered_delta_serializes() {
#[derive(Delta)]
#[delta_struct(delta_leader = "#[derive(serde::Serialize)]")]
struct Fleet {
#[delta_struct(field_type = "unordered-delta")]
services: BTreeMap<String, Service>,
}
let fleet = |port| Fleet {
services: vec![(
"web".to_string(),
Service {
port,
healthy: true,
},
)]
.into_iter()
.collect(),
};
let delta = Delta::delta(fleet(80), fleet(8080)).unwrap();
assert_eq!(
serde_json::to_string(&delta).unwrap(),
r#"{"services":{"add":[],"remove":[],"change":[{"key":"web","delta":{"port":{"changed":8080},"healthy":"unchanged"}}]}}"#
);
}
#[derive(Clone, Debug, Delta, Fingerprint, PartialEq)]
#[delta_struct(delta_leader = "#[derive(Clone, Debug)]")]
struct Tracked {
name: String,
#[delta_struct(field_type = "unordered")]
tags: HashSet<String>,
revision: u32,
}
fn tracked(name: &str, tags: &[&str], revision: u32) -> Tracked {
Tracked {
name: name.to_string(),
tags: tags.iter().map(|t| t.to_string()).collect(),
revision,
}
}
#[test]
fn fingerprint_ignores_set_iteration_order() {
let forwards = tracked("a", &["x", "y", "z"], 1);
let backwards = tracked("a", &["z", "y", "x"], 1);
assert_eq!(fingerprint_of(&forwards), fingerprint_of(&backwards));
assert_ne!(
fingerprint_of(&forwards),
fingerprint_of(&tracked("a", &["x", "y"], 1))
);
assert_ne!(
fingerprint_of(&forwards),
fingerprint_of(&tracked("b", &["x", "y", "z"], 1))
);
}
#[test]
fn fingerprint_derives_on_enums_and_tuple_structs() {
#[derive(Fingerprint)]
enum Shape {
Empty,
Circle(u32),
Rect { w: u32, h: u32 },
}
#[derive(Fingerprint)]
struct Pair(u8, bool);
assert_ne!(
fingerprint_of(&Shape::Empty),
fingerprint_of(&Shape::Circle(0))
);
assert_ne!(
fingerprint_of(&Shape::Circle(1)),
fingerprint_of(&Shape::Rect { w: 1, h: 0 })
);
assert_eq!(
fingerprint_of(&Shape::Rect { w: 2, h: 3 }),
fingerprint_of(&Shape::Rect { w: 2, h: 3 })
);
assert_ne!(
fingerprint_of(&Pair(1, true)),
fingerprint_of(&Pair(1, false))
);
}
#[test]
fn fingerprint_is_stable_across_runs() {
assert_eq!(fingerprint_of(&0u8), 0xaf63bd4c8601b7df);
assert_eq!(fingerprint_of(&true), 0xaf63bc4c8601b62c);
assert_eq!(fingerprint_of(&"delta"), 0x3035df3ae9e50ee6);
}
#[test]
fn versioned_round_trips() {
let mut sender = Versioned::new(tracked("a", &["x"], 1));
let mut receiver = Versioned::new(tracked("a", &["x"], 1));
let message = sender.commit(tracked("a", &["x", "y"], 2)).unwrap();
assert_eq!((message.from, message.to), (0, 1));
assert_eq!(receiver.apply(message), Ok(Applied::Updated));
assert_eq!(receiver.get(), sender.get());
assert_eq!(receiver.version(), sender.version());
}
#[test]
fn versioned_no_change_burns_nothing() {
let mut sender = Versioned::new(tracked("a", &["x"], 1));
assert!(sender.commit(tracked("a", &["x"], 1)).is_none());
assert_eq!(sender.version(), 0);
}
#[test]
fn versioned_ignores_a_replayed_delta() {
let mut sender = Versioned::new(tracked("a", &["x"], 1));
let mut receiver = Versioned::new(tracked("a", &["x"], 1));
let message = sender.commit(tracked("a", &["x", "y"], 2)).unwrap();
assert_eq!(receiver.apply(message.clone()), Ok(Applied::Updated));
assert_eq!(receiver.apply(message), Ok(Applied::Stale));
assert_eq!(receiver.get(), sender.get());
}
#[test]
fn versioned_catches_a_dropped_delta() {
let mut sender = Versioned::new(tracked("a", &["x"], 1));
let mut receiver = Versioned::new(tracked("a", &["x"], 1));
let _lost = sender.commit(tracked("a", &["x", "y"], 2)).unwrap();
let second = sender.commit(tracked("a", &["x", "y"], 3)).unwrap();
assert_eq!(
receiver.apply(second),
Err(Rejected::Gap {
expected: 0,
found: 1
})
);
assert_eq!(receiver.version(), 0);
assert_eq!(receiver.get(), &tracked("a", &["x"], 1));
}
#[test]
fn versioned_catches_drift_from_outside_the_stream() {
let mut sender = Versioned::new(tracked("a", &["x"], 1));
let mut receiver = Versioned::new(tracked("a", &["tampered"], 1));
let message = sender.commit(tracked("a", &["x", "y"], 2)).unwrap();
match receiver.apply(message) {
Err(Rejected::Base { expected, found }) => assert_ne!(expected, found),
other => panic!("expected a base mismatch, got {:?}", other),
}
assert_eq!(receiver.version(), 0);
}
#[test]
fn versioned_resync_recovers() {
let mut sender = Versioned::new(tracked("a", &["x"], 1));
let mut receiver = Versioned::new(tracked("a", &["wrong"], 1));
let message = sender.commit(tracked("a", &["x", "y"], 2)).unwrap();
assert!(receiver.apply(message).is_err());
receiver = sender.clone();
let next = sender.commit(tracked("a", &["x", "y"], 3)).unwrap();
assert_eq!(receiver.apply(next), Ok(Applied::Updated));
assert_eq!(receiver.get(), sender.get());
}
#[test]
fn versioned_catches_a_wrong_result() {
let mut sender = Versioned::new(tracked("a", &["x"], 1));
let mut receiver = Versioned::new(tracked("a", &["x"], 1));
let mut message = sender.commit(tracked("a", &["x"], 2)).unwrap();
message.result ^= 1;
match receiver.apply(message) {
Err(Rejected::Result { expected, found }) => assert_ne!(expected, found),
other => panic!("expected a result mismatch, got {:?}", other),
}
assert_eq!(receiver.version(), 0);
}
#[cfg(feature = "serde")]
#[test]
fn versioned_delta_serializes() {
#[derive(Clone, Delta, Fingerprint)]
#[delta_struct(delta_leader = "#[derive(serde::Serialize, serde::Deserialize)]")]
struct Config {
port: u16,
}
let mut sender = Versioned::new(Config { port: 80 });
let mut receiver = Versioned::new(Config { port: 80 });
let payload =
serde_json::to_string(&sender.commit(Config { port: 8080 }).unwrap()).unwrap();
let message: VersionedDelta<ConfigDelta> = serde_json::from_str(&payload).unwrap();
assert_eq!(receiver.apply(message), Ok(Applied::Updated));
assert_eq!(receiver.get().port, 8080);
}
#[derive(Clone, Debug, Delta, Fingerprint, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(
feature = "serde",
delta_struct(delta_leader = "#[derive(Clone, Debug, PartialEq, serde::Serialize)]")
)]
#[cfg_attr(
not(feature = "serde"),
delta_struct(delta_leader = "#[derive(Clone, Debug, PartialEq)]")
)]
enum Shape {
Empty,
Circle(u32),
Rect {
w: u32,
h: u32,
#[delta_struct(field_type = "unordered")]
tags: BTreeSet<String>,
},
}
fn rect(w: u32, h: u32, tags: &[&str]) -> Shape {
Shape::Rect {
w,
h,
tags: tags.iter().map(|t| t.to_string()).collect(),
}
}
#[test]
fn enum_diffs_within_a_variant() {
let delta = Delta::delta(rect(1, 2, &["a"]), rect(1, 3, &["a", "b"])).unwrap();
match delta {
EnumDelta::Delta(ShapeDelta::Rect { w, h, tags }) => {
assert_eq!(w, ScalarDelta::Unchanged); assert_eq!(h, ScalarDelta::Changed(3));
assert_eq!(tags.add, vec!["b".to_string()]);
assert!(tags.remove.is_empty());
}
other => panic!("expected a same-variant delta, got {:?}", other),
}
}
#[test]
fn enum_replaces_across_variants() {
let delta = Delta::delta(Shape::Circle(1), rect(1, 2, &[])).unwrap();
assert_eq!(delta, EnumDelta::Became(rect(1, 2, &[])));
let delta = Delta::delta(Shape::Empty, Shape::Circle(9)).unwrap();
assert_eq!(delta, EnumDelta::Became(Shape::Circle(9)));
}
#[test]
fn enum_false_positive_check() {
assert!(Delta::delta(Shape::Empty, Shape::Empty).is_none());
assert!(Delta::delta(Shape::Circle(1), Shape::Circle(1)).is_none());
assert!(Delta::delta(rect(1, 2, &["a"]), rect(1, 2, &["a"])).is_none());
}
#[test]
fn enum_apply_round_trips() {
let cases: &[(Shape, Shape)] = &[
(Shape::Circle(1), Shape::Circle(2)),
(rect(1, 2, &["a"]), rect(9, 2, &["b"])),
(Shape::Empty, Shape::Circle(3)),
(Shape::Circle(3), Shape::Empty),
(rect(1, 2, &[]), Shape::Circle(4)),
(Shape::Circle(4), rect(5, 6, &["x", "y"])),
];
for (old, new) in cases {
let mut applied = old.clone();
let delta = Delta::delta(old.clone(), new.clone()).unwrap();
applied.apply_delta(delta).unwrap();
assert_eq!(&applied, new, "{:?} -> {:?}", old, new);
}
}
#[test]
fn enum_apply_reports_the_wrong_variant() {
let delta = Delta::delta(rect(1, 2, &[]), rect(1, 3, &[])).unwrap();
let mut diverged = Shape::Circle(7);
assert_eq!(
diverged.apply_delta(delta),
Err(Mismatch {
type_name: "Shape",
expected: "Rect",
found: "Circle",
})
);
assert_eq!(diverged, Shape::Circle(7));
}
#[test]
fn enum_mismatch_propagates_through_a_struct() {
#[derive(Clone, Debug, Delta, PartialEq)]
struct Canvas {
#[delta_struct(field_type = "delta")]
shape: Shape,
name: String,
}
let canvas = |shape: Shape, name: &str| Canvas {
shape,
name: name.to_string(),
};
let delta =
Delta::delta(canvas(rect(1, 2, &[]), "a"), canvas(rect(1, 3, &[]), "b")).unwrap();
let mut diverged = canvas(Shape::Empty, "a");
assert_eq!(
diverged.apply_delta(delta),
Err(Mismatch {
type_name: "Shape",
expected: "Rect",
found: "Empty",
})
);
}
#[test]
fn enum_of_only_unit_variants() {
#[derive(Clone, Debug, Delta, PartialEq)]
enum Flag {
On,
Off,
}
assert!(Delta::delta(Flag::On, Flag::On).is_none());
let mut applied = Flag::On;
applied
.apply_delta(Delta::delta(Flag::On, Flag::Off).unwrap())
.unwrap();
assert_eq!(applied, Flag::Off);
}
#[test]
fn enum_with_generics() {
#[derive(Clone, Debug, Delta, PartialEq)]
#[delta_struct(delta_leader = "#[derive(Debug, PartialEq)]")]
#[allow(dead_code)] enum Slot<T>
where
T: Clone,
{
Filled(T),
Empty,
}
let delta = Delta::delta(Slot::Filled(1), Slot::Filled(2)).unwrap();
assert_eq!(
delta,
EnumDelta::Delta(SlotDelta::Filled(ScalarDelta::Changed(2)))
);
let mut applied = Slot::Filled(1);
applied.apply_delta(delta).unwrap();
assert_eq!(applied, Slot::Filled(2));
}
#[cfg(feature = "serde")]
#[test]
fn enum_delta_serializes() {
let delta = Delta::delta(Shape::Circle(1), Shape::Circle(2)).unwrap();
assert_eq!(
serde_json::to_string(&delta).unwrap(),
r#"{"Delta":{"Circle":{"changed":2}}}"#
);
let delta = Delta::delta(Shape::Empty, Shape::Circle(2)).unwrap();
assert_eq!(
serde_json::to_string(&delta).unwrap(),
r#"{"Became":{"Circle":2}}"#
);
}
#[test]
fn enum_inside_versioned() {
let mut sender = Versioned::new(rect(1, 2, &["a"]));
let mut receiver = Versioned::new(rect(1, 2, &["a"]));
let message = sender.commit(rect(1, 3, &["a"])).unwrap();
assert_eq!(receiver.apply(message), Ok(Applied::Updated));
assert_eq!(receiver.get(), sender.get());
let mut fresh = Versioned::new(rect(1, 2, &["a"]));
let mut diverged = Versioned::new(Shape::Circle(7));
let message = fresh.commit(rect(1, 4, &["a"])).unwrap();
assert!(matches!(
diverged.apply(message),
Err(Rejected::Base { .. })
));
}
#[test]
fn bounded_generics() {
let delta = Delta::delta(
InlineBoundGeneric { foo: 1, bar: false },
InlineBoundGeneric { foo: 2, bar: false },
)
.unwrap();
assert_eq!(delta.foo, ScalarDelta::Changed(2));
assert_eq!(delta.bar, ScalarDelta::Unchanged);
let delta = Delta::delta(
WhereClauseGeneric { foo: 1, bar: false },
WhereClauseGeneric { foo: 2, bar: false },
)
.unwrap();
assert_eq!(delta.foo, ScalarDelta::Changed(2));
assert_eq!(delta.bar, ScalarDelta::Unchanged);
}
#[test]
fn bounded_generics_with_delta_field() {
let delta = Delta::delta(
InlineBoundDeltaField { foo: NewType(1) },
InlineBoundDeltaField { foo: NewType(2) },
)
.unwrap();
assert_eq!(delta.foo.unwrap().0, ScalarDelta::Changed(2));
let mut applied = WhereClauseDeltaField { foo: NewType(1) };
let delta = Delta::delta(
WhereClauseDeltaField { foo: NewType(1) },
WhereClauseDeltaField { foo: NewType(2) },
)
.unwrap();
applied.apply_delta(delta).unwrap();
assert_eq!(applied.foo, NewType(2));
}
#[test]
fn apply_delta_all_field_types() {
let old = AllFieldTypes {
scalar: 1,
delta: NewType(3),
unordered: vec![1, 2, 3].into_iter().collect(),
};
let new = AllFieldTypes {
scalar: 2,
delta: NewType(4),
unordered: vec![3, 4, 5].into_iter().collect(),
};
let new_clone = new.clone();
let mut old_delta_applied = old.clone();
let delta = Delta::delta(old, new);
old_delta_applied.apply_delta(delta.unwrap()).unwrap();
assert_eq!(new_clone, old_delta_applied);
}
}