1use serde_json::Value;
16use std::fmt;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ChangeType {
21 Added,
22 Removed,
23 Modified,
24}
25
26impl fmt::Display for ChangeType {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 ChangeType::Added => write!(f, "added"),
30 ChangeType::Removed => write!(f, "removed"),
31 ChangeType::Modified => write!(f, "modified"),
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct Change {
39 pub path: String,
40 pub change_type: ChangeType,
41 pub old_value: Option<Value>,
42 pub new_value: Option<Value>,
43}
44
45impl fmt::Display for Change {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self.change_type {
48 ChangeType::Added => {
49 write!(f, "+ {}: {}", self.path, self.new_value.as_ref().unwrap())
50 }
51 ChangeType::Removed => {
52 write!(f, "- {}: {}", self.path, self.old_value.as_ref().unwrap())
53 }
54 ChangeType::Modified => {
55 write!(
56 f,
57 "~ {}: {} -> {}",
58 self.path,
59 self.old_value.as_ref().unwrap(),
60 self.new_value.as_ref().unwrap()
61 )
62 }
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct DiffSummary {
70 pub added: usize,
71 pub removed: usize,
72 pub modified: usize,
73}
74
75pub fn diff(a: &Value, b: &Value) -> Vec<Change> {
80 let mut changes = Vec::new();
81 diff_values(a, b, "", &mut changes);
82 changes
83}
84
85pub fn diff_summary(changes: &[Change]) -> DiffSummary {
87 let mut added = 0;
88 let mut removed = 0;
89 let mut modified = 0;
90
91 for change in changes {
92 match change.change_type {
93 ChangeType::Added => added += 1,
94 ChangeType::Removed => removed += 1,
95 ChangeType::Modified => modified += 1,
96 }
97 }
98
99 DiffSummary {
100 added,
101 removed,
102 modified,
103 }
104}
105
106fn build_path(prefix: &str, key: &str) -> String {
107 if prefix.is_empty() {
108 key.to_string()
109 } else {
110 format!("{}.{}", prefix, key)
111 }
112}
113
114fn build_array_path(prefix: &str, index: usize) -> String {
115 format!("{}[{}]", prefix, index)
116}
117
118fn diff_values(a: &Value, b: &Value, path: &str, changes: &mut Vec<Change>) {
119 match (a, b) {
120 (Value::Object(map_a), Value::Object(map_b)) => {
121 for (key, val_a) in map_a {
123 let child_path = build_path(path, key);
124 match map_b.get(key) {
125 Some(val_b) => diff_values(val_a, val_b, &child_path, changes),
126 None => changes.push(Change {
127 path: child_path,
128 change_type: ChangeType::Removed,
129 old_value: Some(val_a.clone()),
130 new_value: None,
131 }),
132 }
133 }
134 for (key, val_b) in map_b {
136 if !map_a.contains_key(key) {
137 let child_path = build_path(path, key);
138 changes.push(Change {
139 path: child_path,
140 change_type: ChangeType::Added,
141 old_value: None,
142 new_value: Some(val_b.clone()),
143 });
144 }
145 }
146 }
147 (Value::Array(arr_a), Value::Array(arr_b)) => {
148 let max_len = arr_a.len().max(arr_b.len());
149 for i in 0..max_len {
150 let child_path = build_array_path(path, i);
151 match (arr_a.get(i), arr_b.get(i)) {
152 (Some(val_a), Some(val_b)) => {
153 diff_values(val_a, val_b, &child_path, changes);
154 }
155 (Some(val_a), None) => {
156 changes.push(Change {
157 path: child_path,
158 change_type: ChangeType::Removed,
159 old_value: Some(val_a.clone()),
160 new_value: None,
161 });
162 }
163 (None, Some(val_b)) => {
164 changes.push(Change {
165 path: child_path,
166 change_type: ChangeType::Added,
167 old_value: None,
168 new_value: Some(val_b.clone()),
169 });
170 }
171 (None, None) => unreachable!(),
172 }
173 }
174 }
175 _ => {
176 if a != b {
177 changes.push(Change {
178 path: path.to_string(),
179 change_type: ChangeType::Modified,
180 old_value: Some(a.clone()),
181 new_value: Some(b.clone()),
182 });
183 }
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use serde_json::json;
192
193 #[test]
194 fn no_changes() {
195 let a = json!({"name": "Alice", "age": 30});
196 let b = json!({"name": "Alice", "age": 30});
197 let changes = diff(&a, &b);
198 assert!(changes.is_empty());
199 }
200
201 #[test]
202 fn added_key() {
203 let a = json!({"name": "Alice"});
204 let b = json!({"name": "Alice", "age": 30});
205 let changes = diff(&a, &b);
206 assert_eq!(changes.len(), 1);
207 assert_eq!(changes[0].path, "age");
208 assert_eq!(changes[0].change_type, ChangeType::Added);
209 assert_eq!(changes[0].new_value, Some(json!(30)));
210 assert_eq!(changes[0].old_value, None);
211 }
212
213 #[test]
214 fn removed_key() {
215 let a = json!({"name": "Alice", "age": 30});
216 let b = json!({"name": "Alice"});
217 let changes = diff(&a, &b);
218 assert_eq!(changes.len(), 1);
219 assert_eq!(changes[0].path, "age");
220 assert_eq!(changes[0].change_type, ChangeType::Removed);
221 assert_eq!(changes[0].old_value, Some(json!(30)));
222 assert_eq!(changes[0].new_value, None);
223 }
224
225 #[test]
226 fn modified_value() {
227 let a = json!({"name": "Alice"});
228 let b = json!({"name": "Bob"});
229 let changes = diff(&a, &b);
230 assert_eq!(changes.len(), 1);
231 assert_eq!(changes[0].path, "name");
232 assert_eq!(changes[0].change_type, ChangeType::Modified);
233 assert_eq!(changes[0].old_value, Some(json!("Alice")));
234 assert_eq!(changes[0].new_value, Some(json!("Bob")));
235 }
236
237 #[test]
238 fn nested_diff() {
239 let a = json!({"user": {"name": "Alice", "age": 30}});
240 let b = json!({"user": {"name": "Alice", "age": 31}});
241 let changes = diff(&a, &b);
242 assert_eq!(changes.len(), 1);
243 assert_eq!(changes[0].path, "user.age");
244 assert_eq!(changes[0].change_type, ChangeType::Modified);
245 }
246
247 #[test]
248 fn array_diff() {
249 let a = json!({"tags": ["rust", "dev"]});
250 let b = json!({"tags": ["rust", "senior"]});
251 let changes = diff(&a, &b);
252 assert_eq!(changes.len(), 1);
253 assert_eq!(changes[0].path, "tags[1]");
254 assert_eq!(changes[0].change_type, ChangeType::Modified);
255 }
256
257 #[test]
258 fn array_added() {
259 let a = json!({"items": [1, 2]});
260 let b = json!({"items": [1, 2, 3]});
261 let changes = diff(&a, &b);
262 assert_eq!(changes.len(), 1);
263 assert_eq!(changes[0].path, "items[2]");
264 assert_eq!(changes[0].change_type, ChangeType::Added);
265 assert_eq!(changes[0].new_value, Some(json!(3)));
266 }
267
268 #[test]
269 fn array_removed() {
270 let a = json!({"items": [1, 2, 3]});
271 let b = json!({"items": [1, 2]});
272 let changes = diff(&a, &b);
273 assert_eq!(changes.len(), 1);
274 assert_eq!(changes[0].path, "items[2]");
275 assert_eq!(changes[0].change_type, ChangeType::Removed);
276 assert_eq!(changes[0].old_value, Some(json!(3)));
277 }
278
279 #[test]
280 fn type_change() {
281 let a = json!({"value": "hello"});
282 let b = json!({"value": 42});
283 let changes = diff(&a, &b);
284 assert_eq!(changes.len(), 1);
285 assert_eq!(changes[0].path, "value");
286 assert_eq!(changes[0].change_type, ChangeType::Modified);
287 assert_eq!(changes[0].old_value, Some(json!("hello")));
288 assert_eq!(changes[0].new_value, Some(json!(42)));
289 }
290
291 #[test]
292 fn summary() {
293 let a = json!({"a": 1, "b": 2, "c": 3});
294 let b = json!({"a": 1, "b": 5, "d": 4});
295 let changes = diff(&a, &b);
296 let summary = diff_summary(&changes);
297 assert_eq!(summary.added, 1);
298 assert_eq!(summary.removed, 1);
299 assert_eq!(summary.modified, 1);
300 }
301
302 #[test]
303 fn display_change_type() {
304 assert_eq!(format!("{}", ChangeType::Added), "added");
305 assert_eq!(format!("{}", ChangeType::Removed), "removed");
306 assert_eq!(format!("{}", ChangeType::Modified), "modified");
307 }
308
309 #[test]
310 fn display_change() {
311 let added = Change {
312 path: "name".to_string(),
313 change_type: ChangeType::Added,
314 old_value: None,
315 new_value: Some(json!("Alice")),
316 };
317 assert_eq!(format!("{}", added), "+ name: \"Alice\"");
318
319 let removed = Change {
320 path: "age".to_string(),
321 change_type: ChangeType::Removed,
322 old_value: Some(json!(30)),
323 new_value: None,
324 };
325 assert_eq!(format!("{}", removed), "- age: 30");
326
327 let modified = Change {
328 path: "score".to_string(),
329 change_type: ChangeType::Modified,
330 old_value: Some(json!(10)),
331 new_value: Some(json!(20)),
332 };
333 assert_eq!(format!("{}", modified), "~ score: 10 -> 20");
334 }
335
336 #[test]
337 fn complex_nested() {
338 let a = json!({
339 "users": [
340 {"name": "Alice", "roles": ["admin"]},
341 {"name": "Bob", "roles": ["user"]}
342 ],
343 "config": {
344 "debug": false,
345 "version": "1.0"
346 }
347 });
348 let b = json!({
349 "users": [
350 {"name": "Alice", "roles": ["admin", "super"]},
351 {"name": "Charlie", "roles": ["user"]}
352 ],
353 "config": {
354 "debug": true,
355 "version": "1.0",
356 "env": "prod"
357 }
358 });
359 let changes = diff(&a, &b);
360 let summary = diff_summary(&changes);
361
362 assert!(summary.added >= 2);
364 assert!(summary.modified >= 2);
365
366 let paths: Vec<&str> = changes.iter().map(|c| c.path.as_str()).collect();
367 assert!(paths.contains(&"users[0].roles[1]"));
368 assert!(paths.contains(&"users[1].name"));
369 assert!(paths.contains(&"config.debug"));
370 assert!(paths.contains(&"config.env"));
371 }
372}