1use serde_json::Value;
22
23pub use crate::report::{ByteDiff, Change, ValueDiff};
27
28const MAX_DEPTH: usize = 32;
35
36pub fn diff(old: &Value, new: &Value, max_changes: usize) -> ValueDiff {
39 let mut out = ValueDiff::default();
40 walk(String::new(), old, new, max_changes, 0, &mut out);
41 out
42}
43
44fn push(out: &mut ValueDiff, max_changes: usize, change: Change) {
46 if out.changes.len() < max_changes {
47 out.changes.push(change);
48 } else {
49 out.truncated += 1;
50 }
51}
52
53fn join(prefix: &str, key: &str) -> String {
54 if prefix.is_empty() {
55 key.to_string()
56 } else {
57 format!("{prefix}.{key}")
58 }
59}
60
61fn walk(
62 path: String,
63 old: &Value,
64 new: &Value,
65 max_changes: usize,
66 depth: usize,
67 out: &mut ValueDiff,
68) {
69 if old == new {
70 return;
71 }
72 if depth >= MAX_DEPTH {
73 push(
74 out,
75 max_changes,
76 Change::Changed {
77 path,
78 old: old.clone(),
79 new: new.clone(),
80 },
81 );
82 return;
83 }
84 match (old, new) {
85 (Value::Object(a), Value::Object(b)) => {
86 for (k, av) in a {
87 match b.get(k) {
88 Some(bv) => walk(join(&path, k), av, bv, max_changes, depth + 1, out),
89 None => push(
90 out,
91 max_changes,
92 Change::Removed {
93 path: join(&path, k),
94 old: av.clone(),
95 },
96 ),
97 }
98 }
99 for (k, bv) in b {
100 if !a.contains_key(k) {
101 push(
102 out,
103 max_changes,
104 Change::Added {
105 path: join(&path, k),
106 new: bv.clone(),
107 },
108 );
109 }
110 }
111 }
112 (Value::Array(a), Value::Array(b)) => {
116 for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
117 walk(
118 join(&path, &i.to_string()),
119 av,
120 bv,
121 max_changes,
122 depth + 1,
123 out,
124 );
125 }
126 for (i, av) in a.iter().enumerate().skip(b.len()) {
127 push(
128 out,
129 max_changes,
130 Change::Removed {
131 path: join(&path, &i.to_string()),
132 old: av.clone(),
133 },
134 );
135 }
136 for (i, bv) in b.iter().enumerate().skip(a.len()) {
137 push(
138 out,
139 max_changes,
140 Change::Added {
141 path: join(&path, &i.to_string()),
142 new: bv.clone(),
143 },
144 );
145 }
146 }
147 _ => push(
150 out,
151 max_changes,
152 Change::Changed {
153 path,
154 old: old.clone(),
155 new: new.clone(),
156 },
157 ),
158 }
159}
160
161pub fn byte_diff(old: &[u8], new: &[u8]) -> ByteDiff {
163 let common_prefix = old
164 .iter()
165 .zip(new.iter())
166 .take_while(|(a, b)| a == b)
167 .count();
168 let room = old.len().min(new.len()) - common_prefix;
171 let common_suffix = old
172 .iter()
173 .rev()
174 .zip(new.iter().rev())
175 .take(room)
176 .take_while(|(a, b)| a == b)
177 .count();
178 ByteDiff {
179 common_prefix,
180 common_suffix,
181 old_len: old.len(),
182 new_len: new.len(),
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use serde_json::json;
190
191 #[test]
192 fn a_changed_scalar_names_its_path_and_both_sides() {
193 let d = diff(
194 &json!({"value": 41.0, "unit": "percent"}),
195 &json!({"value": 42.0, "unit": "percent"}),
196 32,
197 );
198 assert_eq!(
199 d.changes,
200 [Change::Changed {
201 path: "value".into(),
202 old: json!(41.0),
203 new: json!(42.0),
204 }]
205 );
206 assert_eq!(d.truncated, 0);
207 }
208
209 #[test]
210 fn identical_values_produce_nothing() {
211 let d = diff(
212 &json!({"a": [1, 2, {"b": true}]}),
213 &json!({"a": [1, 2, {"b": true}]}),
214 32,
215 );
216 assert!(d.is_empty(), "no change is not the same as not looked");
217 }
218
219 #[test]
220 fn added_and_removed_fields_are_distinct_from_changed() {
221 let d = diff(
222 &json!({"a": 1, "gone": 2}),
223 &json!({"a": 1, "fresh": 3}),
224 32,
225 );
226 assert!(d.changes.contains(&Change::Removed {
227 path: "gone".into(),
228 old: json!(2)
229 }));
230 assert!(d.changes.contains(&Change::Added {
231 path: "fresh".into(),
232 new: json!(3)
233 }));
234 assert_eq!(d.changes.len(), 2);
235 }
236
237 #[test]
238 fn nesting_produces_dotted_paths() {
239 let d = diff(
240 &json!({"disk": {"var-log": {"used": 1}}}),
241 &json!({"disk": {"var-log": {"used": 2}}}),
242 32,
243 );
244 assert_eq!(d.changes[0].path(), "disk.var-log.used");
245 }
246
247 #[test]
250 fn arrays_are_compared_by_index() {
251 let d = diff(&json!({"xs": [1, 2, 3]}), &json!({"xs": [1, 9, 3]}), 32);
252 assert_eq!(
253 d.changes,
254 [Change::Changed {
255 path: "xs.1".into(),
256 old: json!(2),
257 new: json!(9),
258 }]
259 );
260 }
261
262 #[test]
263 fn a_shorter_array_reports_the_tail_as_removed() {
264 let d = diff(&json!([1, 2, 3]), &json!([1]), 32);
265 assert_eq!(
266 d.changes,
267 [
268 Change::Removed {
269 path: "1".into(),
270 old: json!(2)
271 },
272 Change::Removed {
273 path: "2".into(),
274 old: json!(3)
275 },
276 ]
277 );
278 }
279
280 #[test]
283 fn a_shape_change_is_a_single_change() {
284 let d = diff(&json!({"a": {"b": 1}}), &json!({"a": [1]}), 32);
285 assert_eq!(d.changes.len(), 1);
286 assert_eq!(d.changes[0].path(), "a");
287 }
288
289 #[test]
292 fn changes_past_the_bound_are_counted_not_dropped_silently() {
293 let old = json!({"a": 1, "b": 1, "c": 1, "d": 1, "e": 1});
294 let new = json!({"a": 2, "b": 2, "c": 2, "d": 2, "e": 2});
295 let d = diff(&old, &new, 2);
296 assert_eq!(d.changes.len(), 2);
297 assert_eq!(d.truncated, 3);
298 assert!(!d.is_empty());
299 }
300
301 #[test]
302 fn byte_diff_brackets_the_differing_run() {
303 let d = byte_diff(b"hello world", b"hello there");
304 assert_eq!(d.common_prefix, 6);
305 assert_eq!(d.old_len, 11);
306 assert_eq!(d.new_len, 11);
307 let (old, new) = d.ranges();
308 assert_eq!(&b"hello world"[old], b"world");
309 assert_eq!(&b"hello there"[new], b"there");
310 assert!(!d.is_empty());
311 }
312
313 #[test]
314 fn identical_bytes_are_empty() {
315 let d = byte_diff(b"same", b"same");
316 assert!(d.is_empty());
317 assert_eq!(d.common_prefix, 4);
318 }
319
320 #[test]
323 fn prefix_and_suffix_never_overlap() {
324 let d = byte_diff(b"aaaa", b"aaaaaa");
325 assert_eq!(d.common_prefix, 4);
326 assert_eq!(d.common_suffix, 0);
327 assert!(d.common_prefix + d.common_suffix <= d.old_len);
328 }
329}