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