1#![deny(
142 missing_docs,
143 unused_imports,
144 missing_debug_implementations,
145 missing_copy_implementations,
146 trivial_casts,
147 trivial_numeric_casts,
148 unsafe_code,
149 unstable_features,
150 unused_import_braces,
151 unused_qualifications,
152 unknown_lints
153)]
154
155use diff::diff;
156use serde::Serialize;
157
158mod core_ext;
159mod diff;
160
161#[macro_export]
168macro_rules! assert_json_include {
169 (actual: $actual:expr, expected: $expected:expr $(,)?) => {{
170 $crate::assert_json_matches!(
171 $actual,
172 $expected,
173 $crate::Config::new($crate::CompareMode::Inclusive)
174 )
175 }};
176 (expected: $expected:expr, actual: $actual:expr $(,)?) => {{
177 $crate::assert_json_include!(actual: $actual, expected: $expected)
178 }};
179}
180
181#[macro_export]
187macro_rules! assert_json_eq {
188 ($lhs:expr, $rhs:expr $(,)?) => {{
189 $crate::assert_json_matches!($lhs, $rhs, $crate::Config::new($crate::CompareMode::Strict))
190 }};
191}
192
193#[macro_export]
252macro_rules! assert_json_matches {
253 ($lhs:expr, $rhs:expr, $config:expr $(,)?) => {{
254 if let Err(error) = $crate::assert_json_matches_no_panic(&$lhs, &$rhs, $config) {
255 panic!("\n\n{}\n\n", error);
256 }
257 }};
258}
259
260pub fn assert_json_matches_no_panic<Lhs, Rhs>(
266 lhs: &Lhs,
267 rhs: &Rhs,
268 config: Config,
269) -> Result<(), String>
270where
271 Lhs: Serialize,
272 Rhs: Serialize,
273{
274 let lhs = serde_json::to_value(lhs).unwrap_or_else(|err| {
275 panic!(
276 "Couldn't convert left hand side value to JSON. Serde error: {}",
277 err
278 )
279 });
280 let rhs = serde_json::to_value(rhs).unwrap_or_else(|err| {
281 panic!(
282 "Couldn't convert right hand side value to JSON. Serde error: {}",
283 err
284 )
285 });
286
287 let diffs = diff(&lhs, &rhs, config);
288
289 if diffs.is_empty() {
290 Ok(())
291 } else {
292 let msg = diffs
293 .into_iter()
294 .map(|d| d.to_string())
295 .collect::<Vec<_>>()
296 .join("\n\n");
297 Err(msg)
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
303#[allow(missing_copy_implementations)]
304pub struct Config {
305 pub(crate) compare_mode: CompareMode,
306 pub(crate) numeric_mode: NumericMode,
307}
308
309impl Config {
310 pub fn new(compare_mode: CompareMode) -> Self {
314 Self {
315 compare_mode,
316 numeric_mode: NumericMode::Strict,
317 }
318 }
319
320 pub fn numeric_mode(mut self, numeric_mode: NumericMode) -> Self {
324 self.numeric_mode = numeric_mode;
325 self
326 }
327
328 pub fn compare_mode(mut self, compare_mode: CompareMode) -> Self {
330 self.compare_mode = compare_mode;
331 self
332 }
333}
334
335#[derive(Debug, Copy, Clone, PartialEq, Eq)]
337pub enum CompareMode {
338 Inclusive,
343 Strict,
347}
348
349#[derive(Debug, Copy, Clone, PartialEq, Eq)]
351pub enum NumericMode {
352 Strict,
354 AssumeFloat,
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use serde_json::{json, Value};
362 use std::fmt::Write;
363
364 #[test]
365 fn boolean_root() {
366 let result = test_partial_match(json!(true), json!(true));
367 assert_output_eq(result, Ok(()));
368
369 let result = test_partial_match(json!(false), json!(false));
370 assert_output_eq(result, Ok(()));
371
372 let result = test_partial_match(json!(false), json!(true));
373 assert_output_eq(
374 result,
375 Err(r#"json atoms at path "(root)" are not equal:
376 expected:
377 true
378 actual:
379 false"#),
380 );
381
382 let result = test_partial_match(json!(true), json!(false));
383 assert_output_eq(
384 result,
385 Err(r#"json atoms at path "(root)" are not equal:
386 expected:
387 false
388 actual:
389 true"#),
390 );
391 }
392
393 #[test]
394 fn string_root() {
395 let result = test_partial_match(json!("true"), json!("true"));
396 assert_output_eq(result, Ok(()));
397
398 let result = test_partial_match(json!("false"), json!("false"));
399 assert_output_eq(result, Ok(()));
400
401 let result = test_partial_match(json!("false"), json!("true"));
402 assert_output_eq(
403 result,
404 Err(r#"json atoms at path "(root)" are not equal:
405 expected:
406 "true"
407 actual:
408 "false""#),
409 );
410
411 let result = test_partial_match(json!("true"), json!("false"));
412 assert_output_eq(
413 result,
414 Err(r#"json atoms at path "(root)" are not equal:
415 expected:
416 "false"
417 actual:
418 "true""#),
419 );
420 }
421
422 #[test]
423 fn number_root() {
424 let result = test_partial_match(json!(1), json!(1));
425 assert_output_eq(result, Ok(()));
426
427 let result = test_partial_match(json!(0), json!(0));
428 assert_output_eq(result, Ok(()));
429
430 let result = test_partial_match(json!(0), json!(1));
431 assert_output_eq(
432 result,
433 Err(r#"json atoms at path "(root)" are not equal:
434 expected:
435 1
436 actual:
437 0"#),
438 );
439
440 let result = test_partial_match(json!(1), json!(0));
441 assert_output_eq(
442 result,
443 Err(r#"json atoms at path "(root)" are not equal:
444 expected:
445 0
446 actual:
447 1"#),
448 );
449 }
450
451 #[test]
452 fn null_root() {
453 let result = test_partial_match(json!(null), json!(null));
454 assert_output_eq(result, Ok(()));
455
456 let result = test_partial_match(json!(null), json!(1));
457 assert_output_eq(
458 result,
459 Err(r#"json atoms at path "(root)" are not equal:
460 expected:
461 1
462 actual:
463 null"#),
464 );
465
466 let result = test_partial_match(json!(1), json!(null));
467 assert_output_eq(
468 result,
469 Err(r#"json atoms at path "(root)" are not equal:
470 expected:
471 null
472 actual:
473 1"#),
474 );
475 }
476
477 #[test]
478 fn into_object() {
479 let result = test_partial_match(json!({ "a": true }), json!({ "a": true }));
480 assert_output_eq(result, Ok(()));
481
482 let result = test_partial_match(json!({ "a": false }), json!({ "a": true }));
483 assert_output_eq(
484 result,
485 Err(r#"json atoms at path ".a" are not equal:
486 expected:
487 true
488 actual:
489 false"#),
490 );
491
492 let result =
493 test_partial_match(json!({ "a": { "b": true } }), json!({ "a": { "b": true } }));
494 assert_output_eq(result, Ok(()));
495
496 let result = test_partial_match(json!({ "a": true }), json!({ "a": { "b": true } }));
497 assert_output_eq(
498 result,
499 Err(r#"json atoms at path ".a" are not equal:
500 expected:
501 {
502 "b": true
503 }
504 actual:
505 true"#),
506 );
507
508 let result = test_partial_match(json!({}), json!({ "a": true }));
509 assert_output_eq(
510 result,
511 Err(r#"json atom at path ".a" is missing from actual"#),
512 );
513
514 let result = test_partial_match(json!({ "a": { "b": true } }), json!({ "a": true }));
515 assert_output_eq(
516 result,
517 Err(r#"json atoms at path ".a" are not equal:
518 expected:
519 true
520 actual:
521 {
522 "b": true
523 }"#),
524 );
525 }
526
527 #[test]
528 fn into_array() {
529 let result = test_partial_match(json!([1]), json!([1]));
530 assert_output_eq(result, Ok(()));
531
532 let result = test_partial_match(json!([2]), json!([1]));
533 assert_output_eq(
534 result,
535 Err(r#"json atoms at path "[0]" are not equal:
536 expected:
537 1
538 actual:
539 2"#),
540 );
541
542 let result = test_partial_match(json!([1, 2, 4]), json!([1, 2, 3]));
543 assert_output_eq(
544 result,
545 Err(r#"json atoms at path "[2]" are not equal:
546 expected:
547 3
548 actual:
549 4"#),
550 );
551
552 let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2, 4]}));
553 assert_output_eq(
554 result,
555 Err(r#"json atoms at path ".a[2]" are not equal:
556 expected:
557 4
558 actual:
559 3"#),
560 );
561
562 let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2]}));
563 assert_output_eq(result, Ok(()));
564
565 let result = test_partial_match(json!({ "a": [1, 2]}), json!({ "a": [1, 2, 3]}));
566 assert_output_eq(
567 result,
568 Err(r#"json atom at path ".a[2]" is missing from actual"#),
569 );
570 }
571
572 #[test]
573 fn exact_matching() {
574 let result = test_exact_match(json!(true), json!(true));
575 assert_output_eq(result, Ok(()));
576
577 let result = test_exact_match(json!("s"), json!("s"));
578 assert_output_eq(result, Ok(()));
579
580 let result = test_exact_match(json!("a"), json!("b"));
581 assert_output_eq(
582 result,
583 Err(r#"json atoms at path "(root)" are not equal:
584 lhs:
585 "a"
586 rhs:
587 "b""#),
588 );
589
590 let result = test_exact_match(
591 json!({ "a": [1, { "b": 2 }] }),
592 json!({ "a": [1, { "b": 3 }] }),
593 );
594 assert_output_eq(
595 result,
596 Err(r#"json atoms at path ".a[1].b" are not equal:
597 lhs:
598 2
599 rhs:
600 3"#),
601 );
602 }
603
604 #[test]
605 fn exact_match_output_message() {
606 let result = test_exact_match(json!({ "a": { "b": 1 } }), json!({ "a": {} }));
607 assert_output_eq(
608 result,
609 Err(r#"json atom at path ".a.b" is missing from rhs"#),
610 );
611
612 let result = test_exact_match(json!({ "a": {} }), json!({ "a": { "b": 1 } }));
613 assert_output_eq(
614 result,
615 Err(r#"json atom at path ".a.b" is missing from lhs"#),
616 );
617 }
618
619 fn assert_output_eq(actual: Result<(), String>, expected: Result<(), &str>) {
620 match (actual, expected) {
621 (Ok(()), Ok(())) => {}
622
623 (Err(actual_error), Ok(())) => {
624 let mut f = String::new();
625 writeln!(f, "Did not expect error, but got").unwrap();
626 writeln!(f, "{}", actual_error).unwrap();
627 panic!("{}", f);
628 }
629
630 (Ok(()), Err(expected_error)) => {
631 let expected_error = expected_error.to_string();
632 let mut f = String::new();
633 writeln!(f, "Expected error, but did not get one. Expected error:").unwrap();
634 writeln!(f, "{}", expected_error).unwrap();
635 panic!("{}", f);
636 }
637
638 (Err(actual_error), Err(expected_error)) => {
639 let expected_error = expected_error.to_string();
640 if actual_error != expected_error {
641 let mut f = String::new();
642 writeln!(f, "Errors didn't match").unwrap();
643 writeln!(f, "Expected:").unwrap();
644 writeln!(f, "{}", expected_error).unwrap();
645 writeln!(f, "Got:").unwrap();
646 writeln!(f, "{}", actual_error).unwrap();
647 panic!("{}", f);
648 }
649 }
650 }
651 }
652
653 fn test_partial_match(lhs: Value, rhs: Value) -> Result<(), String> {
654 assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Inclusive))
655 }
656
657 fn test_exact_match(lhs: Value, rhs: Value) -> Result<(), String> {
658 assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Strict))
659 }
660}