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() {
17 Self::compare_recursive(actual, expected, "$", options, &mut results);
18 } else {
19 let mut actual_redacted = actual.clone();
20 Self::redact_value(&mut actual_redacted, &options.redact);
21 Self::compare_recursive(&actual_redacted, expected, "$", options, &mut results);
22 }
23
24 results
25 }
26
27 fn redact_value(value: &mut Value, fields: &[String]) {
28 match value {
29 Value::Object(map) => {
30 for field in fields {
31 map.remove(field);
32 }
33 for (_, v) in map.iter_mut() {
34 Self::redact_value(v, fields);
35 }
36 }
37 Value::Array(arr) => {
38 for v in arr.iter_mut() {
39 Self::redact_value(v, fields);
40 }
41 }
42 _ => {}
43 }
44 }
45
46 fn compare_recursive(
47 actual: &Value,
48 expected: &Value,
49 path: &str,
50 options: &InlineOptions,
51 results: &mut Vec<AssertionResult>,
52 ) {
53 if let Value::String(s) = expected
55 && s == "*"
56 {
57 return; }
59
60 match (actual, expected) {
66 (Value::Object(act_map), Value::Object(exp_map)) => {
67 for (k, exp_val) in exp_map {
69 let new_path = format!("{}.{}", path, k);
70
71 if let Some(act_val) = act_map.get(k) {
72 Self::compare_recursive(act_val, exp_val, &new_path, options, results);
73 } else {
74 if !is_protojson_default_value(exp_val) {
77 results.push(AssertionResult::fail(format!(
78 "Key '{}' missing in actual response",
79 new_path
80 )));
81 }
82 }
83 }
84
85 if !options.partial {
87 for k in act_map.keys() {
88 if !exp_map.contains_key(k) {
89 results.push(AssertionResult::fail(format!(
90 "Unexpected key '{}.{}' in actual response",
91 path, k
92 )));
93 }
94 }
95 }
96 }
97 (Value::Array(act_arr), Value::Array(exp_arr)) => {
98 if !options.partial && act_arr.len() != exp_arr.len() {
100 results.push(AssertionResult::fail_with_diff(
101 format!(
102 "Array length mismatch at '{}': expected {}, got {}",
103 path,
104 exp_arr.len(),
105 act_arr.len()
106 ),
107 format!("length: {}", exp_arr.len()),
108 format!("length: {}", act_arr.len()),
109 ));
110 }
111
112 if options.unordered_arrays {
114 let mut matched_actual_indices = std::collections::HashSet::new();
119 let mut hash_to_indices: std::collections::HashMap<u64, Vec<usize>> =
120 std::collections::HashMap::new();
121
122 for (i, act_item) in act_arr.iter().enumerate() {
124 let hash = Self::hash_value(act_item);
125 hash_to_indices.entry(hash).or_default().push(i);
126 }
127
128 for exp_item in exp_arr {
130 let exp_hash = Self::hash_value(exp_item);
131 let mut found = false;
132
133 if let Some(indices) = hash_to_indices.get_mut(&exp_hash) {
134 for &idx in indices.iter() {
135 if matched_actual_indices.contains(&idx) {
136 continue;
137 }
138
139 let mut temp_results = Vec::new();
141 Self::compare_recursive(
142 &act_arr[idx],
143 exp_item,
144 &format!("{}[{}]", path, idx),
145 options,
146 &mut temp_results,
147 );
148
149 if temp_results.is_empty() {
150 matched_actual_indices.insert(idx);
151 found = true;
152 break;
153 }
154 }
155 }
156
157 if !found && !options.partial {
158 results.push(AssertionResult::fail(format!(
159 "Missing expected item in unordered array at '{}': {:?}",
160 path, exp_item
161 )));
162 }
163 }
164
165 if !options.partial && matched_actual_indices.len() < act_arr.len() {
167 results.push(AssertionResult::fail(format!(
168 "Unordered array at '{}' has {} extra items",
169 path,
170 act_arr.len() - matched_actual_indices.len()
171 )));
172 }
173
174 return;
175 }
176
177 let len = std::cmp::min(act_arr.len(), exp_arr.len());
183 for i in 0..len {
184 let new_path = format!("{}[{}]", path, i);
185 Self::compare_recursive(&act_arr[i], &exp_arr[i], &new_path, options, results);
186 }
187
188 if exp_arr.len() > act_arr.len() {
190 for i in act_arr.len()..exp_arr.len() {
191 results.push(AssertionResult::fail(format!(
192 "Missing array item at '{}[{}]'",
193 path, i
194 )));
195 }
196 }
197 }
198 (Value::String(a), Value::String(e)) => {
199 if a != e {
200 results.push(AssertionResult::fail_with_diff(
201 format!(
202 "Value mismatch at '{}': expected \"{}\", got \"{}\"",
203 path, e, a
204 ),
205 e,
206 a,
207 ));
208 }
209 }
210 (Value::Number(a), Value::Number(e)) => {
211 if let Some(tol) = options.tolerance
213 && let (Some(af), Some(ef)) = (a.as_f64(), e.as_f64())
214 {
215 if (af - ef).abs() > tol {
216 results.push(AssertionResult::fail_with_diff(
217 format!(
218 "Value mismatch at '{}': expected {} (tolerance {}), got {}",
219 path, ef, tol, af
220 ),
221 format!("{} (±{})", ef, tol),
222 format!("{}", af),
223 ));
224 }
225 return;
226 }
227
228 if let (Some(af), Some(ef)) = (a.as_f64(), e.as_f64())
231 && (af == ef || (af - ef).abs() <= 1e-6)
232 {
233 return;
234 }
235
236 if a != e {
237 results.push(AssertionResult::fail_with_diff(
238 format!("Value mismatch at '{}': expected {}, got {}", path, e, a),
239 format!("{}", e),
240 format!("{}", a),
241 ));
242 }
243 }
244 (Value::Bool(a), Value::Bool(e)) => {
245 if a != e {
246 results.push(AssertionResult::fail_with_diff(
247 format!("Value mismatch at '{}': expected {}, got {}", path, e, a),
248 format!("{}", e),
249 format!("{}", a),
250 ));
251 }
252 }
253 (Value::Null, Value::Null) => {}
254 _ => {
255 results.push(AssertionResult::fail_with_diff(
257 format!(
258 "Type mismatch at '{}': expected {:?}, got {:?}",
259 path, expected, actual
260 ),
261 format!("{:?}", expected),
262 format!("{:?}", actual),
263 ));
264 }
265 }
266 }
267
268 fn hash_value(value: &Value) -> u64 {
271 use std::collections::hash_map::DefaultHasher;
272 use std::hash::{Hash, Hasher};
273
274 let mut hasher = DefaultHasher::new();
275
276 match value {
277 Value::Null => 0u8.hash(&mut hasher),
278 Value::Bool(b) => (1u8, b).hash(&mut hasher),
279 Value::Number(n) => {
280 (2u8, n.as_i64(), n.as_u64(), n.as_f64().map(|f| f.to_bits())).hash(&mut hasher)
281 }
282 Value::String(s) => (3u8, s).hash(&mut hasher),
283 Value::Array(arr) => {
284 (4u8, arr.len()).hash(&mut hasher);
285 for item in arr {
286 Self::hash_value(item).hash(&mut hasher);
287 }
288 }
289 Value::Object(obj) => {
290 (5u8, obj.len()).hash(&mut hasher);
291 let mut keys: Vec<_> = obj.keys().collect();
293 keys.sort();
294 for key in keys {
295 key.hash(&mut hasher);
296 Self::hash_value(&obj[key]).hash(&mut hasher);
297 }
298 }
299 }
300
301 hasher.finish()
302 }
303}
304
305fn is_protojson_default_value(value: &Value) -> bool {
306 match value {
307 Value::Null => true,
308 Value::Bool(b) => !*b,
309 Value::Number(n) => {
310 if let Some(i) = n.as_i64() {
311 i == 0
312 } else if let Some(u) = n.as_u64() {
313 u == 0
314 } else if let Some(f) = n.as_f64() {
315 f == 0.0
316 } else {
317 false
318 }
319 }
320 Value::String(s) => s.is_empty(),
321 Value::Array(arr) => arr.is_empty(),
322 Value::Object(map) => map.is_empty(),
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use serde_json::json;
330
331 #[test]
332 fn test_compare_exact_match() {
333 let actual = json!({"foo": "bar", "num": 1});
334 let expected = json!({"foo": "bar", "num": 1});
335 let options = InlineOptions::default();
336
337 let results = JsonComparator::compare(&actual, &expected, &options);
338 assert!(results.is_empty());
339 }
340
341 #[test]
342 fn test_compare_numeric_representation_match() {
343 let actual = json!({"result": 60.0});
344 let expected = json!({"result": 60});
345 let options = InlineOptions::default();
346
347 let results = JsonComparator::compare(&actual, &expected, &options);
348 assert!(results.is_empty());
349 }
350
351 #[test]
352 fn test_compare_mismatch() {
353 let actual = json!({"foo": "bar"});
354 let expected = json!({"foo": "baz"});
355 let options = InlineOptions::default();
356
357 let results = JsonComparator::compare(&actual, &expected, &options);
358 assert_eq!(results.len(), 1);
359 if let AssertionResult::Fail { message: msg, .. } = &results[0] {
360 assert!(msg.contains("Value mismatch"));
361 } else {
362 panic!("Expected Fail");
363 }
364 }
365
366 #[test]
367 fn test_compare_partial_object() {
368 let actual = json!({"foo": "bar", "extra": "field"});
369 let expected = json!({"foo": "bar"});
370
371 let options = InlineOptions::default();
373 let results = JsonComparator::compare(&actual, &expected, &options);
374 assert_eq!(results.len(), 1);
375
376 let options = InlineOptions {
378 partial: true,
379 ..Default::default()
380 };
381 let results = JsonComparator::compare(&actual, &expected, &options);
382 assert!(results.is_empty());
383 }
384
385 #[test]
386 fn test_wildcard() {
387 let actual = json!({"id": 12345, "name": "test"});
388 let expected = json!({"id": "*", "name": "test"});
389 let options = InlineOptions::default();
390
391 let results = JsonComparator::compare(&actual, &expected, &options);
392 assert!(results.is_empty());
393 }
394
395 #[test]
396 fn test_redact() {
397 let actual = json!({"id": 12345, "secret": "hidden", "name": "test"});
398 let expected = json!({"id": 12345, "name": "test"});
401
402 let options = InlineOptions {
403 redact: vec!["secret".to_string()],
404 ..Default::default()
405 };
406
407 let results = JsonComparator::compare(&actual, &expected, &options);
408 assert!(results.is_empty());
409 }
410
411 #[test]
412 fn test_tolerance() {
413 let actual = json!({"val": 10.005});
414 let expected = json!({"val": 10.0});
415
416 let mut options = InlineOptions {
417 tolerance: Some(0.01),
418 ..Default::default()
419 };
420
421 let results = JsonComparator::compare(&actual, &expected, &options);
422 assert!(results.is_empty());
423
424 options.tolerance = Some(0.001);
425 let results = JsonComparator::compare(&actual, &expected, &options);
426 assert_eq!(results.len(), 1);
427 }
428
429 #[test]
430 fn test_compare_empty_objects() {
431 let actual = json!({});
432 let expected = json!({});
433 let options = InlineOptions::default();
434
435 let results = JsonComparator::compare(&actual, &expected, &options);
436 assert!(results.is_empty());
437 }
438
439 #[test]
440 fn test_compare_empty_arrays() {
441 let actual = json!([]);
442 let expected = json!([]);
443 let options = InlineOptions::default();
444
445 let results = JsonComparator::compare(&actual, &expected, &options);
446 assert!(results.is_empty());
447 }
448
449 #[test]
450 fn test_compare_null_values() {
451 let actual = json!({"val": null});
452 let expected = json!({"val": null});
453 let options = InlineOptions::default();
454
455 let results = JsonComparator::compare(&actual, &expected, &options);
456 assert!(results.is_empty());
457 }
458
459 #[test]
460 fn test_compare_null_mismatch() {
461 let actual = json!({"val": "not null"});
462 let expected = json!({"val": null});
463 let options = InlineOptions::default();
464
465 let results = JsonComparator::compare(&actual, &expected, &options);
466 assert_eq!(results.len(), 1);
467 }
468
469 #[test]
470 fn test_compare_boolean_values() {
471 let actual = json!({"active": true, "deleted": false});
472 let expected = json!({"active": true, "deleted": false});
473 let options = InlineOptions::default();
474
475 let results = JsonComparator::compare(&actual, &expected, &options);
476 assert!(results.is_empty());
477 }
478
479 #[test]
480 fn test_compare_boolean_mismatch() {
481 let actual = json!({"active": true});
482 let expected = json!({"active": false});
483 let options = InlineOptions::default();
484
485 let results = JsonComparator::compare(&actual, &expected, &options);
486 assert_eq!(results.len(), 1);
487 }
488
489 #[test]
490 fn test_compare_nested_objects() {
491 let actual = json!({"user": {"name": "test", "age": 25}});
492 let expected = json!({"user": {"name": "test", "age": 25}});
493 let options = InlineOptions::default();
494
495 let results = JsonComparator::compare(&actual, &expected, &options);
496 assert!(results.is_empty());
497 }
498
499 #[test]
500 fn test_compare_nested_mismatch() {
501 let actual = json!({"user": {"name": "test"}});
502 let expected = json!({"user": {"name": "other"}});
503 let options = InlineOptions::default();
504
505 let results = JsonComparator::compare(&actual, &expected, &options);
506 assert_eq!(results.len(), 1);
507 }
508
509 #[test]
510 fn test_compare_arrays_different_lengths() {
511 let actual = json!([1, 2, 3]);
512 let expected = json!([1, 2]);
513 let options = InlineOptions::default();
514
515 let results = JsonComparator::compare(&actual, &expected, &options);
516 assert!(!results.is_empty());
517 }
518
519 #[test]
520 fn test_compare_arrays_with_objects() {
521 let actual = json!([{"id": 1}, {"id": 2}]);
522 let expected = json!([{"id": 1}, {"id": 2}]);
523 let options = InlineOptions::default();
524
525 let results = JsonComparator::compare(&actual, &expected, &options);
526 assert!(results.is_empty());
527 }
528
529 #[test]
530 fn test_compare_partial_nested_object() {
531 let actual = json!({"user": {"name": "test", "age": 25, "extra": "field"}});
532 let expected = json!({"user": {"name": "test"}});
533 let options = InlineOptions {
534 partial: true,
535 ..Default::default()
536 };
537
538 let results = JsonComparator::compare(&actual, &expected, &options);
539 assert!(results.is_empty());
540 }
541
542 #[test]
543 fn test_redact_nested() {
544 let actual = json!({"user": {"password": "secret", "name": "test"}});
545 let expected = json!({"user": {"name": "test"}});
546
547 let options = InlineOptions {
548 redact: vec!["password".to_string()],
549 ..Default::default()
550 };
551
552 let results = JsonComparator::compare(&actual, &expected, &options);
553 assert!(results.is_empty());
554 }
555
556 #[test]
557 fn test_is_protojson_default_value() {
558 assert!(is_protojson_default_value(&Value::String("".to_string())));
559 assert!(is_protojson_default_value(&Value::Number(0.into())));
560 assert!(is_protojson_default_value(&Value::Bool(false)));
561 assert!(is_protojson_default_value(&Value::Array(vec![])));
562 assert!(is_protojson_default_value(&Value::Object(
563 serde_json::Map::new()
564 )));
565 assert!(!is_protojson_default_value(&Value::String(
566 "not empty".to_string()
567 )));
568 assert!(!is_protojson_default_value(&Value::Number(1.into())));
569 assert!(!is_protojson_default_value(&Value::Bool(true)));
570 }
571
572 #[test]
573 fn test_unordered_arrays_optimized() {
574 let actual = json!([3, 1, 2]);
576 let expected = json!([1, 2, 3]);
577 let options = InlineOptions {
578 unordered_arrays: true,
579 ..Default::default()
580 };
581
582 let results = JsonComparator::compare(&actual, &expected, &options);
583 assert!(results.is_empty());
584 }
585
586 #[test]
587 fn test_unordered_arrays_with_objects() {
588 let actual = json!([
590 {"id": 3, "name": "c"},
591 {"id": 1, "name": "a"},
592 {"id": 2, "name": "b"}
593 ]);
594 let expected = json!([
595 {"id": 1, "name": "a"},
596 {"id": 2, "name": "b"},
597 {"id": 3, "name": "c"}
598 ]);
599 let options = InlineOptions {
600 unordered_arrays: true,
601 ..Default::default()
602 };
603
604 let results = JsonComparator::compare(&actual, &expected, &options);
605 assert!(results.is_empty());
606 }
607
608 #[test]
609 fn test_unordered_arrays_missing_item() {
610 let actual = json!([1, 2]);
612 let expected = json!([1, 2, 3]);
613 let options = InlineOptions {
614 unordered_arrays: true,
615 ..Default::default()
616 };
617
618 let results = JsonComparator::compare(&actual, &expected, &options);
619 assert!(!results.is_empty());
620 }
621
622 #[test]
623 fn test_unordered_arrays_extra_item() {
624 let actual = json!([1, 2, 3, 4]);
626 let expected = json!([1, 2, 3]);
627 let options = InlineOptions {
628 unordered_arrays: true,
629 ..Default::default()
630 };
631
632 let results = JsonComparator::compare(&actual, &expected, &options);
633 assert!(!results.is_empty());
634 }
635
636 #[test]
637 fn test_unordered_arrays_partial() {
638 let actual = json!([1, 2, 3, 4]);
640 let expected = json!([1, 3]);
641 let options = InlineOptions {
642 unordered_arrays: true,
643 partial: true,
644 ..Default::default()
645 };
646
647 let results = JsonComparator::compare(&actual, &expected, &options);
648 assert!(results.is_empty());
649 }
650
651 #[test]
652 fn test_hash_value_consistency() {
653 let value1 = json!({"id": 1, "name": "test"});
655 let value2 = json!({"id": 1, "name": "test"});
656 let value3 = json!({"name": "test", "id": 1}); let hash1 = JsonComparator::hash_value(&value1);
660 let hash2 = JsonComparator::hash_value(&value2);
661 let hash3 = JsonComparator::hash_value(&value3);
662
663 assert_eq!(hash1, hash2);
664 assert_eq!(hash2, hash3);
665 }
666}