1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::{BTreeMap, HashSet};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DiffResult {
10 pub is_equal: bool,
12 pub changes: Vec<Change>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Change {
19 pub path: String,
21 pub kind: ChangeKind,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub old_value: Option<Value>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub new_value: Option<Value>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub description: Option<String>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum ChangeKind {
38 Added,
39 Removed,
40 Modified,
41}
42
43pub fn diff(old: &Value, new: &Value, identity_key: &str) -> DiffResult {
50 let mut changes = Vec::new();
51 diff_values(old, new, "", identity_key, &mut changes);
52 changes.sort_by(|a, b| a.path.cmp(&b.path));
56
57 DiffResult {
58 is_equal: changes.is_empty(),
59 changes,
60 }
61}
62
63fn diff_values(
64 old: &Value,
65 new: &Value,
66 path: &str,
67 identity_key: &str,
68 changes: &mut Vec<Change>,
69) {
70 match (old, new) {
71 (Value::Object(old_obj), Value::Object(new_obj)) => {
72 diff_objects(old_obj, new_obj, path, identity_key, changes);
73 }
74 (Value::Array(old_arr), Value::Array(new_arr)) => {
75 diff_arrays(old_arr, new_arr, path, identity_key, changes);
76 }
77 _ if old != new => {
78 changes.push(Change {
79 path: if path.is_empty() {
80 ".".to_string()
81 } else {
82 path.to_string()
83 },
84 kind: ChangeKind::Modified,
85 old_value: Some(old.clone()),
86 new_value: Some(new.clone()),
87 description: None,
88 });
89 }
90 _ => {}
91 }
92}
93
94fn diff_objects(
95 old: &serde_json::Map<String, Value>,
96 new: &serde_json::Map<String, Value>,
97 path: &str,
98 identity_key: &str,
99 changes: &mut Vec<Change>,
100) {
101 let old_keys: HashSet<_> = old.keys().collect();
102 let new_keys: HashSet<_> = new.keys().collect();
103
104 for key in old_keys.difference(&new_keys) {
106 let key_path = format_path(path, key);
107 changes.push(Change {
108 path: key_path,
109 kind: ChangeKind::Removed,
110 old_value: old.get(*key).cloned(),
111 new_value: None,
112 description: None,
113 });
114 }
115
116 for key in new_keys.difference(&old_keys) {
118 let key_path = format_path(path, key);
119 changes.push(Change {
120 path: key_path,
121 kind: ChangeKind::Added,
122 old_value: None,
123 new_value: new.get(*key).cloned(),
124 description: None,
125 });
126 }
127
128 for key in old_keys.intersection(&new_keys) {
130 let old_val = old.get(*key).unwrap();
131 let new_val = new.get(*key).unwrap();
132 let key_path = format_path(path, key);
133 diff_values(old_val, new_val, &key_path, identity_key, changes);
134 }
135}
136
137fn diff_arrays(
138 old: &[Value],
139 new: &[Value],
140 path: &str,
141 identity_key: &str,
142 changes: &mut Vec<Change>,
143) {
144 let old_has_keys = old.iter().all(|v| v.get(identity_key).is_some());
146 let new_has_keys = new.iter().all(|v| v.get(identity_key).is_some());
147
148 if old_has_keys && new_has_keys {
149 diff_arrays_by_key(old, new, path, identity_key, changes);
150 } else {
151 diff_arrays_positional(old, new, path, identity_key, changes);
152 }
153}
154
155fn diff_arrays_by_key(
156 old: &[Value],
157 new: &[Value],
158 path: &str,
159 identity_key: &str,
160 changes: &mut Vec<Change>,
161) {
162 let old_map: BTreeMap<&str, &Value> = old
163 .iter()
164 .filter_map(|v| v.get(identity_key).and_then(|k| k.as_str()).map(|k| (k, v)))
165 .collect();
166
167 let new_map: BTreeMap<&str, &Value> = new
168 .iter()
169 .filter_map(|v| v.get(identity_key).and_then(|k| k.as_str()).map(|k| (k, v)))
170 .collect();
171
172 let old_keys: HashSet<_> = old_map.keys().cloned().collect();
173 let new_keys: HashSet<_> = new_map.keys().cloned().collect();
174
175 for key in old_keys.difference(&new_keys) {
177 let item_path = format!("{}[{}]", path, key);
178 changes.push(Change {
179 path: item_path,
180 kind: ChangeKind::Removed,
181 old_value: old_map.get(key).cloned().cloned(),
182 new_value: None,
183 description: None,
184 });
185 }
186
187 for key in new_keys.difference(&old_keys) {
189 let item_path = format!("{}[{}]", path, key);
190 changes.push(Change {
191 path: item_path,
192 kind: ChangeKind::Added,
193 old_value: None,
194 new_value: new_map.get(key).cloned().cloned(),
195 description: None,
196 });
197 }
198
199 for key in old_keys.intersection(&new_keys) {
201 let old_val = old_map.get(key).unwrap();
202 let new_val = new_map.get(key).unwrap();
203 let item_path = format!("{}[{}]", path, key);
204 diff_values(old_val, new_val, &item_path, identity_key, changes);
205 }
206}
207
208fn diff_arrays_positional(
209 old: &[Value],
210 new: &[Value],
211 path: &str,
212 identity_key: &str,
213 changes: &mut Vec<Change>,
214) {
215 let max_len = old.len().max(new.len());
216
217 for i in 0..max_len {
218 let item_path = format!("{}[{}]", path, i);
219
220 match (old.get(i), new.get(i)) {
221 (Some(old_val), Some(new_val)) => {
222 diff_values(old_val, new_val, &item_path, identity_key, changes);
223 }
224 (Some(old_val), None) => {
225 changes.push(Change {
226 path: item_path,
227 kind: ChangeKind::Removed,
228 old_value: Some(old_val.clone()),
229 new_value: None,
230 description: None,
231 });
232 }
233 (None, Some(new_val)) => {
234 changes.push(Change {
235 path: item_path,
236 kind: ChangeKind::Added,
237 old_value: None,
238 new_value: Some(new_val.clone()),
239 description: None,
240 });
241 }
242 (None, None) => unreachable!(),
243 }
244 }
245}
246
247fn format_path(base: &str, key: &str) -> String {
248 if base.is_empty() {
249 key.to_string()
250 } else {
251 format!("{}.{}", base, key)
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use serde_json::json;
259
260 #[test]
261 fn test_equal_values() {
262 let old = json!({"name": "test", "value": 42});
263 let new = json!({"name": "test", "value": 42});
264
265 let result = diff(&old, &new, "name");
266 assert!(result.is_equal);
267 assert!(result.changes.is_empty());
268 }
269
270 #[test]
271 fn test_added_field() {
272 let old = json!({"name": "test"});
273 let new = json!({"name": "test", "value": 42});
274
275 let result = diff(&old, &new, "name");
276 assert!(!result.is_equal);
277 assert_eq!(result.changes.len(), 1);
278 assert_eq!(result.changes[0].kind, ChangeKind::Added);
279 assert_eq!(result.changes[0].path, "value");
280 }
281
282 #[test]
283 fn test_removed_field() {
284 let old = json!({"name": "test", "value": 42});
285 let new = json!({"name": "test"});
286
287 let result = diff(&old, &new, "name");
288 assert!(!result.is_equal);
289 assert_eq!(result.changes.len(), 1);
290 assert_eq!(result.changes[0].kind, ChangeKind::Removed);
291 assert_eq!(result.changes[0].path, "value");
292 }
293
294 #[test]
295 fn test_modified_field() {
296 let old = json!({"name": "test", "value": 42});
297 let new = json!({"name": "test", "value": 100});
298
299 let result = diff(&old, &new, "name");
300 assert!(!result.is_equal);
301 assert_eq!(result.changes.len(), 1);
302 assert_eq!(result.changes[0].kind, ChangeKind::Modified);
303 assert_eq!(result.changes[0].path, "value");
304 }
305
306 #[test]
307 fn test_array_by_key() {
308 let old = json!({
309 "items": [
310 {"name": "a", "value": 1},
311 {"name": "b", "value": 2}
312 ]
313 });
314 let new = json!({
315 "items": [
316 {"name": "b", "value": 2},
317 {"name": "c", "value": 3}
318 ]
319 });
320
321 let result = diff(&old, &new, "name");
322 assert!(!result.is_equal);
323
324 let removed: Vec<_> = result
326 .changes
327 .iter()
328 .filter(|c| c.kind == ChangeKind::Removed)
329 .collect();
330 let added: Vec<_> = result
331 .changes
332 .iter()
333 .filter(|c| c.kind == ChangeKind::Added)
334 .collect();
335
336 assert_eq!(removed.len(), 1);
337 assert!(removed[0].path.contains("[a]"));
338
339 assert_eq!(added.len(), 1);
340 assert!(added[0].path.contains("[c]"));
341 }
342
343 #[test]
344 fn change_order_is_deterministic_and_sorted_by_path() {
345 let old = serde_json::json!({"name": "a", "zeta": 1, "alpha": 1, "mid": 1});
348 let new = serde_json::json!({"name": "a", "zeta": 2, "alpha": 2, "mid": 2});
349 let paths: Vec<String> = diff(&old, &new, "name")
350 .changes
351 .into_iter()
352 .map(|c| c.path)
353 .collect();
354 assert_eq!(paths, vec!["alpha", "mid", "zeta"]);
355 }
356}