1use super::engine::AssertionResult;
2use apif_ast::ast::InlineOptions;
3use serde_json::Value;
4
5pub struct JsonComparator;
6
7impl JsonComparator {
8 pub fn compare(
9 actual: &Value,
10 expected: &Value,
11 options: &InlineOptions,
12 ) -> Vec<AssertionResult> {
13 let mut results = Vec::new();
14
15 if options.redact.is_empty() {
16 Self::compare_recursive(actual, expected, "$", options, &mut results);
17 } else {
18 let mut actual_redacted = actual.clone();
19 Self::redact_value(&mut actual_redacted, &options.redact);
20 Self::compare_recursive(&actual_redacted, expected, "$", options, &mut results);
21 }
22
23 results
24 }
25
26 fn redact_value(value: &mut Value, fields: &[String]) {
27 match value {
28 Value::Object(map) => {
29 for field in fields {
30 map.remove(field);
31 }
32 for (_, v) in map.iter_mut() {
33 Self::redact_value(v, fields);
34 }
35 }
36 Value::Array(arr) => {
37 for v in arr.iter_mut() {
38 Self::redact_value(v, fields);
39 }
40 }
41 _ => {}
42 }
43 }
44
45 fn compare_recursive(
46 actual: &Value,
47 expected: &Value,
48 path: &str,
49 options: &InlineOptions,
50 results: &mut Vec<AssertionResult>,
51 ) {
52 if let Value::String(s) = expected
53 && s == "*"
54 {
55 return; }
57
58 match (actual, expected) {
63 (Value::Object(act_map), Value::Object(exp_map)) => {
64 for (k, exp_val) in exp_map {
66 let new_path = format!("{}.{}", path, k);
67
68 if let Some(act_val) = act_map.get(k) {
69 Self::compare_recursive(act_val, exp_val, &new_path, options, results);
70 } else {
71 if !is_protojson_default_value(exp_val) {
74 results.push(AssertionResult::fail(format!(
75 "Key '{}' missing in actual response",
76 new_path
77 )));
78 }
79 }
80 }
81
82 if !options.partial {
83 for k in act_map.keys() {
84 if !exp_map.contains_key(k) {
85 results.push(AssertionResult::fail(format!(
86 "Unexpected key '{}.{}' in actual response",
87 path, k
88 )));
89 }
90 }
91 }
92 }
93 (Value::Array(act_arr), Value::Array(exp_arr)) => {
94 if !options.partial && act_arr.len() != exp_arr.len() {
95 results.push(AssertionResult::fail_with_diff(
96 format!(
97 "Array length mismatch at '{}': expected {}, got {}",
98 path,
99 exp_arr.len(),
100 act_arr.len()
101 ),
102 format!("length: {}", exp_arr.len()),
103 format!("length: {}", act_arr.len()),
104 ));
105 }
106
107 if options.unordered_arrays {
108 let mut matched_actual_indices = std::collections::HashSet::new();
113 let mut hash_to_indices: std::collections::HashMap<u64, Vec<usize>> =
114 std::collections::HashMap::new();
115
116 for (i, act_item) in act_arr.iter().enumerate() {
117 let hash = Self::hash_value(act_item);
118 hash_to_indices.entry(hash).or_default().push(i);
119 }
120
121 for exp_item in exp_arr {
122 let exp_hash = Self::hash_value(exp_item);
123 let mut found = false;
124
125 if let Some(indices) = hash_to_indices.get(&exp_hash) {
127 for &idx in indices.iter() {
128 if matched_actual_indices.contains(&idx) {
129 continue;
130 }
131
132 let mut temp_results = Vec::new();
133 Self::compare_recursive(
134 &act_arr[idx],
135 exp_item,
136 &format!("{}[{}]", path, idx),
137 options,
138 &mut temp_results,
139 );
140
141 if temp_results.is_empty() {
142 matched_actual_indices.insert(idx);
143 found = true;
144 break;
145 }
146 }
147 }
148
149 if !found {
154 for (idx, act_item) in act_arr.iter().enumerate() {
155 if matched_actual_indices.contains(&idx) {
156 continue;
157 }
158
159 let mut temp_results = Vec::new();
160 Self::compare_recursive(
161 act_item,
162 exp_item,
163 &format!("{}[{}]", path, idx),
164 options,
165 &mut temp_results,
166 );
167
168 if temp_results.is_empty() {
169 matched_actual_indices.insert(idx);
170 found = true;
171 break;
172 }
173 }
174 }
175
176 if !found {
179 results.push(AssertionResult::fail(format!(
180 "Missing expected item in unordered array at '{}': {:?}",
181 path, exp_item
182 )));
183 }
184 }
185
186 if !options.partial && matched_actual_indices.len() < act_arr.len() {
187 results.push(AssertionResult::fail(format!(
188 "Unordered array at '{}' has {} extra items",
189 path,
190 act_arr.len() - matched_actual_indices.len()
191 )));
192 }
193
194 return;
195 }
196
197 let len = std::cmp::min(act_arr.len(), exp_arr.len());
202 for i in 0..len {
203 let new_path = format!("{}[{}]", path, i);
204 Self::compare_recursive(&act_arr[i], &exp_arr[i], &new_path, options, results);
205 }
206
207 if exp_arr.len() > act_arr.len() {
209 for i in act_arr.len()..exp_arr.len() {
210 results.push(AssertionResult::fail(format!(
211 "Missing array item at '{}[{}]'",
212 path, i
213 )));
214 }
215 }
216 }
217 (Value::String(a), Value::String(e)) => {
218 if a != e {
219 results.push(AssertionResult::fail_with_diff(
220 format!(
221 "Value mismatch at '{}': expected \"{}\", got \"{}\"",
222 path, e, a
223 ),
224 e,
225 a,
226 ));
227 }
228 }
229 (Value::Number(a), Value::Number(e)) => {
230 if let Some(tol) = options.tolerance
231 && let (Some(af), Some(ef)) = (a.as_f64(), e.as_f64())
232 {
233 if (af - ef).abs() > tol {
234 results.push(AssertionResult::fail_with_diff(
235 format!(
236 "Value mismatch at '{}': expected {} (tolerance {}), got {}",
237 path, ef, tol, af
238 ),
239 format!("{} (±{})", ef, tol),
240 format!("{}", af),
241 ));
242 }
243 return;
244 }
245
246 let a_is_int = a.is_i64() || a.is_u64();
249 let e_is_int = e.is_i64() || e.is_u64();
250 if a_is_int && e_is_int {
251 let equal = if let (Some(ai), Some(ei)) = (a.as_i64(), e.as_i64()) {
252 ai == ei
253 } else if let (Some(au), Some(eu)) = (a.as_u64(), e.as_u64()) {
254 au == eu
255 } else {
256 false
258 };
259 if equal {
260 return;
261 }
262 } else if let (Some(af), Some(ef)) = (a.as_f64(), e.as_f64()) {
263 let scale = af.abs().max(ef.abs());
268 if af == ef || (af - ef).abs() <= 1e-9 * scale {
269 return;
270 }
271 }
272
273 results.push(AssertionResult::fail_with_diff(
274 format!("Value mismatch at '{}': expected {}, got {}", path, e, a),
275 format!("{}", e),
276 format!("{}", a),
277 ));
278 }
279 (Value::Bool(a), Value::Bool(e)) => {
280 if a != e {
281 results.push(AssertionResult::fail_with_diff(
282 format!("Value mismatch at '{}': expected {}, got {}", path, e, a),
283 format!("{}", e),
284 format!("{}", a),
285 ));
286 }
287 }
288 (Value::Null, Value::Null) => {}
289 _ => {
290 results.push(AssertionResult::fail_with_diff(
291 format!(
292 "Type mismatch at '{}': expected {:?}, got {:?}",
293 path, expected, actual
294 ),
295 format!("{:?}", expected),
296 format!("{:?}", actual),
297 ));
298 }
299 }
300 }
301
302 fn hash_value(value: &Value) -> u64 {
305 use std::collections::hash_map::DefaultHasher;
306 use std::hash::{Hash, Hasher};
307
308 let mut hasher = DefaultHasher::new();
309
310 match value {
311 Value::Null => 0u8.hash(&mut hasher),
312 Value::Bool(b) => (1u8, b).hash(&mut hasher),
313 Value::Number(n) => {
314 (2u8, n.as_i64(), n.as_u64(), n.as_f64().map(|f| f.to_bits())).hash(&mut hasher)
315 }
316 Value::String(s) => (3u8, s).hash(&mut hasher),
317 Value::Array(arr) => {
318 (4u8, arr.len()).hash(&mut hasher);
319 for item in arr {
320 Self::hash_value(item).hash(&mut hasher);
321 }
322 }
323 Value::Object(obj) => {
324 (5u8, obj.len()).hash(&mut hasher);
325 let mut keys: Vec<_> = obj.keys().collect();
327 keys.sort();
328 for key in keys {
329 key.hash(&mut hasher);
330 Self::hash_value(&obj[key]).hash(&mut hasher);
331 }
332 }
333 }
334
335 hasher.finish()
336 }
337}
338
339fn is_protojson_default_value(value: &Value) -> bool {
340 match value {
341 Value::Null => true,
342 Value::Bool(b) => !*b,
343 Value::Number(n) => {
344 if let Some(i) = n.as_i64() {
345 i == 0
346 } else if let Some(u) = n.as_u64() {
347 u == 0
348 } else if let Some(f) = n.as_f64() {
349 f == 0.0
350 } else {
351 false
352 }
353 }
354 Value::String(s) => s.is_empty(),
355 Value::Array(arr) => arr.is_empty(),
356 Value::Object(map) => map.is_empty(),
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use serde_json::json;
364
365 #[test]
366 fn test_compare_exact_match() {
367 let actual = json!({"foo": "bar", "num": 1});
368 let expected = json!({"foo": "bar", "num": 1});
369 let options = InlineOptions::default();
370
371 let results = JsonComparator::compare(&actual, &expected, &options);
372 assert!(results.is_empty());
373 }
374
375 #[test]
376 fn test_compare_numeric_representation_match() {
377 let actual = json!({"result": 60.0});
378 let expected = json!({"result": 60});
379 let options = InlineOptions::default();
380
381 let results = JsonComparator::compare(&actual, &expected, &options);
382 assert!(results.is_empty());
383 }
384
385 #[test]
386 fn test_compare_mismatch() {
387 let actual = json!({"foo": "bar"});
388 let expected = json!({"foo": "baz"});
389 let options = InlineOptions::default();
390
391 let results = JsonComparator::compare(&actual, &expected, &options);
392 assert_eq!(results.len(), 1);
393 if let AssertionResult::Fail { message: msg, .. } = &results[0] {
394 assert!(msg.contains("Value mismatch"));
395 } else {
396 panic!("Expected Fail");
397 }
398 }
399
400 #[test]
401 fn test_compare_partial_object() {
402 let actual = json!({"foo": "bar", "extra": "field"});
403 let expected = json!({"foo": "bar"});
404
405 let options = InlineOptions::default();
407 let results = JsonComparator::compare(&actual, &expected, &options);
408 assert_eq!(results.len(), 1);
409
410 let options = InlineOptions {
412 partial: true,
413 ..Default::default()
414 };
415 let results = JsonComparator::compare(&actual, &expected, &options);
416 assert!(results.is_empty());
417 }
418
419 #[test]
420 fn test_wildcard() {
421 let actual = json!({"id": 12345, "name": "test"});
422 let expected = json!({"id": "*", "name": "test"});
423 let options = InlineOptions::default();
424
425 let results = JsonComparator::compare(&actual, &expected, &options);
426 assert!(results.is_empty());
427 }
428
429 #[test]
430 fn test_redact() {
431 let actual = json!({"id": 12345, "secret": "hidden", "name": "test"});
432 let expected = json!({"id": 12345, "name": "test"});
435
436 let options = InlineOptions {
437 redact: vec!["secret".to_string()],
438 ..Default::default()
439 };
440
441 let results = JsonComparator::compare(&actual, &expected, &options);
442 assert!(results.is_empty());
443 }
444
445 #[test]
446 fn test_tolerance() {
447 let actual = json!({"val": 10.005});
448 let expected = json!({"val": 10.0});
449
450 let mut options = InlineOptions {
451 tolerance: Some(0.01),
452 ..Default::default()
453 };
454
455 let results = JsonComparator::compare(&actual, &expected, &options);
456 assert!(results.is_empty());
457
458 options.tolerance = Some(0.001);
459 let results = JsonComparator::compare(&actual, &expected, &options);
460 assert_eq!(results.len(), 1);
461 }
462
463 #[test]
464 fn test_compare_empty_objects() {
465 let actual = json!({});
466 let expected = json!({});
467 let options = InlineOptions::default();
468
469 let results = JsonComparator::compare(&actual, &expected, &options);
470 assert!(results.is_empty());
471 }
472
473 #[test]
474 fn test_compare_empty_arrays() {
475 let actual = json!([]);
476 let expected = json!([]);
477 let options = InlineOptions::default();
478
479 let results = JsonComparator::compare(&actual, &expected, &options);
480 assert!(results.is_empty());
481 }
482
483 #[test]
484 fn test_compare_null_values() {
485 let actual = json!({"val": null});
486 let expected = json!({"val": null});
487 let options = InlineOptions::default();
488
489 let results = JsonComparator::compare(&actual, &expected, &options);
490 assert!(results.is_empty());
491 }
492
493 #[test]
494 fn test_compare_null_mismatch() {
495 let actual = json!({"val": "not null"});
496 let expected = json!({"val": null});
497 let options = InlineOptions::default();
498
499 let results = JsonComparator::compare(&actual, &expected, &options);
500 assert_eq!(results.len(), 1);
501 }
502
503 #[test]
504 fn test_compare_boolean_values() {
505 let actual = json!({"active": true, "deleted": false});
506 let expected = json!({"active": true, "deleted": false});
507 let options = InlineOptions::default();
508
509 let results = JsonComparator::compare(&actual, &expected, &options);
510 assert!(results.is_empty());
511 }
512
513 #[test]
514 fn test_compare_boolean_mismatch() {
515 let actual = json!({"active": true});
516 let expected = json!({"active": false});
517 let options = InlineOptions::default();
518
519 let results = JsonComparator::compare(&actual, &expected, &options);
520 assert_eq!(results.len(), 1);
521 }
522
523 #[test]
524 fn test_compare_nested_objects() {
525 let actual = json!({"user": {"name": "test", "age": 25}});
526 let expected = json!({"user": {"name": "test", "age": 25}});
527 let options = InlineOptions::default();
528
529 let results = JsonComparator::compare(&actual, &expected, &options);
530 assert!(results.is_empty());
531 }
532
533 #[test]
534 fn test_compare_nested_mismatch() {
535 let actual = json!({"user": {"name": "test"}});
536 let expected = json!({"user": {"name": "other"}});
537 let options = InlineOptions::default();
538
539 let results = JsonComparator::compare(&actual, &expected, &options);
540 assert_eq!(results.len(), 1);
541 }
542
543 #[test]
544 fn test_compare_arrays_different_lengths() {
545 let actual = json!([1, 2, 3]);
546 let expected = json!([1, 2]);
547 let options = InlineOptions::default();
548
549 let results = JsonComparator::compare(&actual, &expected, &options);
550 assert!(!results.is_empty());
551 }
552
553 #[test]
554 fn test_compare_arrays_with_objects() {
555 let actual = json!([{"id": 1}, {"id": 2}]);
556 let expected = json!([{"id": 1}, {"id": 2}]);
557 let options = InlineOptions::default();
558
559 let results = JsonComparator::compare(&actual, &expected, &options);
560 assert!(results.is_empty());
561 }
562
563 #[test]
564 fn test_compare_partial_nested_object() {
565 let actual = json!({"user": {"name": "test", "age": 25, "extra": "field"}});
566 let expected = json!({"user": {"name": "test"}});
567 let options = InlineOptions {
568 partial: true,
569 ..Default::default()
570 };
571
572 let results = JsonComparator::compare(&actual, &expected, &options);
573 assert!(results.is_empty());
574 }
575
576 #[test]
577 fn test_redact_nested() {
578 let actual = json!({"user": {"password": "secret", "name": "test"}});
579 let expected = json!({"user": {"name": "test"}});
580
581 let options = InlineOptions {
582 redact: vec!["password".to_string()],
583 ..Default::default()
584 };
585
586 let results = JsonComparator::compare(&actual, &expected, &options);
587 assert!(results.is_empty());
588 }
589
590 #[test]
591 fn test_is_protojson_default_value() {
592 assert!(is_protojson_default_value(&Value::String("".to_string())));
593 assert!(is_protojson_default_value(&Value::Number(0.into())));
594 assert!(is_protojson_default_value(&Value::Bool(false)));
595 assert!(is_protojson_default_value(&Value::Array(vec![])));
596 assert!(is_protojson_default_value(&Value::Object(
597 serde_json::Map::new()
598 )));
599 assert!(!is_protojson_default_value(&Value::String(
600 "not empty".to_string()
601 )));
602 assert!(!is_protojson_default_value(&Value::Number(1.into())));
603 assert!(!is_protojson_default_value(&Value::Bool(true)));
604 }
605
606 #[test]
607 fn test_unordered_arrays_optimized() {
608 let actual = json!([3, 1, 2]);
609 let expected = json!([1, 2, 3]);
610 let options = InlineOptions {
611 unordered_arrays: true,
612 ..Default::default()
613 };
614
615 let results = JsonComparator::compare(&actual, &expected, &options);
616 assert!(results.is_empty());
617 }
618
619 #[test]
620 fn test_unordered_arrays_with_objects() {
621 let actual = json!([
622 {"id": 3, "name": "c"},
623 {"id": 1, "name": "a"},
624 {"id": 2, "name": "b"}
625 ]);
626 let expected = json!([
627 {"id": 1, "name": "a"},
628 {"id": 2, "name": "b"},
629 {"id": 3, "name": "c"}
630 ]);
631 let options = InlineOptions {
632 unordered_arrays: true,
633 ..Default::default()
634 };
635
636 let results = JsonComparator::compare(&actual, &expected, &options);
637 assert!(results.is_empty());
638 }
639
640 #[test]
641 fn test_unordered_arrays_missing_item() {
642 let actual = json!([1, 2]);
643 let expected = json!([1, 2, 3]);
644 let options = InlineOptions {
645 unordered_arrays: true,
646 ..Default::default()
647 };
648
649 let results = JsonComparator::compare(&actual, &expected, &options);
650 assert!(!results.is_empty());
651 }
652
653 #[test]
654 fn test_unordered_arrays_extra_item() {
655 let actual = json!([1, 2, 3, 4]);
656 let expected = json!([1, 2, 3]);
657 let options = InlineOptions {
658 unordered_arrays: true,
659 ..Default::default()
660 };
661
662 let results = JsonComparator::compare(&actual, &expected, &options);
663 assert!(!results.is_empty());
664 }
665
666 #[test]
667 fn test_unordered_arrays_partial() {
668 let actual = json!([1, 2, 3, 4]);
669 let expected = json!([1, 3]);
670 let options = InlineOptions {
671 unordered_arrays: true,
672 partial: true,
673 ..Default::default()
674 };
675
676 let results = JsonComparator::compare(&actual, &expected, &options);
677 assert!(results.is_empty());
678
679 let expected = json!([1, 999]);
682 let results = JsonComparator::compare(&actual, &expected, &options);
683 assert!(
684 !results.is_empty(),
685 "missing expected item must fail even in partial mode"
686 );
687 }
688
689 #[test]
690 fn test_unordered_arrays_partial_missing_item_fails() {
691 let actual = json!([1, 2, 3]);
694 let expected = json!([999]);
695 let options = InlineOptions {
696 unordered_arrays: true,
697 partial: true,
698 ..Default::default()
699 };
700
701 let results = JsonComparator::compare(&actual, &expected, &options);
702 assert_eq!(results.len(), 1);
703 if let AssertionResult::Fail { message, .. } = &results[0] {
704 assert!(message.contains("Missing expected item"));
705 } else {
706 panic!("Expected Fail");
707 }
708 }
709
710 #[test]
711 fn test_unordered_arrays_numeric_representation() {
712 let actual = json!([60.0]);
715 let expected = json!([60]);
716 let options = InlineOptions {
717 unordered_arrays: true,
718 ..Default::default()
719 };
720
721 let results = JsonComparator::compare(&actual, &expected, &options);
722 assert!(results.is_empty(), "got: {:?}", results);
723 }
724
725 #[test]
726 fn test_unordered_arrays_wildcard() {
727 let actual = json!(["abc", 1]);
729 let expected = json!([1, "*"]);
730 let options = InlineOptions {
731 unordered_arrays: true,
732 ..Default::default()
733 };
734
735 let results = JsonComparator::compare(&actual, &expected, &options);
736 assert!(results.is_empty(), "got: {:?}", results);
737 }
738
739 #[test]
740 fn test_unordered_arrays_tolerance() {
741 let actual = json!([10.005, 20.0]);
744 let expected = json!([20.0, 10.0]);
745 let options = InlineOptions {
746 unordered_arrays: true,
747 tolerance: Some(0.01),
748 ..Default::default()
749 };
750
751 let results = JsonComparator::compare(&actual, &expected, &options);
752 assert!(results.is_empty(), "got: {:?}", results);
753 }
754
755 #[test]
756 fn test_unordered_arrays_partial_objects() {
757 let actual = json!([{"id": 2, "extra": "y"}, {"id": 1, "extra": "x"}]);
760 let expected = json!([{"id": 1}, {"id": 2}]);
761 let options = InlineOptions {
762 unordered_arrays: true,
763 partial: true,
764 ..Default::default()
765 };
766
767 let results = JsonComparator::compare(&actual, &expected, &options);
768 assert!(results.is_empty(), "got: {:?}", results);
769 }
770
771 #[test]
772 fn test_compare_large_integers_exact() {
773 let actual = json!({"id": 9223372036854775807i64});
776 let expected = json!({"id": 9223372036854775806i64});
777 let options = InlineOptions::default();
778
779 let results = JsonComparator::compare(&actual, &expected, &options);
780 assert_eq!(results.len(), 1);
781
782 let expected = json!({"id": 9223372036854775807i64});
783 let results = JsonComparator::compare(&actual, &expected, &options);
784 assert!(results.is_empty());
785 }
786
787 #[test]
788 fn test_compare_small_floats_not_equal() {
789 let actual = json!({"val": 0.0000001});
791 let expected = json!({"val": 0.0000009});
792 let options = InlineOptions::default();
793
794 let results = JsonComparator::compare(&actual, &expected, &options);
795 assert_eq!(results.len(), 1);
796 }
797
798 #[test]
799 fn test_compare_float_rounding_noise_equal() {
800 let actual = json!({"val": 0.1 + 0.2});
802 let expected = json!({"val": 0.3});
803 let options = InlineOptions::default();
804
805 let results = JsonComparator::compare(&actual, &expected, &options);
806 assert!(results.is_empty(), "got: {:?}", results);
807 }
808
809 #[test]
810 fn test_compare_mixed_sign_large_integers() {
811 let actual = json!({"val": -1i64});
813 let expected = json!({"val": 18446744073709551615u64});
814 let options = InlineOptions::default();
815
816 let results = JsonComparator::compare(&actual, &expected, &options);
817 assert_eq!(results.len(), 1);
818 }
819
820 #[test]
821 fn test_hash_value_consistency() {
822 let value1 = json!({"id": 1, "name": "test"});
823 let value2 = json!({"id": 1, "name": "test"});
824 let value3 = json!({"name": "test", "id": 1}); let hash1 = JsonComparator::hash_value(&value1);
828 let hash2 = JsonComparator::hash_value(&value2);
829 let hash3 = JsonComparator::hash_value(&value3);
830
831 assert_eq!(hash1, hash2);
832 assert_eq!(hash2, hash3);
833 }
834}