1use serde::Serialize;
11use serde_json::{Map, Value};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "lowercase")]
16pub enum DiffKind {
17 Added,
19 Removed,
21 Changed,
23}
24
25#[derive(Debug, Clone, Serialize)]
27pub struct DiffEntry {
28 pub path: String,
30 pub kind: DiffKind,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub before: Option<Value>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub after: Option<Value>,
38}
39
40pub fn prune_nulls(value: Value) -> Value {
44 match value {
45 Value::Object(map) => {
46 let mut pruned = Map::new();
47 for (key, val) in map {
48 if val.is_null() {
49 continue;
50 }
51 pruned.insert(key, prune_nulls(val));
52 }
53 Value::Object(pruned)
54 }
55 Value::Array(items) => Value::Array(items.into_iter().map(prune_nulls).collect()),
56 other => other,
57 }
58}
59
60pub fn collect_diff_entries(before: &Value, after: &Value) -> Vec<DiffEntry> {
62 let mut output = Vec::new();
63 collect_recursive(before, after, "", &mut output);
64 output.sort_by(|a, b| a.path.cmp(&b.path));
65 output
66}
67
68fn collect_recursive(
69 before: &Value,
70 after: &Value,
71 current_path: &str,
72 output: &mut Vec<DiffEntry>,
73) {
74 if before == after {
75 return;
76 }
77
78 if let (Value::Object(before_map), Value::Object(after_map)) = (before, after) {
79 let mut keys: Vec<&String> = before_map.keys().chain(after_map.keys()).collect();
80 keys.sort();
81 keys.dedup();
82
83 for key in keys {
84 let next_path = if current_path.is_empty() {
85 key.clone()
86 } else {
87 format!("{current_path}.{key}")
88 };
89 match (before_map.get(key), after_map.get(key)) {
90 (None, Some(after_val)) => output.push(DiffEntry {
91 path: next_path,
92 kind: DiffKind::Added,
93 before: None,
94 after: Some(after_val.clone()),
95 }),
96 (Some(before_val), None) => output.push(DiffEntry {
97 path: next_path,
98 kind: DiffKind::Removed,
99 before: Some(before_val.clone()),
100 after: None,
101 }),
102 (Some(before_val), Some(after_val)) => {
103 collect_recursive(before_val, after_val, &next_path, output)
104 }
105 (None, None) => {}
106 }
107 }
108 return;
109 }
110
111 if let (Value::Array(before_arr), Value::Array(after_arr)) = (before, after) {
112 let max_len = before_arr.len().max(after_arr.len());
113 for index in 0..max_len {
114 let next_path = format!("{current_path}[{index}]");
115 match (before_arr.get(index), after_arr.get(index)) {
116 (Some(before_val), Some(after_val)) => {
117 collect_recursive(before_val, after_val, &next_path, output)
118 }
119 (None, Some(after_val)) => output.push(DiffEntry {
120 path: next_path,
121 kind: DiffKind::Added,
122 before: None,
123 after: Some(after_val.clone()),
124 }),
125 (Some(before_val), None) => output.push(DiffEntry {
126 path: next_path,
127 kind: DiffKind::Removed,
128 before: Some(before_val.clone()),
129 after: None,
130 }),
131 (None, None) => {}
132 }
133 }
134 return;
135 }
136
137 output.push(DiffEntry {
138 path: if current_path.is_empty() {
139 "<root>".to_string()
140 } else {
141 current_path.to_string()
142 },
143 kind: DiffKind::Changed,
144 before: Some(before.clone()),
145 after: Some(after.clone()),
146 });
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use serde_json::json;
153
154 #[test]
155 fn identical_values_have_no_diff() {
156 let v = json!({"a": 1, "b": [1, 2]});
157 assert!(collect_diff_entries(&v, &v).is_empty());
158 }
159
160 #[test]
161 fn detects_removed_added_changed_sorted_by_path() {
162 let before = json!({"keep": 1, "gone": {"x": 1}, "moved": "a"});
163 let after = json!({"keep": 1, "added": true, "moved": "b"});
164 let diff = collect_diff_entries(&before, &after);
165 assert_eq!(diff.len(), 3);
167 assert_eq!(diff[0].path, "added");
168 assert_eq!(diff[0].kind, DiffKind::Added);
169 assert_eq!(diff[1].path, "gone");
170 assert_eq!(diff[1].kind, DiffKind::Removed);
171 assert_eq!(diff[1].before, Some(json!({"x": 1})));
172 assert_eq!(diff[2].path, "moved");
173 assert_eq!(diff[2].kind, DiffKind::Changed);
174 assert_eq!(diff[2].before, Some(json!("a")));
175 assert_eq!(diff[2].after, Some(json!("b")));
176 }
177
178 #[test]
179 fn prune_nulls_drops_object_nulls_keeps_empty_objects() {
180 let pruned = prune_nulls(json!({"a": null, "b": {"c": null, "d": 1}, "e": {}}));
181 assert_eq!(pruned, json!({"b": {"d": 1}, "e": {}}));
182 }
183
184 #[test]
185 fn added_entry_omits_before_in_json() {
186 let before = json!({});
187 let after = json!({"x": 1});
188 let diff = collect_diff_entries(&before, &after);
189 let serialized = serde_json::to_string(&diff[0]).unwrap();
190 assert!(!serialized.contains("\"before\""));
191 assert!(serialized.contains("\"after\":1"));
192 assert!(serialized.contains("\"kind\":\"added\""));
193 }
194}