1use super::patch::{JsonPatch, PatchOperation};
8use super::simd_compare::simd_bytes_equal;
9use serde_json::Value;
10
11#[derive(Debug, Clone, Default)]
13pub struct DiffOptions {
14 pub detect_moves: bool,
16 pub detect_copies: bool,
18 pub optimize_arrays: bool,
20}
21
22impl DiffOptions {
23 #[must_use]
25 pub const fn with_moves(mut self) -> Self {
26 self.detect_moves = true;
27 self
28 }
29
30 #[must_use]
32 pub const fn with_copies(mut self) -> Self {
33 self.detect_copies = true;
34 self
35 }
36
37 #[must_use]
39 pub const fn with_array_optimization(mut self) -> Self {
40 self.optimize_arrays = true;
41 self
42 }
43}
44
45#[must_use]
56pub fn json_diff(source: &Value, target: &Value) -> JsonPatch {
57 json_diff_with_options(source, target, &DiffOptions::default())
58}
59
60#[must_use]
62pub fn json_diff_with_options(source: &Value, target: &Value, options: &DiffOptions) -> JsonPatch {
63 let mut patch = JsonPatch::new();
64 diff_values(source, target, "", &mut patch, options);
65 patch
66}
67
68fn diff_values(
70 source: &Value,
71 target: &Value,
72 path: &str,
73 ops: &mut JsonPatch,
74 options: &DiffOptions,
75) {
76 if values_equal_fast(source, target) {
78 return;
79 }
80
81 match (source, target) {
82 (Value::Object(src_map), Value::Object(tgt_map)) => {
84 diff_objects(src_map, tgt_map, path, ops, options);
85 }
86
87 (Value::Array(src_arr), Value::Array(tgt_arr)) => {
89 if options.optimize_arrays {
90 diff_arrays_optimized(src_arr, tgt_arr, path, ops, options);
91 } else {
92 diff_arrays_simple(src_arr, tgt_arr, path, ops, options);
93 }
94 }
95
96 _ => {
98 ops.push(PatchOperation::Replace {
99 path: path.to_string(),
100 value: target.clone(),
101 });
102 }
103 }
104}
105
106fn values_equal_fast(a: &Value, b: &Value) -> bool {
108 match (a, b) {
109 (Value::Null, Value::Null) => true,
110 (Value::Bool(a), Value::Bool(b)) => a == b,
111 (Value::Number(a), Value::Number(b)) => a == b,
112 (Value::String(a), Value::String(b)) => {
113 simd_bytes_equal(a.as_bytes(), b.as_bytes())
115 }
116 (Value::Array(a), Value::Array(b)) => {
117 if a.len() != b.len() {
118 return false;
119 }
120 a.iter().zip(b.iter()).all(|(x, y)| values_equal_fast(x, y))
121 }
122 (Value::Object(a), Value::Object(b)) => {
123 if a.len() != b.len() {
124 return false;
125 }
126 a.iter()
127 .all(|(k, v)| b.get(k).is_some_and(|bv| values_equal_fast(v, bv)))
128 }
129 _ => false,
130 }
131}
132
133fn diff_objects(
135 source: &serde_json::Map<String, Value>,
136 target: &serde_json::Map<String, Value>,
137 path: &str,
138 ops: &mut JsonPatch,
139 options: &DiffOptions,
140) {
141 for key in source.keys() {
143 if !target.contains_key(key) {
144 let key_path = format_path(path, key);
145 ops.push(PatchOperation::Remove { path: key_path });
146 }
147 }
148
149 for (key, tgt_value) in target {
151 let key_path = format_path(path, key);
152
153 match source.get(key) {
154 Some(src_value) => {
155 diff_values(src_value, tgt_value, &key_path, ops, options);
157 }
158 None => {
159 ops.push(PatchOperation::Add {
161 path: key_path,
162 value: tgt_value.clone(),
163 });
164 }
165 }
166 }
167}
168
169fn diff_arrays_simple(
171 source: &[Value],
172 target: &[Value],
173 path: &str,
174 ops: &mut JsonPatch,
175 options: &DiffOptions,
176) {
177 let src_len = source.len();
178 let tgt_len = target.len();
179
180 let common_len = src_len.min(tgt_len);
182 for i in 0..common_len {
183 let elem_path = format!("{path}/{i}");
184 diff_values(&source[i], &target[i], &elem_path, ops, options);
185 }
186
187 for i in (tgt_len..src_len).rev() {
189 ops.push(PatchOperation::Remove {
190 path: format!("{path}/{i}"),
191 });
192 }
193
194 for (i, item) in target.iter().enumerate().take(tgt_len).skip(src_len) {
196 ops.push(PatchOperation::Add {
197 path: format!("{path}/{i}"),
198 value: item.clone(),
199 });
200 }
201}
202
203fn diff_arrays_optimized(
208 source: &[Value],
209 target: &[Value],
210 path: &str,
211 ops: &mut JsonPatch,
212 options: &DiffOptions,
213) {
214 if source.len() <= 4 || target.len() <= 4 {
216 diff_arrays_simple(source, target, path, ops, options);
217 return;
218 }
219
220 let lcs = compute_lcs(source, target);
222
223 generate_patch_from_lcs(source, target, &lcs, path, ops, options);
225}
226
227fn compute_lcs(source: &[Value], target: &[Value]) -> Vec<(usize, usize)> {
229 let m = source.len();
230 let n = target.len();
231
232 let mut dp = vec![vec![0usize; n + 1]; m + 1];
234
235 for (i, src_val) in source.iter().enumerate() {
236 for (j, tgt_val) in target.iter().enumerate() {
237 if values_equal_fast(src_val, tgt_val) {
238 dp[i + 1][j + 1] = dp[i][j] + 1;
239 } else {
240 dp[i + 1][j + 1] = dp[i + 1][j].max(dp[i][j + 1]);
241 }
242 }
243 }
244
245 let mut lcs = Vec::new();
247 let mut i = m;
248 let mut j = n;
249
250 while i > 0 && j > 0 {
251 if values_equal_fast(&source[i - 1], &target[j - 1]) {
252 lcs.push((i - 1, j - 1));
253 i -= 1;
254 j -= 1;
255 } else if dp[i - 1][j] > dp[i][j - 1] {
256 i -= 1;
257 } else {
258 j -= 1;
259 }
260 }
261
262 lcs.reverse();
263 lcs
264}
265
266fn generate_patch_from_lcs(
268 source: &[Value],
269 target: &[Value],
270 lcs: &[(usize, usize)],
271 path: &str,
272 ops: &mut JsonPatch,
273 options: &DiffOptions,
274) {
275 let mut src_idx = 0;
276 let mut tgt_idx = 0;
277 let mut lcs_idx = 0;
278
279 let mut removals: Vec<usize> = Vec::new();
281
282 while tgt_idx < target.len() {
283 if lcs_idx < lcs.len() && lcs[lcs_idx].1 == tgt_idx {
284 let (src_lcs, _) = lcs[lcs_idx];
286
287 while src_idx < src_lcs {
289 removals.push(src_idx);
290 src_idx += 1;
291 }
292
293 let elem_path = format!("{path}/{tgt_idx}");
295 diff_values(&source[src_lcs], &target[tgt_idx], &elem_path, ops, options);
296
297 src_idx += 1;
298 tgt_idx += 1;
299 lcs_idx += 1;
300 } else {
301 ops.push(PatchOperation::Add {
303 path: format!("{path}/{tgt_idx}"),
304 value: target[tgt_idx].clone(),
305 });
306 tgt_idx += 1;
307 }
308 }
309
310 while src_idx < source.len() {
312 removals.push(src_idx);
313 src_idx += 1;
314 }
315
316 for &idx in removals.iter().rev() {
318 ops.push(PatchOperation::Remove {
319 path: format!("{path}/{idx}"),
320 });
321 }
322}
323
324fn format_path(base: &str, key: &str) -> String {
326 let escaped = key.replace('~', "~0").replace('/', "~1");
328 format!("{base}/{escaped}")
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use serde_json::json;
335
336 #[test]
337 fn test_diff_identical() {
338 let a = json!({"foo": "bar", "baz": 42});
339 let patch = json_diff(&a, &a);
340 assert!(patch.is_empty());
341 }
342
343 #[test]
344 fn test_diff_add_field() {
345 let a = json!({"foo": "bar"});
346 let b = json!({"foo": "bar", "baz": 42});
347 let patch = json_diff(&a, &b);
348
349 assert_eq!(patch.len(), 1);
350 assert!(
351 matches!(&patch.operations[0], PatchOperation::Add { path, value }
352 if path == "/baz" && *value == json!(42))
353 );
354 }
355
356 #[test]
357 fn test_diff_remove_field() {
358 let a = json!({"foo": "bar", "baz": 42});
359 let b = json!({"foo": "bar"});
360 let patch = json_diff(&a, &b);
361
362 assert_eq!(patch.len(), 1);
363 assert!(
364 matches!(&patch.operations[0], PatchOperation::Remove { path }
365 if path == "/baz")
366 );
367 }
368
369 #[test]
370 fn test_diff_replace_value() {
371 let a = json!({"foo": "bar"});
372 let b = json!({"foo": "baz"});
373 let patch = json_diff(&a, &b);
374
375 assert_eq!(patch.len(), 1);
376 assert!(
377 matches!(&patch.operations[0], PatchOperation::Replace { path, value }
378 if path == "/foo" && *value == json!("baz"))
379 );
380 }
381
382 #[test]
383 fn test_diff_nested_change() {
384 let a = json!({"outer": {"inner": "old"}});
385 let b = json!({"outer": {"inner": "new"}});
386 let patch = json_diff(&a, &b);
387
388 assert_eq!(patch.len(), 1);
389 assert!(
390 matches!(&patch.operations[0], PatchOperation::Replace { path, .. }
391 if path == "/outer/inner")
392 );
393 }
394
395 #[test]
396 fn test_diff_array_append() {
397 let a = json!({"arr": [1, 2]});
398 let b = json!({"arr": [1, 2, 3]});
399 let patch = json_diff(&a, &b);
400
401 assert_eq!(patch.len(), 1);
402 assert!(
403 matches!(&patch.operations[0], PatchOperation::Add { path, value }
404 if path == "/arr/2" && *value == json!(3))
405 );
406 }
407
408 #[test]
409 fn test_diff_array_remove() {
410 let a = json!({"arr": [1, 2, 3]});
411 let b = json!({"arr": [1, 2]});
412 let patch = json_diff(&a, &b);
413
414 assert_eq!(patch.len(), 1);
415 assert!(
416 matches!(&patch.operations[0], PatchOperation::Remove { path }
417 if path == "/arr/2")
418 );
419 }
420
421 #[test]
422 fn test_diff_type_change() {
423 let a = json!({"foo": "bar"});
424 let b = json!({"foo": 42});
425 let patch = json_diff(&a, &b);
426
427 assert_eq!(patch.len(), 1);
428 assert!(
429 matches!(&patch.operations[0], PatchOperation::Replace { path, value }
430 if path == "/foo" && *value == json!(42))
431 );
432 }
433
434 #[test]
435 fn test_diff_root_replace() {
436 let a = json!("foo");
437 let b = json!(42);
438 let patch = json_diff(&a, &b);
439
440 assert_eq!(patch.len(), 1);
441 assert!(
442 matches!(&patch.operations[0], PatchOperation::Replace { path, .. }
443 if path.is_empty())
444 );
445 }
446
447 #[test]
448 fn test_diff_roundtrip() {
449 use super::super::patch::apply_patch;
450
451 let a = json!({
452 "name": "Alice",
453 "age": 30,
454 "hobbies": ["reading", "swimming"],
455 "address": {
456 "city": "New York",
457 "zip": "10001"
458 }
459 });
460
461 let b = json!({
462 "name": "Alice",
463 "age": 31,
464 "hobbies": ["reading", "cycling"],
465 "address": {
466 "city": "Boston",
467 "zip": "02101"
468 },
469 "email": "alice@example.com"
470 });
471
472 let patch = json_diff(&a, &b);
473 let result = apply_patch(&a, &patch).unwrap();
474 assert_eq!(result, b);
475 }
476
477 #[test]
478 fn test_diff_with_special_chars() {
479 let a = json!({"foo/bar": "old"});
480 let b = json!({"foo/bar": "new"});
481 let patch = json_diff(&a, &b);
482
483 assert_eq!(patch.len(), 1);
484 assert!(
486 matches!(&patch.operations[0], PatchOperation::Replace { path, .. }
487 if path == "/foo~1bar")
488 );
489 }
490
491 #[test]
492 fn test_diff_empty_to_populated() {
493 let a = json!({});
494 let b = json!({"foo": "bar", "baz": [1, 2, 3]});
495 let patch = json_diff(&a, &b);
496
497 assert_eq!(patch.len(), 2);
499 }
500
501 #[test]
502 fn test_diff_options_default() {
503 let options = DiffOptions::default();
504 assert!(!options.detect_moves);
505 assert!(!options.detect_copies);
506 assert!(!options.optimize_arrays);
507 }
508
509 #[test]
510 fn test_diff_array_optimized() {
511 use super::super::patch::apply_patch;
512 let a = json!([1, 2, 3, 4, 5]);
513 let b = json!([1, 3, 5, 6]);
514
515 let options = DiffOptions::default().with_array_optimization();
516 let patch = json_diff_with_options(&a, &b, &options);
517
518 let result = apply_patch(&a, &patch).unwrap();
520 assert_eq!(result, b);
521 }
522
523 #[test]
524 fn test_values_equal_fast_strings() {
525 assert!(values_equal_fast(
526 &json!("hello world"),
527 &json!("hello world")
528 ));
529 assert!(!values_equal_fast(&json!("hello"), &json!("world")));
530 }
531
532 #[test]
533 fn test_values_equal_fast_numbers() {
534 assert!(values_equal_fast(&json!(42), &json!(42)));
535 assert!(!values_equal_fast(&json!(42), &json!(43)));
536 assert!(values_equal_fast(
537 &json!(std::f64::consts::PI),
538 &json!(std::f64::consts::PI)
539 ));
540 }
541
542 #[test]
543 fn test_values_equal_fast_nested() {
544 let a = json!({"arr": [1, 2, {"nested": true}]});
545 let b = json!({"arr": [1, 2, {"nested": true}]});
546 let c = json!({"arr": [1, 2, {"nested": false}]});
547
548 assert!(values_equal_fast(&a, &b));
549 assert!(!values_equal_fast(&a, &c));
550 }
551
552 #[test]
557 fn test_diff_options_with_moves() {
558 let opts = DiffOptions::default().with_moves();
559 assert!(opts.detect_moves);
560 assert!(!opts.detect_copies);
561 assert!(!opts.optimize_arrays);
562 }
563
564 #[test]
565 fn test_diff_options_with_copies() {
566 let opts = DiffOptions::default().with_copies();
567 assert!(!opts.detect_moves);
568 assert!(opts.detect_copies);
569 assert!(!opts.optimize_arrays);
570 }
571
572 #[test]
573 fn test_diff_options_with_array_optimization() {
574 let opts = DiffOptions::default().with_array_optimization();
575 assert!(!opts.detect_moves);
576 assert!(!opts.detect_copies);
577 assert!(opts.optimize_arrays);
578 }
579
580 #[test]
581 fn test_diff_options_chained() {
582 let opts = DiffOptions::default()
583 .with_moves()
584 .with_copies()
585 .with_array_optimization();
586 assert!(opts.detect_moves);
587 assert!(opts.detect_copies);
588 assert!(opts.optimize_arrays);
589 }
590
591 #[test]
592 fn test_diff_options_debug() {
593 let opts = DiffOptions::default().with_moves();
594 let debug_str = format!("{opts:?}");
595 assert!(debug_str.contains("DiffOptions"));
596 assert!(debug_str.contains("detect_moves"));
597 }
598
599 #[test]
600 fn test_diff_options_clone() {
601 let opts = DiffOptions::default()
602 .with_copies()
603 .with_array_optimization();
604 #[allow(clippy::redundant_clone)]
605 let cloned = opts.clone();
606 assert_eq!(cloned.detect_copies, opts.detect_copies);
607 assert_eq!(cloned.optimize_arrays, opts.optimize_arrays);
608 }
609
610 #[test]
615 fn test_values_equal_fast_null() {
616 assert!(values_equal_fast(&json!(null), &json!(null)));
617 assert!(!values_equal_fast(&json!(null), &json!(0)));
618 }
619
620 #[test]
621 fn test_values_equal_fast_bool() {
622 assert!(values_equal_fast(&json!(true), &json!(true)));
623 assert!(values_equal_fast(&json!(false), &json!(false)));
624 assert!(!values_equal_fast(&json!(true), &json!(false)));
625 assert!(!values_equal_fast(&json!(false), &json!(true)));
626 }
627
628 #[test]
629 fn test_values_equal_fast_different_types() {
630 assert!(!values_equal_fast(&json!(null), &json!(false)));
631 assert!(!values_equal_fast(&json!(0), &json!("0")));
632 assert!(!values_equal_fast(&json!([]), &json!({})));
633 assert!(!values_equal_fast(&json!(""), &json!(null)));
634 assert!(!values_equal_fast(&json!(1), &json!(true)));
635 }
636
637 #[test]
638 fn test_values_equal_fast_array_different_lengths() {
639 assert!(!values_equal_fast(&json!([1, 2]), &json!([1, 2, 3])));
640 assert!(!values_equal_fast(&json!([1, 2, 3]), &json!([1, 2])));
641 }
642
643 #[test]
644 fn test_values_equal_fast_array_empty() {
645 assert!(values_equal_fast(&json!([]), &json!([])));
646 }
647
648 #[test]
649 fn test_values_equal_fast_object_different_lengths() {
650 assert!(!values_equal_fast(
651 &json!({"a": 1}),
652 &json!({"a": 1, "b": 2})
653 ));
654 assert!(!values_equal_fast(
655 &json!({"a": 1, "b": 2}),
656 &json!({"a": 1})
657 ));
658 }
659
660 #[test]
661 fn test_values_equal_fast_object_missing_key() {
662 assert!(!values_equal_fast(&json!({"a": 1}), &json!({"b": 1})));
664 }
665
666 #[test]
667 fn test_values_equal_fast_object_empty() {
668 assert!(values_equal_fast(&json!({}), &json!({})));
669 }
670
671 #[test]
672 fn test_values_equal_fast_long_strings() {
673 let long1 = "a".repeat(1000);
674 let long2 = "a".repeat(1000);
675 let long3 = "a".repeat(999) + "b";
676 assert!(values_equal_fast(&json!(long1), &json!(long2)));
677 assert!(!values_equal_fast(&json!(long1), &json!(long3)));
678 }
679
680 #[test]
685 fn test_format_path_simple() {
686 assert_eq!(format_path("", "foo"), "/foo");
687 assert_eq!(format_path("/root", "child"), "/root/child");
688 }
689
690 #[test]
691 fn test_format_path_with_slash() {
692 assert_eq!(format_path("", "foo/bar"), "/foo~1bar");
693 }
694
695 #[test]
696 fn test_format_path_with_tilde() {
697 assert_eq!(format_path("", "foo~bar"), "/foo~0bar");
698 }
699
700 #[test]
701 fn test_format_path_with_both_special_chars() {
702 assert_eq!(format_path("", "~a/b~"), "/~0a~1b~0");
703 }
704
705 #[test]
710 fn test_compute_lcs_identical() {
711 let arr = vec![json!(1), json!(2), json!(3)];
712 let lcs = compute_lcs(&arr, &arr);
713 assert_eq!(lcs.len(), 3);
714 assert_eq!(lcs, vec![(0, 0), (1, 1), (2, 2)]);
715 }
716
717 #[test]
718 fn test_compute_lcs_empty_source() {
719 let lcs = compute_lcs(&[], &[json!(1), json!(2)]);
720 assert!(lcs.is_empty());
721 }
722
723 #[test]
724 fn test_compute_lcs_empty_target() {
725 let lcs = compute_lcs(&[json!(1), json!(2)], &[]);
726 assert!(lcs.is_empty());
727 }
728
729 #[test]
730 fn test_compute_lcs_no_common() {
731 let source = vec![json!(1), json!(2)];
732 let target = vec![json!(3), json!(4)];
733 let lcs = compute_lcs(&source, &target);
734 assert!(lcs.is_empty());
735 }
736
737 #[test]
738 fn test_compute_lcs_partial_overlap() {
739 let source = vec![json!(1), json!(2), json!(3), json!(4)];
740 let target = vec![json!(1), json!(3), json!(5)];
741 let lcs = compute_lcs(&source, &target);
742 assert_eq!(lcs.len(), 2);
744 }
745
746 #[test]
747 fn test_compute_lcs_interleaved() {
748 let source = vec![json!(1), json!(3), json!(5), json!(7)];
749 let target = vec![json!(2), json!(3), json!(4), json!(5), json!(6)];
750 let lcs = compute_lcs(&source, &target);
751 assert_eq!(lcs.len(), 2);
753 }
754
755 #[test]
760 fn test_diff_arrays_simple_all_different() {
761 let a = json!([1, 2, 3]);
762 let b = json!([4, 5, 6]);
763 let patch = json_diff(&a, &b);
764
765 assert_eq!(patch.len(), 3);
767 for op in &patch.operations {
768 assert!(matches!(op, PatchOperation::Replace { .. }));
769 }
770 }
771
772 #[test]
773 fn test_diff_arrays_remove_multiple() {
774 let a = json!([1, 2, 3, 4, 5]);
775 let b = json!([1, 2]);
776 let patch = json_diff(&a, &b);
777
778 let removes_count = patch
780 .operations
781 .iter()
782 .filter(|op| matches!(op, PatchOperation::Remove { .. }))
783 .count();
784 assert_eq!(removes_count, 3);
785 }
786
787 #[test]
788 fn test_diff_arrays_add_multiple() {
789 let a = json!([1]);
790 let b = json!([1, 2, 3, 4]);
791 let patch = json_diff(&a, &b);
792
793 let adds_count = patch
795 .operations
796 .iter()
797 .filter(|op| matches!(op, PatchOperation::Add { .. }))
798 .count();
799 assert_eq!(adds_count, 3);
800 }
801
802 #[test]
803 fn test_diff_arrays_optimized_large() {
804 let a = json!([1, 2, 3, 4, 5, 6, 7, 8]);
806 let b = json!([1, 3, 5, 7, 9]);
807
808 let options = DiffOptions::default().with_array_optimization();
809 let patch = json_diff_with_options(&a, &b, &options);
810
811 assert!(!patch.is_empty());
815 }
816
817 #[test]
818 fn test_diff_arrays_optimized_insertion_in_middle() {
819 let a = json!([1, 2, 3, 4, 5, 6]);
820 let b = json!([1, 2, 100, 3, 4, 5, 6]);
821
822 let options = DiffOptions::default().with_array_optimization();
823 let patch = json_diff_with_options(&a, &b, &options);
824
825 assert!(!patch.is_empty());
827 }
828
829 #[test]
830 fn test_diff_arrays_optimized_deletion_from_middle() {
831 let a = json!([1, 2, 3, 4, 5, 6, 7, 8]);
832 let b = json!([1, 2, 5, 6, 7, 8]);
833
834 let options = DiffOptions::default().with_array_optimization();
835 let patch = json_diff_with_options(&a, &b, &options);
836
837 assert!(!patch.is_empty());
839 }
840
841 #[test]
842 fn test_diff_arrays_optimized_complete_replacement() {
843 let a = json!([1, 2, 3, 4, 5, 6]);
844 let b = json!([10, 20, 30, 40, 50, 60]);
845
846 let options = DiffOptions::default().with_array_optimization();
847 let patch = json_diff_with_options(&a, &b, &options);
848
849 assert!(!patch.is_empty());
851 }
852
853 #[test]
854 fn test_diff_arrays_small_uses_simple() {
855 use super::super::patch::apply_patch;
856 let a = json!([1, 2, 3, 4]);
858 let b = json!([1, 3, 5]);
859
860 let options = DiffOptions::default().with_array_optimization();
861 let patch = json_diff_with_options(&a, &b, &options);
862
863 let result = apply_patch(&a, &patch).unwrap();
864 assert_eq!(result, b);
865 }
866
867 #[test]
872 fn test_diff_objects_multiple_changes() {
873 let a = json!({
874 "keep": "same",
875 "modify": "old",
876 "remove": "gone"
877 });
878 let b = json!({
879 "keep": "same",
880 "modify": "new",
881 "add": "fresh"
882 });
883 let patch = json_diff(&a, &b);
884
885 assert_eq!(patch.len(), 3);
887 }
888
889 #[test]
890 fn test_diff_objects_nested_add() {
891 let a = json!({"outer": {}});
892 let b = json!({"outer": {"inner": "value"}});
893 let patch = json_diff(&a, &b);
894
895 assert_eq!(patch.len(), 1);
896 assert!(
897 matches!(&patch.operations[0], PatchOperation::Add { path, .. }
898 if path == "/outer/inner")
899 );
900 }
901
902 #[test]
903 fn test_diff_objects_nested_remove() {
904 let a = json!({"outer": {"inner": "value"}});
905 let b = json!({"outer": {}});
906 let patch = json_diff(&a, &b);
907
908 assert_eq!(patch.len(), 1);
909 assert!(
910 matches!(&patch.operations[0], PatchOperation::Remove { path }
911 if path == "/outer/inner")
912 );
913 }
914
915 #[test]
916 fn test_diff_deep_nesting() {
917 let a = json!({"a": {"b": {"c": {"d": "old"}}}});
918 let b = json!({"a": {"b": {"c": {"d": "new"}}}});
919 let patch = json_diff(&a, &b);
920
921 assert_eq!(patch.len(), 1);
922 assert!(
923 matches!(&patch.operations[0], PatchOperation::Replace { path, .. }
924 if path == "/a/b/c/d")
925 );
926 }
927
928 #[test]
933 fn test_diff_empty_array() {
934 let a = json!([]);
935 let b = json!([]);
936 let patch = json_diff(&a, &b);
937 assert!(patch.is_empty());
938 }
939
940 #[test]
941 fn test_diff_empty_to_array() {
942 let a = json!([]);
943 let b = json!([1, 2, 3]);
944 let patch = json_diff(&a, &b);
945 assert_eq!(patch.len(), 3);
946 }
947
948 #[test]
949 fn test_diff_array_to_empty() {
950 let a = json!([1, 2, 3]);
951 let b = json!([]);
952 let patch = json_diff(&a, &b);
953 assert_eq!(patch.len(), 3);
954 }
955
956 #[test]
957 fn test_diff_null_values() {
958 let a = json!({"key": null});
959 let b = json!({"key": "value"});
960 let patch = json_diff(&a, &b);
961
962 assert_eq!(patch.len(), 1);
963 assert!(matches!(
964 &patch.operations[0],
965 PatchOperation::Replace { .. }
966 ));
967 }
968
969 #[test]
970 fn test_diff_boolean_values() {
971 let a = json!({"flag": true});
972 let b = json!({"flag": false});
973 let patch = json_diff(&a, &b);
974
975 assert_eq!(patch.len(), 1);
976 }
977
978 #[test]
979 fn test_diff_number_precision() {
980 let a = json!({"value": 0.1});
981 let b = json!({"value": 0.1});
982 let patch = json_diff(&a, &b);
983 assert!(patch.is_empty());
984 }
985
986 #[test]
987 fn test_diff_array_object_mix() {
988 let a = json!([{"a": 1}, {"b": 2}]);
989 let b = json!([{"a": 1}, {"b": 3}]);
990 let patch = json_diff(&a, &b);
991
992 assert_eq!(patch.len(), 1);
993 assert!(
994 matches!(&patch.operations[0], PatchOperation::Replace { path, .. }
995 if path == "/1/b")
996 );
997 }
998
999 #[test]
1000 fn test_diff_with_options_no_optimization() {
1001 use super::super::patch::apply_patch;
1002 let a = json!([1, 2, 3, 4, 5, 6, 7, 8]);
1003 let b = json!([1, 3, 5, 7]);
1004
1005 let opts = DiffOptions::default();
1007 let patch = json_diff_with_options(&a, &b, &opts);
1008
1009 let result = apply_patch(&a, &patch).unwrap();
1010 assert_eq!(result, b);
1011 }
1012
1013 #[test]
1014 fn test_generate_patch_from_lcs_edge_cases() {
1015 let source = vec![json!(1), json!(2), json!(3), json!(4), json!(5)];
1017 let target = vec![json!(1), json!(2)];
1018
1019 let lcs = compute_lcs(&source, &target);
1020 let mut patch = JsonPatch::new();
1021 let opts = DiffOptions::default();
1022 generate_patch_from_lcs(&source, &target, &lcs, "", &mut patch, &opts);
1023
1024 let removes_count = patch
1026 .operations
1027 .iter()
1028 .filter(|op| matches!(op, PatchOperation::Remove { .. }))
1029 .count();
1030 assert_eq!(removes_count, 3);
1031 }
1032
1033 #[test]
1034 fn test_diff_complex_roundtrip() {
1035 use super::super::patch::apply_patch;
1036 let a = json!({
1037 "users": [
1038 {"id": 1, "name": "Alice", "tags": ["admin", "active"]},
1039 {"id": 2, "name": "Bob", "tags": ["user"]},
1040 {"id": 3, "name": "Charlie", "tags": ["user", "inactive"]}
1041 ],
1042 "metadata": {
1043 "version": "1.0",
1044 "count": 3
1045 }
1046 });
1047
1048 let b = json!({
1049 "users": [
1050 {"id": 1, "name": "Alice", "tags": ["admin", "active", "vip"]},
1051 {"id": 3, "name": "Charlie Updated", "tags": ["user"]}
1052 ],
1053 "metadata": {
1054 "version": "2.0",
1055 "count": 2,
1056 "updated": true
1057 }
1058 });
1059
1060 let patch = json_diff(&a, &b);
1061 let result = apply_patch(&a, &patch).unwrap();
1062 assert_eq!(result, b);
1063 }
1064}