use serde_json::Value;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Difference {
pub path: String,
pub before: Option<Value>,
pub after: Option<Value>,
}
fn same_json_type(a: &Value, b: &Value) -> bool {
std::mem::discriminant(a) == std::mem::discriminant(b)
}
fn recurse(a: &Value, b: &Value, differences: &mut Vec<Difference>, path: String) {
if !same_json_type(a, b) {
differences.push(Difference {
path: path.clone(),
before: Some(a.clone()),
after: Some(b.clone()),
});
return;
}
match a {
Value::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => {
if a != b {
differences.push(Difference {
path: path.clone(),
before: Some(a.clone()),
after: Some(b.clone()),
})
}
}
Value::Array(a_values) => {
let b_values = b.as_array().unwrap();
for i in 0..a_values.len().max(b_values.len()) {
let va = a_values.get(i).unwrap_or(&Value::Null);
let vb = b_values.get(i).unwrap_or(&Value::Null);
recurse(va, vb, differences, format!("{}[{}]", path, i));
}
}
Value::Object(map) => {
for (ak, av) in map {
match b.get(ak) {
Some(bv) => {
let full_path = if path.is_empty() {
ak.to_string()
} else {
format!("{}.{}", path, ak)
};
recurse(av, bv, differences, full_path);
}
None => differences.push(Difference {
path: format!("{}", ak),
before: Some(av.clone()),
after: None,
}),
}
}
for (bk, bv) in b.as_object().unwrap() {
if !map.contains_key(bk) {
let full_path = if path.is_empty() {
bk.to_string()
} else {
format!("{}.{}", path, bk)
};
differences.push(Difference {
path: full_path,
before: None,
after: Some(bv.clone()),
});
}
}
}
}
}
pub fn deep_diff(a: &Value, b: &Value) -> Vec<Difference> {
let mut differences = Vec::new();
recurse(a, b, &mut differences, "".to_string());
differences
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Value, json};
#[test]
fn test_no_change() {
let a = json!("Alice");
let result = deep_diff(&a, &a);
assert!(result.is_empty());
}
#[test]
fn test_top_level_change() {
let a = json!("Alice");
let b = json!("Bob");
let result = deep_diff(&a, &b);
assert_eq!(
result,
vec![Difference {
path: "".to_string(),
before: Some(json!("Alice")),
after: Some(json!("Bob")),
}]
);
}
#[test]
fn test_array_number_change() {
let a = json!([1, 2]);
let b = json!([1, 3]);
let result = deep_diff(&a, &b);
assert_eq!(
result,
vec![Difference {
path: "[1]".to_string(),
before: Some(json!(2)),
after: Some(json!(3)),
}]
);
}
#[test]
fn test_array_string_change() {
let a = json!(["Alice", "Bob"]);
let b = json!(["Alice", "Hob"]);
let result = deep_diff(&a, &b);
assert_eq!(
result,
vec![Difference {
path: "[1]".to_string(),
before: Some(json!("Bob")),
after: Some(json!("Hob")),
}]
);
}
#[test]
fn test_array_unequal_length() {
let a = json!([1, 2]);
let b = json!([1]);
let result = deep_diff(&a, &b);
assert_eq!(
result,
vec![Difference {
path: "[1]".to_string(),
before: Some(json!(2)),
after: Some(Value::Null),
}]
);
}
#[test]
fn test_compare_map_same() {
let a = json!({"name": "Bob", "age": 25});
let result = deep_diff(&a, &a);
assert!(result.is_empty());
}
#[test]
fn test_compare_map_different() {
let a = json!({"name": "Bob", "age": 25});
let b = json!({"name": "Bob", "age": 26});
let result = deep_diff(&a, &b);
assert_eq!(
result,
vec![Difference {
path: "age".to_string(),
before: Some(json!(25)),
after: Some(json!(26)),
}]
);
}
#[test]
fn test_deep_nested_object() {
let a = json!({ "person": { "name": { "first": "Alice" } } });
let b = json!({ "person": { "name": { "first": "Bob" } } });
let result = deep_diff(&a, &b);
assert_eq!(
result,
vec![Difference {
path: "person.name.first".to_string(),
before: Some(json!("Alice")),
after: Some(json!("Bob")),
}]
);
}
#[test]
fn test_deep_nested_array() {
let a = json!({ "person": { "name": { "first": [1, 2, 3] } } });
let b = json!({ "person": { "name": { "first": [1, 2, 4] } } });
let result = deep_diff(&a, &b);
assert_eq!(
result,
vec![Difference {
path: "person.name.first[2]".to_string(),
before: Some(json!(3)),
after: Some(json!(4)),
}]
);
}
}