use serde_json::Value;
const MAX_DEPTH: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Change {
Added { path: String, new: Value },
Removed { path: String, old: Value },
Changed {
path: String,
old: Value,
new: Value,
},
}
impl Change {
pub fn path(&self) -> &str {
match self {
Change::Added { path, .. }
| Change::Removed { path, .. }
| Change::Changed { path, .. } => path,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ValueDiff {
pub changes: Vec<Change>,
pub truncated: usize,
}
impl ValueDiff {
pub fn is_empty(&self) -> bool {
self.changes.is_empty() && self.truncated == 0
}
}
pub fn diff(old: &Value, new: &Value, max_changes: usize) -> ValueDiff {
let mut out = ValueDiff::default();
walk(String::new(), old, new, max_changes, 0, &mut out);
out
}
fn push(out: &mut ValueDiff, max_changes: usize, change: Change) {
if out.changes.len() < max_changes {
out.changes.push(change);
} else {
out.truncated += 1;
}
}
fn join(prefix: &str, key: &str) -> String {
if prefix.is_empty() {
key.to_string()
} else {
format!("{prefix}.{key}")
}
}
fn walk(
path: String,
old: &Value,
new: &Value,
max_changes: usize,
depth: usize,
out: &mut ValueDiff,
) {
if old == new {
return;
}
if depth >= MAX_DEPTH {
push(
out,
max_changes,
Change::Changed {
path,
old: old.clone(),
new: new.clone(),
},
);
return;
}
match (old, new) {
(Value::Object(a), Value::Object(b)) => {
for (k, av) in a {
match b.get(k) {
Some(bv) => walk(join(&path, k), av, bv, max_changes, depth + 1, out),
None => push(
out,
max_changes,
Change::Removed {
path: join(&path, k),
old: av.clone(),
},
),
}
}
for (k, bv) in b {
if !a.contains_key(k) {
push(
out,
max_changes,
Change::Added {
path: join(&path, k),
new: bv.clone(),
},
);
}
}
}
(Value::Array(a), Value::Array(b)) => {
for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
walk(
join(&path, &i.to_string()),
av,
bv,
max_changes,
depth + 1,
out,
);
}
for (i, av) in a.iter().enumerate().skip(b.len()) {
push(
out,
max_changes,
Change::Removed {
path: join(&path, &i.to_string()),
old: av.clone(),
},
);
}
for (i, bv) in b.iter().enumerate().skip(a.len()) {
push(
out,
max_changes,
Change::Added {
path: join(&path, &i.to_string()),
new: bv.clone(),
},
);
}
}
_ => push(
out,
max_changes,
Change::Changed {
path,
old: old.clone(),
new: new.clone(),
},
),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ByteDiff {
pub common_prefix: usize,
pub common_suffix: usize,
pub old_len: usize,
pub new_len: usize,
}
impl ByteDiff {
pub fn is_empty(&self) -> bool {
self.old_len == self.new_len && self.common_prefix == self.old_len
}
pub fn ranges(&self) -> (std::ops::Range<usize>, std::ops::Range<usize>) {
(
self.common_prefix..self.old_len - self.common_suffix,
self.common_prefix..self.new_len - self.common_suffix,
)
}
}
pub fn byte_diff(old: &[u8], new: &[u8]) -> ByteDiff {
let common_prefix = old
.iter()
.zip(new.iter())
.take_while(|(a, b)| a == b)
.count();
let room = old.len().min(new.len()) - common_prefix;
let common_suffix = old
.iter()
.rev()
.zip(new.iter().rev())
.take(room)
.take_while(|(a, b)| a == b)
.count();
ByteDiff {
common_prefix,
common_suffix,
old_len: old.len(),
new_len: new.len(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_changed_scalar_names_its_path_and_both_sides() {
let d = diff(
&json!({"value": 41.0, "unit": "percent"}),
&json!({"value": 42.0, "unit": "percent"}),
32,
);
assert_eq!(
d.changes,
[Change::Changed {
path: "value".into(),
old: json!(41.0),
new: json!(42.0),
}]
);
assert_eq!(d.truncated, 0);
}
#[test]
fn identical_values_produce_nothing() {
let d = diff(
&json!({"a": [1, 2, {"b": true}]}),
&json!({"a": [1, 2, {"b": true}]}),
32,
);
assert!(d.is_empty(), "no change is not the same as not looked");
}
#[test]
fn added_and_removed_fields_are_distinct_from_changed() {
let d = diff(
&json!({"a": 1, "gone": 2}),
&json!({"a": 1, "fresh": 3}),
32,
);
assert!(d.changes.contains(&Change::Removed {
path: "gone".into(),
old: json!(2)
}));
assert!(d.changes.contains(&Change::Added {
path: "fresh".into(),
new: json!(3)
}));
assert_eq!(d.changes.len(), 2);
}
#[test]
fn nesting_produces_dotted_paths() {
let d = diff(
&json!({"disk": {"var-log": {"used": 1}}}),
&json!({"disk": {"var-log": {"used": 2}}}),
32,
);
assert_eq!(d.changes[0].path(), "disk.var-log.used");
}
#[test]
fn arrays_are_compared_by_index() {
let d = diff(&json!({"xs": [1, 2, 3]}), &json!({"xs": [1, 9, 3]}), 32);
assert_eq!(
d.changes,
[Change::Changed {
path: "xs.1".into(),
old: json!(2),
new: json!(9),
}]
);
}
#[test]
fn a_shorter_array_reports_the_tail_as_removed() {
let d = diff(&json!([1, 2, 3]), &json!([1]), 32);
assert_eq!(
d.changes,
[
Change::Removed {
path: "1".into(),
old: json!(2)
},
Change::Removed {
path: "2".into(),
old: json!(3)
},
]
);
}
#[test]
fn a_shape_change_is_a_single_change() {
let d = diff(&json!({"a": {"b": 1}}), &json!({"a": [1]}), 32);
assert_eq!(d.changes.len(), 1);
assert_eq!(d.changes[0].path(), "a");
}
#[test]
fn changes_past_the_bound_are_counted_not_dropped_silently() {
let old = json!({"a": 1, "b": 1, "c": 1, "d": 1, "e": 1});
let new = json!({"a": 2, "b": 2, "c": 2, "d": 2, "e": 2});
let d = diff(&old, &new, 2);
assert_eq!(d.changes.len(), 2);
assert_eq!(d.truncated, 3);
assert!(!d.is_empty());
}
#[test]
fn byte_diff_brackets_the_differing_run() {
let d = byte_diff(b"hello world", b"hello there");
assert_eq!(d.common_prefix, 6);
assert_eq!(d.old_len, 11);
assert_eq!(d.new_len, 11);
let (old, new) = d.ranges();
assert_eq!(&b"hello world"[old], b"world");
assert_eq!(&b"hello there"[new], b"there");
assert!(!d.is_empty());
}
#[test]
fn identical_bytes_are_empty() {
let d = byte_diff(b"same", b"same");
assert!(d.is_empty());
assert_eq!(d.common_prefix, 4);
}
#[test]
fn prefix_and_suffix_never_overlap() {
let d = byte_diff(b"aaaa", b"aaaaaa");
assert_eq!(d.common_prefix, 4);
assert_eq!(d.common_suffix, 0);
assert!(d.common_prefix + d.common_suffix <= d.old_len);
}
}