1use serde_json::Value;
22
23const MAX_DEPTH: usize = 32;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Change {
35 Added { path: String, new: Value },
37 Removed { path: String, old: Value },
39 Changed {
41 path: String,
42 old: Value,
43 new: Value,
44 },
45}
46
47impl Change {
48 pub fn path(&self) -> &str {
49 match self {
50 Change::Added { path, .. }
51 | Change::Removed { path, .. }
52 | Change::Changed { path, .. } => path,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Default, PartialEq, Eq)]
59pub struct ValueDiff {
60 pub changes: Vec<Change>,
61 pub truncated: usize,
67}
68
69impl ValueDiff {
70 pub fn is_empty(&self) -> bool {
72 self.changes.is_empty() && self.truncated == 0
73 }
74}
75
76pub fn diff(old: &Value, new: &Value, max_changes: usize) -> ValueDiff {
79 let mut out = ValueDiff::default();
80 walk(String::new(), old, new, max_changes, 0, &mut out);
81 out
82}
83
84fn push(out: &mut ValueDiff, max_changes: usize, change: Change) {
86 if out.changes.len() < max_changes {
87 out.changes.push(change);
88 } else {
89 out.truncated += 1;
90 }
91}
92
93fn join(prefix: &str, key: &str) -> String {
94 if prefix.is_empty() {
95 key.to_string()
96 } else {
97 format!("{prefix}.{key}")
98 }
99}
100
101fn walk(
102 path: String,
103 old: &Value,
104 new: &Value,
105 max_changes: usize,
106 depth: usize,
107 out: &mut ValueDiff,
108) {
109 if old == new {
110 return;
111 }
112 if depth >= MAX_DEPTH {
113 push(
114 out,
115 max_changes,
116 Change::Changed {
117 path,
118 old: old.clone(),
119 new: new.clone(),
120 },
121 );
122 return;
123 }
124 match (old, new) {
125 (Value::Object(a), Value::Object(b)) => {
126 for (k, av) in a {
127 match b.get(k) {
128 Some(bv) => walk(join(&path, k), av, bv, max_changes, depth + 1, out),
129 None => push(
130 out,
131 max_changes,
132 Change::Removed {
133 path: join(&path, k),
134 old: av.clone(),
135 },
136 ),
137 }
138 }
139 for (k, bv) in b {
140 if !a.contains_key(k) {
141 push(
142 out,
143 max_changes,
144 Change::Added {
145 path: join(&path, k),
146 new: bv.clone(),
147 },
148 );
149 }
150 }
151 }
152 (Value::Array(a), Value::Array(b)) => {
156 for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
157 walk(
158 join(&path, &i.to_string()),
159 av,
160 bv,
161 max_changes,
162 depth + 1,
163 out,
164 );
165 }
166 for (i, av) in a.iter().enumerate().skip(b.len()) {
167 push(
168 out,
169 max_changes,
170 Change::Removed {
171 path: join(&path, &i.to_string()),
172 old: av.clone(),
173 },
174 );
175 }
176 for (i, bv) in b.iter().enumerate().skip(a.len()) {
177 push(
178 out,
179 max_changes,
180 Change::Added {
181 path: join(&path, &i.to_string()),
182 new: bv.clone(),
183 },
184 );
185 }
186 }
187 _ => push(
190 out,
191 max_changes,
192 Change::Changed {
193 path,
194 old: old.clone(),
195 new: new.clone(),
196 },
197 ),
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
203pub struct ByteDiff {
204 pub common_prefix: usize,
205 pub common_suffix: usize,
206 pub old_len: usize,
207 pub new_len: usize,
208}
209
210impl ByteDiff {
211 pub fn is_empty(&self) -> bool {
213 self.old_len == self.new_len && self.common_prefix == self.old_len
214 }
215
216 pub fn ranges(&self) -> (std::ops::Range<usize>, std::ops::Range<usize>) {
220 (
221 self.common_prefix..self.old_len - self.common_suffix,
222 self.common_prefix..self.new_len - self.common_suffix,
223 )
224 }
225}
226
227pub fn byte_diff(old: &[u8], new: &[u8]) -> ByteDiff {
229 let common_prefix = old
230 .iter()
231 .zip(new.iter())
232 .take_while(|(a, b)| a == b)
233 .count();
234 let room = old.len().min(new.len()) - common_prefix;
237 let common_suffix = old
238 .iter()
239 .rev()
240 .zip(new.iter().rev())
241 .take(room)
242 .take_while(|(a, b)| a == b)
243 .count();
244 ByteDiff {
245 common_prefix,
246 common_suffix,
247 old_len: old.len(),
248 new_len: new.len(),
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use serde_json::json;
256
257 #[test]
258 fn a_changed_scalar_names_its_path_and_both_sides() {
259 let d = diff(
260 &json!({"value": 41.0, "unit": "percent"}),
261 &json!({"value": 42.0, "unit": "percent"}),
262 32,
263 );
264 assert_eq!(
265 d.changes,
266 [Change::Changed {
267 path: "value".into(),
268 old: json!(41.0),
269 new: json!(42.0),
270 }]
271 );
272 assert_eq!(d.truncated, 0);
273 }
274
275 #[test]
276 fn identical_values_produce_nothing() {
277 let d = diff(
278 &json!({"a": [1, 2, {"b": true}]}),
279 &json!({"a": [1, 2, {"b": true}]}),
280 32,
281 );
282 assert!(d.is_empty(), "no change is not the same as not looked");
283 }
284
285 #[test]
286 fn added_and_removed_fields_are_distinct_from_changed() {
287 let d = diff(
288 &json!({"a": 1, "gone": 2}),
289 &json!({"a": 1, "fresh": 3}),
290 32,
291 );
292 assert!(d.changes.contains(&Change::Removed {
293 path: "gone".into(),
294 old: json!(2)
295 }));
296 assert!(d.changes.contains(&Change::Added {
297 path: "fresh".into(),
298 new: json!(3)
299 }));
300 assert_eq!(d.changes.len(), 2);
301 }
302
303 #[test]
304 fn nesting_produces_dotted_paths() {
305 let d = diff(
306 &json!({"disk": {"var-log": {"used": 1}}}),
307 &json!({"disk": {"var-log": {"used": 2}}}),
308 32,
309 );
310 assert_eq!(d.changes[0].path(), "disk.var-log.used");
311 }
312
313 #[test]
316 fn arrays_are_compared_by_index() {
317 let d = diff(&json!({"xs": [1, 2, 3]}), &json!({"xs": [1, 9, 3]}), 32);
318 assert_eq!(
319 d.changes,
320 [Change::Changed {
321 path: "xs.1".into(),
322 old: json!(2),
323 new: json!(9),
324 }]
325 );
326 }
327
328 #[test]
329 fn a_shorter_array_reports_the_tail_as_removed() {
330 let d = diff(&json!([1, 2, 3]), &json!([1]), 32);
331 assert_eq!(
332 d.changes,
333 [
334 Change::Removed {
335 path: "1".into(),
336 old: json!(2)
337 },
338 Change::Removed {
339 path: "2".into(),
340 old: json!(3)
341 },
342 ]
343 );
344 }
345
346 #[test]
349 fn a_shape_change_is_a_single_change() {
350 let d = diff(&json!({"a": {"b": 1}}), &json!({"a": [1]}), 32);
351 assert_eq!(d.changes.len(), 1);
352 assert_eq!(d.changes[0].path(), "a");
353 }
354
355 #[test]
358 fn changes_past_the_bound_are_counted_not_dropped_silently() {
359 let old = json!({"a": 1, "b": 1, "c": 1, "d": 1, "e": 1});
360 let new = json!({"a": 2, "b": 2, "c": 2, "d": 2, "e": 2});
361 let d = diff(&old, &new, 2);
362 assert_eq!(d.changes.len(), 2);
363 assert_eq!(d.truncated, 3);
364 assert!(!d.is_empty());
365 }
366
367 #[test]
368 fn byte_diff_brackets_the_differing_run() {
369 let d = byte_diff(b"hello world", b"hello there");
370 assert_eq!(d.common_prefix, 6);
371 assert_eq!(d.old_len, 11);
372 assert_eq!(d.new_len, 11);
373 let (old, new) = d.ranges();
374 assert_eq!(&b"hello world"[old], b"world");
375 assert_eq!(&b"hello there"[new], b"there");
376 assert!(!d.is_empty());
377 }
378
379 #[test]
380 fn identical_bytes_are_empty() {
381 let d = byte_diff(b"same", b"same");
382 assert!(d.is_empty());
383 assert_eq!(d.common_prefix, 4);
384 }
385
386 #[test]
389 fn prefix_and_suffix_never_overlap() {
390 let d = byte_diff(b"aaaa", b"aaaaaa");
391 assert_eq!(d.common_prefix, 4);
392 assert_eq!(d.common_suffix, 0);
393 assert!(d.common_prefix + d.common_suffix <= d.old_len);
394 }
395}