1use serde_json::Value;
2use snafu::Snafu;
3
4use crate::config::{AmbiguousMatchStrategy, ArrayMatchMode, DiffConfig};
5use crate::model::{ChildKind, DiffKind, DiffNode, DiffTree, PathSegment};
6
7type Result<T, E = Error> = std::result::Result<T, E>;
8
9const NO_DIFFERENCES: Vec<DiffNode> = vec![];
11
12#[derive(Debug, Snafu)]
14pub enum Error {
15 #[snafu(display(
18 "expected array element at path `{path}` is missing the key field `{key_field}` required for matching"
19 ))]
20 MissingKeyField {
21 path: String,
23
24 key_field: String,
26 },
27
28 #[snafu(display("ambiguous match at path `{path}`: {count} candidates matched"))]
31 AmbiguousMatch {
32 path: String,
34
35 count: u16,
37 },
38}
39
40pub fn diff(actual: &Value, expected: &Value, config: &DiffConfig) -> Result<DiffTree> {
46 let path = "";
48 let roots = match diff_values(actual, expected, config, path)? {
49 DiffResult::Equal => NO_DIFFERENCES,
53 DiffResult::Leaf(_kind) => NO_DIFFERENCES,
57 DiffResult::Children { nodes, .. } => nodes,
59 };
60 Ok(DiffTree { roots })
61}
62
63enum DiffResult {
66 Equal,
68
69 Leaf(DiffKind),
72
73 Children {
76 child_kind: ChildKind,
77 nodes: Vec<DiffNode>,
78 omitted_count: u16,
79 },
80}
81
82fn diff_values(
87 actual: &Value,
88 expected: &Value,
89 config: &DiffConfig,
90 path: &str,
91) -> Result<DiffResult> {
92 if std::mem::discriminant(actual) != std::mem::discriminant(expected) {
95 return Ok(DiffResult::Leaf(DiffKind::type_mismatch(
96 actual.clone(),
97 value_type_name(actual),
98 expected.clone(),
99 value_type_name(expected),
100 )));
101 }
102
103 match (actual, expected) {
104 (Value::Null, Value::Null) => Ok(DiffResult::Equal),
106 (Value::Bool(a), Value::Bool(e)) if a == e => Ok(DiffResult::Equal),
107 (Value::Number(a), Value::Number(e)) if a == e => Ok(DiffResult::Equal),
108 (Value::String(a), Value::String(e)) if a == e => Ok(DiffResult::Equal),
109
110 (Value::Bool(_), Value::Bool(_))
112 | (Value::Number(_), Value::Number(_))
113 | (Value::String(_), Value::String(_)) => Ok(DiffResult::Leaf(DiffKind::changed(
114 actual.clone(),
115 expected.clone(),
116 ))),
117
118 (Value::Object(actual_map), Value::Object(expected_map)) => {
120 diff_objects(actual_map, expected_map, config, path)
121 }
122
123 (Value::Array(actual_arr), Value::Array(expected_arr)) => {
125 diff_arrays(actual_arr, expected_arr, config, path)
126 }
127
128 _ => unreachable!("discriminant check above ensures matching types"),
129 }
130}
131
132fn diff_objects(
140 actual_map: &serde_json::Map<String, Value>,
141 expected_map: &serde_json::Map<String, Value>,
142 config: &DiffConfig,
143 path: &str,
144) -> Result<DiffResult> {
145 let mut children = Vec::new();
146
147 for (key, expected_val) in expected_map {
150 let child_path = if path.is_empty() {
153 key.clone()
154 } else {
155 format!("{path}.{key}")
156 };
157 let segment = PathSegment::Key(key.clone());
158
159 match actual_map.get(key) {
160 None => {
162 let kind = DiffKind::missing(expected_val.clone());
163 children.push(DiffNode::leaf(segment, kind));
164 }
165
166 Some(actual_val) => {
168 match diff_values(actual_val, expected_val, config, &child_path)? {
169 DiffResult::Equal => {}
171
172 DiffResult::Leaf(kind) => {
174 children.push(DiffNode::leaf(segment, kind));
175 }
176
177 DiffResult::Children {
179 child_kind,
180 nodes,
181 omitted_count,
182 } => {
183 children.push(DiffNode::container(
184 segment,
185 child_kind,
186 omitted_count,
187 nodes,
188 ));
189 }
190 }
191 }
192 }
193 }
194
195 if children.is_empty() {
197 return Ok(DiffResult::Equal);
198 }
199
200 let omitted_count = actual_map.len().saturating_sub(expected_map.len()) as u16;
203 Ok(DiffResult::Children {
204 child_kind: ChildKind::Fields,
205 nodes: children,
206 omitted_count,
207 })
208}
209
210fn diff_arrays(
215 actual_arr: &[Value],
216 expected_arr: &[Value],
217 config: &DiffConfig,
218 path: &str,
219) -> Result<DiffResult> {
220 let path_config = config.match_config().config_at(path);
221 let mode = path_config
222 .map(|c| c.mode())
223 .unwrap_or(config.default_array_mode());
224 let ambiguous_strategy = path_config
225 .and_then(|c| c.ambiguous_strategy())
226 .unwrap_or(config.default_ambiguous_strategy());
227
228 match mode {
229 ArrayMatchMode::Index => diff_arrays_by_index(actual_arr, expected_arr, config, path),
230 ArrayMatchMode::Key(key_field) => diff_arrays_by_key(
231 actual_arr,
232 expected_arr,
233 key_field,
234 ambiguous_strategy,
235 config,
236 path,
237 ),
238 ArrayMatchMode::Contains => {
239 diff_arrays_by_contains(actual_arr, expected_arr, ambiguous_strategy, config, path)
240 }
241 }
242}
243
244fn diff_arrays_by_index(
249 actual_arr: &[Value],
250 expected_arr: &[Value],
251 config: &DiffConfig,
252 path: &str,
253) -> Result<DiffResult> {
254 let mut children = Vec::new();
255
256 for (i, expected_elem) in expected_arr.iter().enumerate() {
259 let segment = PathSegment::Index(i as u16);
260
261 match actual_arr.get(i) {
262 None => {
264 let kind = DiffKind::missing(expected_elem.clone());
265 children.push(DiffNode::leaf(segment, kind));
266 }
267
268 Some(actual_elem) => {
270 match diff_values(actual_elem, expected_elem, config, path)? {
271 DiffResult::Equal => {}
273
274 DiffResult::Leaf(kind) => {
276 children.push(DiffNode::leaf(segment, kind));
277 }
278
279 DiffResult::Children {
281 child_kind,
282 nodes,
283 omitted_count,
284 } => {
285 children.push(DiffNode::container(
286 segment,
287 child_kind,
288 omitted_count,
289 nodes,
290 ));
291 }
292 }
293 }
294 }
295 }
296
297 if children.is_empty() {
299 return Ok(DiffResult::Equal);
300 }
301
302 let omitted_count = actual_arr.len().saturating_sub(expected_arr.len()) as u16;
305 Ok(DiffResult::Children {
306 child_kind: ChildKind::Items,
307 nodes: children,
308 omitted_count,
309 })
310}
311
312fn diff_arrays_by_key(
318 actual_arr: &[Value],
319 expected_arr: &[Value],
320 key_field: &str,
321 ambiguous_strategy: &AmbiguousMatchStrategy,
322 config: &DiffConfig,
323 path: &str,
324) -> Result<DiffResult> {
325 let mut children = Vec::new();
326 let mut matched_count: u16 = 0;
328
329 for expected_elem in expected_arr {
332 let expected_key_val = expected_elem
336 .get(key_field)
337 .and_then(|v| v.as_str())
338 .ok_or_else(|| Error::MissingKeyField {
339 path: path.to_owned(),
340 key_field: key_field.to_owned(),
341 })?;
342
343 let candidates: Vec<&Value> = actual_arr
345 .iter()
346 .filter(|elem| elem.get(key_field).and_then(|v| v.as_str()) == Some(expected_key_val))
347 .collect();
348
349 match candidates.len() {
350 0 => {
352 let kind = DiffKind::missing(expected_elem.clone());
353 children.push(DiffNode::leaf(PathSegment::Unmatched, kind));
354 }
355
356 1 => {
358 matched_count += 1;
359 let segment = PathSegment::NamedElement {
360 match_key: key_field.to_owned(),
361 match_value: expected_key_val.to_owned(),
362 };
363 match diff_values(candidates[0], expected_elem, config, path)? {
364 DiffResult::Equal => {}
366
367 DiffResult::Leaf(kind) => {
369 children.push(DiffNode::leaf(segment, kind));
370 }
371
372 DiffResult::Children {
374 child_kind,
375 nodes,
376 omitted_count,
377 } => {
378 children.push(DiffNode::container(
379 segment,
380 child_kind,
381 omitted_count,
382 nodes,
383 ));
384 }
385 }
386 }
387
388 _ => match ambiguous_strategy {
390 AmbiguousMatchStrategy::Strict => {
391 return Err(Error::AmbiguousMatch {
392 path: path.to_owned(),
393 count: candidates.len() as u16,
394 });
395 }
396
397 AmbiguousMatchStrategy::BestMatch | AmbiguousMatchStrategy::Silent => {
398 matched_count += 1;
399 let segment = PathSegment::NamedElement {
400 match_key: key_field.to_owned(),
401 match_value: expected_key_val.to_owned(),
402 };
403 let best =
405 pick_best_match(candidates.iter().copied(), expected_elem, config, path)?;
406 push_diff_result(&mut children, segment, best);
407 }
408 },
409 }
410 }
411
412 if children.is_empty() {
414 return Ok(DiffResult::Equal);
415 }
416
417 let omitted_count = (actual_arr.len() as u16).saturating_sub(matched_count);
419 Ok(DiffResult::Children {
420 child_kind: ChildKind::Items,
421 nodes: children,
422 omitted_count,
423 })
424}
425
426fn diff_arrays_by_contains(
432 actual_arr: &[Value],
433 expected_arr: &[Value],
434 ambiguous_strategy: &AmbiguousMatchStrategy,
435 _config: &DiffConfig,
436 path: &str,
437) -> Result<DiffResult> {
438 let mut children = Vec::new();
439 let mut matched_count: u16 = 0;
441
442 for expected_elem in expected_arr {
443 let candidates: Vec<(usize, &Value)> = actual_arr
448 .iter()
449 .enumerate()
450 .filter(|(_, actual_elem)| value_contains(actual_elem, expected_elem))
451 .collect();
452
453 match candidates.len() {
454 0 => {
456 let kind = DiffKind::missing(expected_elem.clone());
457 children.push(DiffNode::leaf(PathSegment::Unmatched, kind));
458 }
459
460 1 => {
463 matched_count += 1;
464 }
465
466 _ => match ambiguous_strategy {
468 AmbiguousMatchStrategy::Strict => {
469 return Err(Error::AmbiguousMatch {
470 path: path.to_owned(),
471 count: candidates.len() as u16,
472 });
473 }
474
475 AmbiguousMatchStrategy::BestMatch | AmbiguousMatchStrategy::Silent => {
478 matched_count += 1;
479 }
480 },
481 }
482 }
483
484 if children.is_empty() {
485 return Ok(DiffResult::Equal);
486 }
487
488 let omitted_count = (actual_arr.len() as u16).saturating_sub(matched_count);
490 Ok(DiffResult::Children {
491 child_kind: ChildKind::Items,
492 nodes: children,
493 omitted_count,
494 })
495}
496
497fn value_contains(actual: &Value, expected: &Value) -> bool {
503 match (actual, expected) {
504 _ if std::mem::discriminant(actual) != std::mem::discriminant(expected) => false,
506
507 (Value::Null, Value::Null) => true,
509 (Value::Bool(a), Value::Bool(e)) => a == e,
510 (Value::Number(a), Value::Number(e)) => a == e,
511 (Value::String(a), Value::String(e)) => a == e,
512
513 (Value::Object(actual_map), Value::Object(expected_map)) => {
515 expected_map.iter().all(|(key, expected_val)| {
516 actual_map
517 .get(key)
518 .is_some_and(|actual_val| value_contains(actual_val, expected_val))
519 })
520 }
521
522 (Value::Array(a), Value::Array(e)) => a == e,
524
525 _ => unreachable!("discriminant check above ensures matching types"),
526 }
527}
528
529fn pick_best_match<'a>(
534 candidates: impl Iterator<Item = &'a Value>,
535 expected: &Value,
536 config: &DiffConfig,
537 path: &str,
538) -> Result<DiffResult> {
539 let mut best: Option<DiffResult> = None;
540 let mut best_count: Option<usize> = None;
542
543 for candidate in candidates {
544 let result = diff_values(candidate, expected, config, path)?;
545
546 if matches!(result, DiffResult::Equal) {
548 return Ok(result);
549 }
550
551 let count = match &result {
555 DiffResult::Children { nodes, .. } => nodes.len(),
556 DiffResult::Leaf(_) => 1,
557 DiffResult::Equal => unreachable!("handled above"),
558 };
559
560 if best_count.is_none() || count < best_count.expect("guarded by is_none check") {
562 best = Some(result);
563 best_count = Some(count);
564 }
565 }
566
567 Ok(best.unwrap_or(DiffResult::Equal))
568}
569
570fn push_diff_result(children: &mut Vec<DiffNode>, segment: PathSegment, result: DiffResult) {
572 match result {
573 DiffResult::Equal => {}
575
576 DiffResult::Leaf(kind) => {
578 children.push(DiffNode::leaf(segment, kind));
579 }
580
581 DiffResult::Children {
583 child_kind,
584 nodes,
585 omitted_count,
586 } => {
587 children.push(DiffNode::container(segment, child_kind, omitted_count, nodes));
588 }
589 }
590}
591
592fn value_type_name(value: &Value) -> &'static str {
594 match value {
595 Value::Null => "null",
596 Value::Bool(_) => "bool",
597 Value::Number(_) => "number",
598 Value::String(_) => "string",
599 Value::Array(_) => "array",
600 Value::Object(_) => "object",
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use serde_json::json;
608
609 fn default_config() -> DiffConfig {
610 DiffConfig::default()
611 }
612
613 #[test]
619 fn object_key_order_does_not_affect_equality() {
620 let actual = json!({"z": 1, "a": 2, "m": 3});
621 let expected = json!({"m": 3, "z": 1, "a": 2});
622 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
623 assert!(tree.is_empty());
624 }
625
626 #[test]
627 fn object_key_order_does_not_affect_diffs() {
628 let actual = json!({"z": 1, "a": 2});
631 let expected = json!({"a": 99, "z": 1});
632 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
633
634 assert_eq!(tree.roots.len(), 1);
635 let DiffNode::Leaf { segment, kind } = &tree.roots[0] else {
636 panic!("expected Leaf");
637 };
638 assert!(matches!(segment, PathSegment::Key(k) if k == "a"));
639 assert!(matches!(kind, DiffKind::Changed { .. }));
640 }
641
642 #[test]
643 fn equal_objects_produce_empty_diff() {
644 let actual = json!({"a": 1, "b": "hello"});
645 let expected = json!({"a": 1, "b": "hello"});
646 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
647 assert!(tree.is_empty());
648 }
649
650 #[test]
651 fn scalar_changed() {
652 let actual = json!({"a": {"b": {"c": "foo"}}});
653 let expected = json!({"a": {"b": {"c": "bar"}}});
654 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
655
656 assert_eq!(tree.roots.len(), 1);
658 let DiffNode::Container {
659 segment, children, ..
660 } = &tree.roots[0]
661 else {
662 panic!("expected Container");
663 };
664 assert!(matches!(segment, PathSegment::Key(k) if k == "a"));
665
666 let DiffNode::Container {
667 segment, children, ..
668 } = &children[0]
669 else {
670 panic!("expected Container");
671 };
672 assert!(matches!(segment, PathSegment::Key(k) if k == "b"));
673
674 let DiffNode::Leaf { segment, kind } = &children[0] else {
675 panic!("expected Leaf");
676 };
677 assert!(matches!(segment, PathSegment::Key(k) if k == "c"));
678 assert!(matches!(kind, DiffKind::Changed { actual, expected }
679 if actual == &json!("foo") && expected == &json!("bar")
680 ));
681 }
682
683 #[test]
684 fn missing_key() {
685 let actual = json!({"a": 1});
686 let expected = json!({"a": 1, "b": 2});
687 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
688
689 assert_eq!(tree.roots.len(), 1);
690 let DiffNode::Leaf { segment, kind } = &tree.roots[0] else {
691 panic!("expected Leaf");
692 };
693 assert!(matches!(segment, PathSegment::Key(k) if k == "b"));
694 assert!(matches!(kind, DiffKind::Missing { expected } if expected == &json!(2)));
695 }
696
697 #[test]
698 fn type_mismatch() {
699 let actual = json!({"a": 42});
700 let expected = json!({"a": "42"});
701 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
702
703 assert_eq!(tree.roots.len(), 1);
704 let DiffNode::Leaf { segment, kind } = &tree.roots[0] else {
705 panic!("expected Leaf");
706 };
707 assert!(matches!(segment, PathSegment::Key(k) if k == "a"));
708 assert!(matches!(
709 kind,
710 DiffKind::TypeMismatch {
711 actual_type: "number",
712 expected_type: "string",
713 ..
714 }
715 ));
716 }
717
718 #[test]
719 fn omitted_count_reflects_extra_actual_keys() {
720 let actual = json!({"a": 1, "b": 2, "c": 3});
721 let expected = json!({"a": 99});
722 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
723
724 assert_eq!(tree.roots.len(), 1);
725 let DiffNode::Leaf { kind, .. } = &tree.roots[0] else {
726 panic!("expected Leaf for Changed");
727 };
728 assert!(matches!(kind, DiffKind::Changed { .. }));
729
730 let result = diff_values(&actual, &expected, &default_config(), "")
733 .expect("diff_values with valid inputs");
734 assert!(matches!(
735 result,
736 DiffResult::Children {
737 omitted_count: 2,
738 ..
739 }
740 ));
741 }
742
743 #[test]
744 fn nested_missing_key() {
745 let actual = json!({"a": {"x": 1}});
746 let expected = json!({"a": {"x": 1, "y": 2}});
747 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
748
749 assert_eq!(tree.roots.len(), 1);
750 let DiffNode::Container {
751 segment,
752 children,
753 omitted_count,
754 ..
755 } = &tree.roots[0]
756 else {
757 panic!("expected Container");
758 };
759 assert!(matches!(segment, PathSegment::Key(k) if k == "a"));
760 assert_eq!(*omitted_count, 0);
761
762 assert_eq!(children.len(), 1);
763 let DiffNode::Leaf { segment, kind } = &children[0] else {
764 panic!("expected Leaf");
765 };
766 assert!(matches!(segment, PathSegment::Key(k) if k == "y"));
767 assert!(matches!(kind, DiffKind::Missing { expected } if expected == &json!(2)));
768 }
769
770 #[test]
771 fn index_based_array_equal() {
772 let actual = json!({"items": [1, 2, 3]});
773 let expected = json!({"items": [1, 2, 3]});
774 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
775 assert!(tree.is_empty());
776 }
777
778 #[test]
779 fn index_based_array_changed() {
780 let actual = json!({"items": [1, 2, 3]});
781 let expected = json!({"items": [1, 99, 3]});
782 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
783
784 assert_eq!(tree.roots.len(), 1);
786 let DiffNode::Container { children, .. } = &tree.roots[0] else {
787 panic!("expected Container");
788 };
789 assert_eq!(children.len(), 1);
790 let DiffNode::Leaf { segment, kind } = &children[0] else {
791 panic!("expected Leaf");
792 };
793 assert!(matches!(segment, PathSegment::Index(1)));
794 assert!(matches!(kind, DiffKind::Changed { actual, expected }
795 if actual == &json!(2) && expected == &json!(99)
796 ));
797 }
798
799 #[test]
800 fn index_based_array_missing_element() {
801 let actual = json!({"items": [1]});
802 let expected = json!({"items": [1, 2, 3]});
803 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
804
805 let DiffNode::Container { children, .. } = &tree.roots[0] else {
806 panic!("expected Container");
807 };
808 assert_eq!(children.len(), 2);
809
810 let DiffNode::Leaf { segment, kind } = &children[0] else {
812 panic!("expected Leaf");
813 };
814 assert!(matches!(segment, PathSegment::Index(1)));
815 assert!(matches!(kind, DiffKind::Missing { expected } if expected == &json!(2)));
816
817 let DiffNode::Leaf { segment, kind } = &children[1] else {
819 panic!("expected Leaf");
820 };
821 assert!(matches!(segment, PathSegment::Index(2)));
822 assert!(matches!(kind, DiffKind::Missing { expected } if expected == &json!(3)));
823 }
824
825 #[test]
826 fn index_based_array_omitted_count() {
827 let actual = json!({"items": [1, 2, 3, 4, 5]});
829 let expected = json!({"items": [1, 99]});
830 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
831
832 let DiffNode::Container {
834 segment,
835 children,
836 omitted_count,
837 ..
838 } = &tree.roots[0]
839 else {
840 panic!("expected Container for items key");
841 };
842 assert!(matches!(segment, PathSegment::Key(k) if k == "items"));
843 assert_eq!(*omitted_count, 3);
844 assert_eq!(children.len(), 1);
846 assert!(matches!(
847 &children[0],
848 DiffNode::Leaf {
849 segment: PathSegment::Index(1),
850 kind: DiffKind::Changed { .. },
851 }
852 ));
853 }
854
855 fn config_with_key_at(path: &str, key: &str) -> DiffConfig {
856 use crate::config::{ArrayMatchConfig, ArrayMatchMode, MatchConfig};
857 DiffConfig::new().with_match_config(MatchConfig::new().with_config_at(
858 path,
859 ArrayMatchConfig::new(ArrayMatchMode::Key(key.to_owned())),
860 ))
861 }
862
863 #[test]
864 fn key_based_array_equal() {
865 let config = config_with_key_at("items", "name");
866 let actual = json!({"items": [{"name": "a", "val": 1}, {"name": "b", "val": 2}]});
867 let expected = json!({"items": [{"name": "a", "val": 1}]});
868 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
869 assert!(tree.is_empty());
870 }
871
872 #[test]
873 fn key_based_array_changed() {
874 let config = config_with_key_at("items", "name");
875 let actual = json!({"items": [{"name": "FOO", "value": "bar"}]});
876 let expected = json!({"items": [{"name": "FOO", "value": "baz"}]});
877 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
878
879 assert_eq!(tree.roots.len(), 1);
881 let DiffNode::Container { children, .. } = &tree.roots[0] else {
882 panic!("expected Container for items");
883 };
884 assert_eq!(children.len(), 1);
885 let DiffNode::Container {
886 segment, children, ..
887 } = &children[0]
888 else {
889 panic!("expected Container for named element");
890 };
891 assert!(
892 matches!(segment, PathSegment::NamedElement { match_key, match_value }
893 if match_key == "name" && match_value == "FOO"
894 )
895 );
896 assert_eq!(children.len(), 1);
897 let DiffNode::Leaf { segment, kind } = &children[0] else {
898 panic!("expected Leaf");
899 };
900 assert!(matches!(segment, PathSegment::Key(k) if k == "value"));
901 assert!(matches!(kind, DiffKind::Changed { actual, expected }
902 if actual == &json!("bar") && expected == &json!("baz")
903 ));
904 }
905
906 #[test]
907 fn key_based_array_missing_element() {
908 let config = config_with_key_at("items", "name");
909 let actual = json!({"items": [{"name": "a"}]});
910 let expected = json!({"items": [{"name": "missing"}]});
911 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
912
913 let DiffNode::Container { children, .. } = &tree.roots[0] else {
914 panic!("expected Container for items");
915 };
916 assert_eq!(children.len(), 1);
917 let DiffNode::Leaf { segment, kind } = &children[0] else {
918 panic!("expected Leaf");
919 };
920 assert!(matches!(segment, PathSegment::Unmatched));
921 assert!(matches!(kind, DiffKind::Missing { .. }));
922 }
923
924 #[test]
925 fn key_based_array_omitted_count() {
926 let config = config_with_key_at("items", "name");
927 let actual = json!({"items": [{"name": "a"}, {"name": "b"}, {"name": "c"}]});
928 let expected = json!({"items": [{"name": "b"}]});
929 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
930
931 assert!(tree.is_empty());
935
936 let actual = json!({"items": [{"name": "a"}, {"name": "b", "x": 1}, {"name": "c"}]});
938 let expected = json!({"items": [{"name": "b", "x": 99}]});
939 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
940
941 let DiffNode::Container {
942 children: _children,
943 omitted_count,
944 ..
945 } = &tree.roots[0]
946 else {
947 panic!("expected Container for items");
948 };
949 assert_eq!(*omitted_count, 2);
951 }
952
953 fn config_with_contains_at(path: &str) -> DiffConfig {
954 use crate::config::{ArrayMatchConfig, ArrayMatchMode, MatchConfig};
955 DiffConfig::new().with_match_config(
956 MatchConfig::new()
957 .with_config_at(path, ArrayMatchConfig::new(ArrayMatchMode::Contains)),
958 )
959 }
960
961 #[test]
962 fn contains_array_scalar_equal() {
963 let config = config_with_contains_at("items");
964 let actual = json!({"items": ["a", "b", "c"]});
965 let expected = json!({"items": ["b"]});
966 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
967 assert!(tree.is_empty());
968 }
969
970 #[test]
971 fn contains_array_object_subset_equal() {
972 let config = config_with_contains_at("items");
973 let actual = json!({"items": [{"a": 1, "b": 2}, {"c": 3}]});
974 let expected = json!({"items": [{"a": 1}]});
975 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
976 assert!(tree.is_empty());
977 }
978
979 #[test]
980 fn contains_array_missing_element() {
981 let config = config_with_contains_at("items");
982 let actual = json!({"items": ["a", "b"]});
983 let expected = json!({"items": ["x"]});
984 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
985
986 let DiffNode::Container { children, .. } = &tree.roots[0] else {
987 panic!("expected Container for items");
988 };
989 assert_eq!(children.len(), 1);
990 let DiffNode::Leaf { segment, kind } = &children[0] else {
991 panic!("expected Leaf");
992 };
993 assert!(matches!(segment, PathSegment::Unmatched));
994 assert!(matches!(kind, DiffKind::Missing { expected } if expected == &json!("x")));
995 }
996
997 #[test]
998 fn contains_array_match_not_at_first_position() {
999 let config = config_with_contains_at("items");
1000 let actual = json!({"items": [{"a": 1}, {"b": 1}]});
1003 let expected = json!({"items": [{"b": 1}]});
1004 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
1005 assert!(tree.is_empty());
1006 }
1007
1008 #[test]
1009 fn contains_array_omitted_count() {
1010 let config = config_with_contains_at("items");
1011 let actual = json!({"items": ["a", "b", "c"]});
1012 let expected = json!({"items": ["x"]});
1013 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
1014
1015 let DiffNode::Container { omitted_count, .. } = &tree.roots[0] else {
1016 panic!("expected Container for items");
1017 };
1018 assert_eq!(*omitted_count, 3);
1020 }
1021
1022 fn config_with_key_and_strategy(
1023 path: &str,
1024 key: &str,
1025 strategy: AmbiguousMatchStrategy,
1026 ) -> DiffConfig {
1027 use crate::config::{ArrayMatchConfig, ArrayMatchMode, MatchConfig};
1028 DiffConfig::new().with_match_config(
1029 MatchConfig::new().with_config_at(
1030 path,
1031 ArrayMatchConfig::new(ArrayMatchMode::Key(key.to_owned()))
1032 .with_ambiguous_strategy(strategy),
1033 ),
1034 )
1035 }
1036
1037 fn config_with_contains_and_strategy(
1038 path: &str,
1039 strategy: AmbiguousMatchStrategy,
1040 ) -> DiffConfig {
1041 use crate::config::{ArrayMatchConfig, ArrayMatchMode, MatchConfig};
1042 DiffConfig::new().with_match_config(MatchConfig::new().with_config_at(
1043 path,
1044 ArrayMatchConfig::new(ArrayMatchMode::Contains).with_ambiguous_strategy(strategy),
1045 ))
1046 }
1047
1048 #[test]
1049 fn ambiguous_key_best_match_picks_fewest_diffs() {
1050 let config =
1054 config_with_key_and_strategy("items", "name", AmbiguousMatchStrategy::BestMatch);
1055 let actual = json!({"items": [
1056 {"name": "FOO", "value": "wrong"},
1057 {"name": "FOO", "value": "almost"}
1058 ]});
1059 let expected = json!({"items": [{"name": "FOO", "value": "almost"}]});
1060 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
1061
1062 assert!(tree.is_empty());
1064 }
1065
1066 #[test]
1067 fn ambiguous_key_best_match_with_diffs() {
1068 let config =
1069 config_with_key_and_strategy("items", "name", AmbiguousMatchStrategy::BestMatch);
1070 let actual = json!({"items": [
1072 {"name": "FOO", "a": 1, "b": 2},
1073 {"name": "FOO", "a": 99, "b": 99}
1074 ]});
1075 let expected = json!({"items": [{"name": "FOO", "a": 1, "b": 99}]});
1076 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
1077
1078 assert!(!tree.is_empty());
1081 let DiffNode::Container { children, .. } = &tree.roots[0] else {
1082 panic!("expected Container for items");
1083 };
1084 assert_eq!(children.len(), 1);
1085 }
1086
1087 #[test]
1088 fn ambiguous_contains_best_match() {
1089 let config = config_with_contains_and_strategy("items", AmbiguousMatchStrategy::BestMatch);
1092 let actual = json!({"items": [
1093 {"a": 1, "b": 2},
1094 {"a": 1, "c": 3}
1095 ]});
1096 let expected = json!({"items": [{"a": 1}]});
1097 let tree = diff(&actual, &expected, &config).expect("diff with valid inputs");
1098 assert!(tree.is_empty());
1099 }
1100
1101 #[test]
1105 fn null_vs_empty_array() {
1106 let actual = json!({"foo": null});
1107 let expected = json!({"foo": []});
1108 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
1109
1110 assert_eq!(tree.roots.len(), 1);
1111 let DiffNode::Leaf { kind, .. } = &tree.roots[0] else {
1112 panic!("expected Leaf");
1113 };
1114 assert!(matches!(
1115 kind,
1116 DiffKind::TypeMismatch {
1117 actual_type: "null",
1118 expected_type: "array",
1119 ..
1120 }
1121 ));
1122 }
1123
1124 #[test]
1125 fn null_vs_empty_object() {
1126 let actual = json!({"bar": null});
1127 let expected = json!({"bar": {}});
1128 let tree = diff(&actual, &expected, &default_config()).expect("diff with valid inputs");
1129
1130 assert_eq!(tree.roots.len(), 1);
1131 let DiffNode::Leaf { kind, .. } = &tree.roots[0] else {
1132 panic!("expected Leaf");
1133 };
1134 assert!(matches!(
1135 kind,
1136 DiffKind::TypeMismatch {
1137 actual_type: "null",
1138 expected_type: "object",
1139 ..
1140 }
1141 ));
1142 }
1143
1144 #[test]
1147 fn missing_key_field_returns_error() {
1148 let config = config_with_key_at("items", "name");
1149 let actual = json!({"items": [{"name": "a"}]});
1150 let expected = json!({"items": [{"value": "foo"}]});
1152 let result = diff(&actual, &expected, &config);
1153
1154 let Err(err) = result else {
1155 panic!("expected an error");
1156 };
1157 assert!(matches!(err, Error::MissingKeyField { .. }));
1158 assert!(err.to_string().contains("missing the key field `name`"));
1159 }
1160
1161 #[test]
1162 fn strict_ambiguous_key_match_returns_error() {
1163 let config = config_with_key_and_strategy("items", "name", AmbiguousMatchStrategy::Strict);
1164 let actual = json!({"items": [
1165 {"name": "FOO", "value": "a"},
1166 {"name": "FOO", "value": "b"}
1167 ]});
1168 let expected = json!({"items": [{"name": "FOO", "value": "a"}]});
1169 let result = diff(&actual, &expected, &config);
1170
1171 let Err(err) = result else {
1172 panic!("expected an error");
1173 };
1174 assert!(matches!(err, Error::AmbiguousMatch { count: 2, .. }));
1175 }
1176
1177 #[test]
1178 fn strict_ambiguous_contains_match_returns_error() {
1179 let config = config_with_contains_and_strategy("items", AmbiguousMatchStrategy::Strict);
1180 let actual = json!({"items": [
1181 {"a": 1, "b": 2},
1182 {"a": 1, "c": 3}
1183 ]});
1184 let expected = json!({"items": [{"a": 1}]});
1185 let result = diff(&actual, &expected, &config);
1186
1187 let Err(err) = result else {
1188 panic!("expected an error");
1189 };
1190 assert!(matches!(err, Error::AmbiguousMatch { count: 2, .. }));
1191 }
1192}