1use serde_json::{Map, Value};
6
7pub fn compute(previous: &Value, current: &Value) -> Value {
10 let mut out = Map::new();
11 walk("", previous, current, &mut out);
12 Value::Object(out)
13}
14
15pub fn changed(previous: &Value, current: &Value) -> bool {
17 let mut out = Map::new();
18 walk("", previous, current, &mut out);
19 !out.is_empty()
20}
21
22fn record(path: &str, previous: Value, current: Value, out: &mut Map<String, Value>) {
23 let mut entry = Map::new();
24 entry.insert("previous".into(), previous);
25 entry.insert("current".into(), current);
26 out.insert(path.to_string(), Value::Object(entry));
27}
28
29fn walk(path: &str, prev: &Value, cur: &Value, out: &mut Map<String, Value>) {
30 match (prev, cur) {
31 (Value::Object(pm), Value::Object(cm)) => {
32 let mut keys: Vec<&String> = pm.keys().chain(cm.keys()).collect();
34 keys.sort();
35 keys.dedup();
36 for k in keys {
37 let child = if path.is_empty() {
38 k.to_string()
39 } else {
40 format!("{path}.{k}")
41 };
42 match (pm.get(k), cm.get(k)) {
43 (Some(pv), Some(cv)) => walk(&child, pv, cv, out),
44 (Some(pv), None) => record(&child, pv.clone(), Value::Null, out),
45 (None, Some(cv)) => record(&child, Value::Null, cv.clone(), out),
46 (None, None) => {}
47 }
48 }
49 }
50 (Value::Array(pa), Value::Array(ca)) => {
51 let max = pa.len().max(ca.len());
52 for i in 0..max {
53 let child = format!("{path}[{i}]");
54 match (pa.get(i), ca.get(i)) {
55 (Some(pv), Some(cv)) => walk(&child, pv, cv, out),
56 (Some(pv), None) => record(&child, pv.clone(), Value::Null, out),
57 (None, Some(cv)) => record(&child, Value::Null, cv.clone(), out),
58 (None, None) => {}
59 }
60 }
61 }
62 _ => {
63 if prev != cur {
64 record(path, prev.clone(), cur.clone(), out);
65 }
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use serde_json::json;
74
75 #[test]
76 fn no_change_is_empty() {
77 let a = json!({"plans": [{"price": "$19"}]});
78 assert!(!changed(&a, &a));
79 assert_eq!(compute(&a, &a), json!({}));
80 }
81
82 #[test]
83 fn leaf_change_keyed_by_path() {
84 let a = json!({"plans": [{"price": "$19"}, {"price": "$49"}]});
85 let b = json!({"plans": [{"price": "$24"}, {"price": "$49"}]});
86 let d = compute(&a, &b);
87 assert_eq!(
88 d["plans[0].price"],
89 json!({"previous": "$19", "current": "$24"})
90 );
91 assert!(d.get("plans[1].price").is_none());
92 }
93
94 #[test]
95 fn added_and_removed_fields() {
96 let a = json!({"a": 1});
97 let b = json!({"b": 2});
98 let d = compute(&a, &b);
99 assert_eq!(d["a"], json!({"previous": 1, "current": null}));
100 assert_eq!(d["b"], json!({"previous": null, "current": 2}));
101 }
102}