1use serde_json::Value;
7
8pub fn generate_diff(before: &Value, after: &Value) -> Option<String> {
13 match (before, after) {
14 (Value::Object(before_obj), Value::Object(after_obj)) => {
15 let mut changes = Vec::new();
16
17 for (key, before_val) in before_obj {
19 if let Some(after_val) = after_obj.get(key) {
20 if before_val != after_val {
21 changes.push(format!(
22 "{}: {} -> {}",
23 key,
24 format_value(before_val),
25 format_value(after_val)
26 ));
27 }
28 } else {
29 changes.push(format!(
30 "{}: {} -> (removed)",
31 key,
32 format_value(before_val)
33 ));
34 }
35 }
36
37 for (key, after_val) in after_obj {
39 if !before_obj.contains_key(key) {
40 changes.push(format!("{}: (added) -> {}", key, format_value(after_val)));
41 }
42 }
43
44 if changes.is_empty() {
45 None
46 } else {
47 Some(changes.join(", "))
48 }
49 }
50 _ => {
51 if before != after {
53 Some(format!(
54 "{} -> {}",
55 format_value(before),
56 format_value(after)
57 ))
58 } else {
59 None
60 }
61 }
62 }
63}
64
65fn format_value(value: &Value) -> String {
67 match value {
68 Value::Null => "null".to_string(),
69 Value::Bool(b) => b.to_string(),
70 Value::Number(n) => n.to_string(),
71 Value::String(s) => {
72 if s.len() > 50 {
74 format!("\"{}...\"", &s[..47])
75 } else {
76 format!("\"{}\"", s)
77 }
78 }
79 Value::Array(arr) => format!("[{} items]", arr.len()),
80 Value::Object(obj) => format!("{{{} fields}}", obj.len()),
81 }
82}
83
84pub fn generate_detailed_diff(before: &Value, after: &Value, prefix: &str) -> Vec<String> {
88 let mut changes = Vec::new();
89
90 match (before, after) {
91 (Value::Object(before_obj), Value::Object(after_obj)) => {
92 for (key, before_val) in before_obj {
94 let field_prefix = if prefix.is_empty() {
95 key.clone()
96 } else {
97 format!("{}.{}", prefix, key)
98 };
99
100 if let Some(after_val) = after_obj.get(key) {
101 if before_val != after_val {
102 if before_val.is_object() && after_val.is_object() {
104 changes.extend(generate_detailed_diff(
105 before_val,
106 after_val,
107 &field_prefix,
108 ));
109 } else {
110 changes.push(format!(
111 "{}: {} -> {}",
112 field_prefix,
113 format_value(before_val),
114 format_value(after_val)
115 ));
116 }
117 }
118 } else {
119 changes.push(format!(
120 "{}: {} -> (removed)",
121 field_prefix,
122 format_value(before_val)
123 ));
124 }
125 }
126
127 for (key, after_val) in after_obj {
129 if !before_obj.contains_key(key) {
130 let field_prefix = if prefix.is_empty() {
131 key.clone()
132 } else {
133 format!("{}.{}", prefix, key)
134 };
135 changes.push(format!(
136 "{}: (added) -> {}",
137 field_prefix,
138 format_value(after_val)
139 ));
140 }
141 }
142 }
143 (Value::Array(before_arr), Value::Array(after_arr)) => {
144 if before_arr.len() != after_arr.len() {
145 changes.push(format!(
146 "{}: [{} items] -> [{} items]",
147 prefix,
148 before_arr.len(),
149 after_arr.len()
150 ));
151 } else {
152 for (i, (b, a)) in before_arr.iter().zip(after_arr.iter()).enumerate() {
153 if b != a {
154 let item_prefix = format!("{}[{}]", prefix, i);
155 changes.extend(generate_detailed_diff(b, a, &item_prefix));
156 }
157 }
158 }
159 }
160 _ => {
161 if before != after {
162 changes.push(format!(
163 "{}: {} -> {}",
164 prefix,
165 format_value(before),
166 format_value(after)
167 ));
168 }
169 }
170 }
171
172 changes
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use serde_json::json;
179
180 #[test]
181 fn test_simple_field_change() {
182 let before = json!({"name": "Checking", "balance": 1000});
183 let after = json!({"name": "Checking", "balance": 1500});
184
185 let diff = generate_diff(&before, &after).unwrap();
186 assert!(diff.contains("balance: 1000 -> 1500"));
187 assert!(!diff.contains("name")); }
189
190 #[test]
191 fn test_string_field_change() {
192 let before = json!({"name": "Old Name"});
193 let after = json!({"name": "New Name"});
194
195 let diff = generate_diff(&before, &after).unwrap();
196 assert!(diff.contains("name: \"Old Name\" -> \"New Name\""));
197 }
198
199 #[test]
200 fn test_field_added() {
201 let before = json!({"name": "Test"});
202 let after = json!({"name": "Test", "balance": 100});
203
204 let diff = generate_diff(&before, &after).unwrap();
205 assert!(diff.contains("balance: (added) -> 100"));
206 }
207
208 #[test]
209 fn test_field_removed() {
210 let before = json!({"name": "Test", "old_field": "value"});
211 let after = json!({"name": "Test"});
212
213 let diff = generate_diff(&before, &after).unwrap();
214 assert!(diff.contains("old_field: \"value\" -> (removed)"));
215 }
216
217 #[test]
218 fn test_no_changes() {
219 let before = json!({"name": "Test", "value": 100});
220 let after = json!({"name": "Test", "value": 100});
221
222 let diff = generate_diff(&before, &after);
223 assert!(diff.is_none());
224 }
225
226 #[test]
227 fn test_multiple_changes() {
228 let before = json!({"a": 1, "b": 2, "c": 3});
229 let after = json!({"a": 10, "b": 2, "c": 30});
230
231 let diff = generate_diff(&before, &after).unwrap();
232 assert!(diff.contains("a: 1 -> 10"));
233 assert!(diff.contains("c: 3 -> 30"));
234 assert!(!diff.contains("b:")); }
236
237 #[test]
238 fn test_bool_change() {
239 let before = json!({"active": true});
240 let after = json!({"active": false});
241
242 let diff = generate_diff(&before, &after).unwrap();
243 assert!(diff.contains("active: true -> false"));
244 }
245
246 #[test]
247 fn test_null_handling() {
248 let before = json!({"value": null});
249 let after = json!({"value": 100});
250
251 let diff = generate_diff(&before, &after).unwrap();
252 assert!(diff.contains("value: null -> 100"));
253 }
254
255 #[test]
256 fn test_array_change_summary() {
257 let before = json!({"items": [1, 2, 3]});
258 let after = json!({"items": [1, 2, 3, 4, 5]});
259
260 let diff = generate_diff(&before, &after).unwrap();
261 assert!(diff.contains("items: [3 items] -> [5 items]"));
262 }
263
264 #[test]
265 fn test_detailed_diff_nested() {
266 let before = json!({"account": {"name": "Old", "balance": 100}});
267 let after = json!({"account": {"name": "New", "balance": 100}});
268
269 let changes = generate_detailed_diff(&before, &after, "");
270 assert!(changes.iter().any(|c| c.contains("account.name")));
271 }
272
273 #[test]
274 fn test_long_string_truncation() {
275 let long_string = "a".repeat(100);
276 let before = json!({"memo": long_string});
277 let after = json!({"memo": "short"});
278
279 let diff = generate_diff(&before, &after).unwrap();
280 assert!(diff.contains("...\""));
281 }
282
283 #[test]
284 fn test_format_value() {
285 assert_eq!(format_value(&json!(null)), "null");
286 assert_eq!(format_value(&json!(true)), "true");
287 assert_eq!(format_value(&json!(42)), "42");
288 assert_eq!(format_value(&json!("test")), "\"test\"");
289 assert_eq!(format_value(&json!([1, 2, 3])), "[3 items]");
290 assert_eq!(format_value(&json!({"a": 1, "b": 2})), "{2 fields}");
291 }
292}