assert_json_same/
lib.rs

1//! This crate includes macros for comparing two serializable values by diffing their JSON
2//! representations. It is designed to give much more helpful error messages than the standard
3//! [`assert_eq!`]. It basically does a diff of the two objects and tells you the exact
4//! differences. This is useful when asserting that two large JSON objects are the same.
5//!
6//! It uses the [serde] and [serde_json] to perform the serialization.
7//!
8//! [serde]: https://crates.io/crates/serde
9//! [serde_json]: https://crates.io/crates/serde_json
10//! [`assert_eq!`]: https://doc.rust-lang.org/std/macro.assert_eq.html
11//!
12//! ## Partial matching
13//!
14//! If you want to assert that one JSON value is "included" in another use
15//! [`assert_json_include`](macro.assert_json_include.html):
16//!
17//! ```should_panic
18//! use assert_json_same::assert_json_include;
19//! use serde_json::json;
20//!
21//! let a = json!({
22//!     "data": {
23//!         "users": [
24//!             {
25//!                 "id": 1,
26//!                 "country": {
27//!                     "name": "Denmark"
28//!                 }
29//!             },
30//!             {
31//!                 "id": 24,
32//!                 "country": {
33//!                     "name": "Denmark"
34//!                 }
35//!             }
36//!         ]
37//!     }
38//! });
39//!
40//! let b = json!({
41//!     "data": {
42//!         "users": [
43//!             {
44//!                 "id": 1,
45//!                 "country": {
46//!                     "name": "Sweden"
47//!                 }
48//!             },
49//!             {
50//!                 "id": 2,
51//!                 "country": {
52//!                     "name": "Denmark"
53//!                 }
54//!             }
55//!         ]
56//!     }
57//! });
58//!
59//! assert_json_include!(actual: a, expected: b)
60//! ```
61//!
62//! This will panic with the error message:
63//!
64//! ```text
65//! json atoms at path ".data.users[0].country.name" are not equal:
66//!     expected:
67//!         "Sweden"
68//!     actual:
69//!         "Denmark"
70//!
71//! json atoms at path ".data.users[1].id" are not equal:
72//!     expected:
73//!         2
74//!     actual:
75//!         24
76//! ```
77//!
78//! [`assert_json_include`](macro.assert_json_include.html) allows extra data in `actual` but not in `expected`. That is so you can verify just a part
79//! of the JSON without having to specify the whole thing. For example this test passes:
80//!
81//! ```
82//! use assert_json_same::assert_json_include;
83//! use serde_json::json;
84//!
85//! assert_json_include!(
86//!     actual: json!({
87//!         "a": { "b": 1 },
88//!     }),
89//!     expected: json!({
90//!         "a": {},
91//!     })
92//! )
93//! ```
94//!
95//! However `expected` cannot contain additional data so this test fails:
96//!
97//! ```should_panic
98//! use assert_json_same::assert_json_include;
99//! use serde_json::json;
100//!
101//! assert_json_include!(
102//!     actual: json!({
103//!         "a": {},
104//!     }),
105//!     expected: json!({
106//!         "a": { "b": 1 },
107//!     })
108//! )
109//! ```
110//!
111//! That will print
112//!
113//! ```text
114//! json atom at path ".a.b" is missing from actual
115//! ```
116//!
117//! ## Exact matching
118//!
119//! If you want to ensure two JSON values are *exactly* the same, use [`assert_json_eq`](macro.assert_json_eq.html).
120//!
121//! ```rust,should_panic
122//! use assert_json_same::assert_json_eq;
123//! use serde_json::json;
124//!
125//! assert_json_eq!(
126//!     json!({ "a": { "b": 1 } }),
127//!     json!({ "a": {} })
128//! )
129//! ```
130//!
131//! This will panic with the error message:
132//!
133//! ```text
134//! json atom at path ".a.b" is missing from lhs
135//! ```
136//!
137//! ## Further customization
138//!
139//! You can use [`assert_json_matches`] to further customize the comparison.
140
141#![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
158use crate::diff::DifferenceBuf;
159
160mod core_ext;
161mod diff;
162
163/// Compare two JSON values for an inclusive match.
164///
165/// It allows `actual` to contain additional data. If you want an exact match use
166/// [`assert_json_eq`](macro.assert_json_eq.html) instead.
167///
168/// See [crate documentation](index.html) for examples.
169#[macro_export]
170macro_rules! assert_json_include {
171    (actual: $actual:expr, expected: $expected:expr $(,)?) => {{
172        $crate::assert_json_matches!(
173            $actual,
174            $expected,
175            $crate::Config::new($crate::CompareMode::Inclusive)
176        )
177    }};
178    (expected: $expected:expr, actual: $actual:expr $(,)?) => {{
179        $crate::assert_json_include!(actual: $actual, expected: $expected)
180    }};
181}
182
183/// Compare two JSON values for an exact match.
184///
185/// If you want an inclusive match use [`assert_json_include`](macro.assert_json_include.html) instead.
186///
187/// See [crate documentation](index.html) for examples.
188#[macro_export]
189macro_rules! assert_json_eq {
190    ($lhs:expr, $rhs:expr $(,)?) => {{
191        $crate::assert_json_matches!($lhs, $rhs, $crate::Config::new($crate::CompareMode::Strict))
192    }};
193}
194
195/// Compare two JSON values according to a configuration.
196///
197/// ```
198/// use assert_json_same::{
199///     CompareMode,
200///     Config,
201///     NumericMode,
202///     assert_json_matches,
203/// };
204/// use serde_json::json;
205///
206/// let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat);
207///
208/// assert_json_matches!(
209///     json!({
210///         "a": { "b": [1, 2, 3.0] },
211///     }),
212///     json!({
213///         "a": { "b": [1, 2.0, 3] },
214///     }),
215///     config,
216/// )
217/// ```
218///
219/// When using `CompareMode::Inclusive` the first argument is `actual` and the second argument is
220/// `expected`. Example:
221///
222/// ```
223/// # use assert_json_same::{
224/// #     CompareMode,
225/// #     Config,
226/// #     NumericMode,
227/// #     assert_json_matches,
228/// #     assert_json_include,
229/// # };
230/// # use serde_json::json;
231/// #
232/// // This
233/// assert_json_matches!(
234///     json!({
235///         "a": { "b": 1 },
236///     }),
237///     json!({
238///         "a": {},
239///     }),
240///     Config::new(CompareMode::Inclusive),
241/// );
242///
243/// // Is the same as this
244/// assert_json_include!(
245///     actual: json!({
246///         "a": { "b": 1 },
247///     }),
248///     expected: json!({
249///         "a": {},
250///     }),
251/// );
252/// ```
253#[macro_export]
254macro_rules! assert_json_matches {
255    ($lhs:expr, $rhs:expr, $config:expr $(,)?) => {{
256        if let Err(error) = $crate::assert_json_matches_no_panic_to_string(&$lhs, &$rhs, $config) {
257            panic!("\n\n{}\n\n", error);
258        }
259    }};
260}
261
262/// Compares two JSON values without panicking.
263///
264/// Returns a `Result` containing either `Ok(())` if the values match,
265/// or an `Err` with a `Vec<DifferenceBuf>` describing the differences.
266///
267/// # Note:
268///
269/// This function performs some cloning and may be less efficient.
270///
271/// If you only need a string error message, use [`assert_json_matches_no_panic_to_string`] or the assertion macros.
272pub fn assert_json_matches_no_panic<Lhs, Rhs>(
273    lhs: &Lhs,
274    rhs: &Rhs,
275    config: Config,
276) -> Result<(), Vec<DifferenceBuf>>
277where
278    Lhs: Serialize,
279    Rhs: Serialize,
280{
281    let lhs = serde_json::to_value(lhs).unwrap_or_else(|err| {
282        panic!(
283            "Couldn't convert left hand side value to JSON. Serde error: {}",
284            err
285        )
286    });
287    let rhs = serde_json::to_value(rhs).unwrap_or_else(|err| {
288        panic!(
289            "Couldn't convert right hand side value to JSON. Serde error: {}",
290            err
291        )
292    });
293
294    let diffs = diff(&lhs, &rhs, config);
295    let diffs: Vec<DifferenceBuf> = diffs.into_iter().map(|d| d.into()).collect();
296
297    if diffs.is_empty() {
298        Ok(())
299    } else {
300        Err(diffs)
301    }
302}
303/// Compares two JSON values without panicking.
304///
305/// Instead it returns a `Result` where the error is the message that would be passed to `panic!`.
306/// This is might be useful if you want to control how failures are reported and don't want to deal
307/// with panics.
308pub fn assert_json_matches_no_panic_to_string<Lhs, Rhs>(
309    lhs: &Lhs,
310    rhs: &Rhs,
311    config: Config,
312) -> Result<(), String>
313where
314    Lhs: Serialize,
315    Rhs: Serialize,
316{
317    let lhs = serde_json::to_value(lhs).unwrap_or_else(|err| {
318        panic!(
319            "Couldn't convert left hand side value to JSON. Serde error: {}",
320            err
321        )
322    });
323    let rhs = serde_json::to_value(rhs).unwrap_or_else(|err| {
324        panic!(
325            "Couldn't convert right hand side value to JSON. Serde error: {}",
326            err
327        )
328    });
329
330    let diffs = diff(&lhs, &rhs, config);
331
332    if diffs.is_empty() {
333        Ok(())
334    } else {
335        let msg = diffs
336            .into_iter()
337            .map(|d| d.to_string())
338            .collect::<Vec<_>>()
339            .join("\n\n");
340        Err(msg)
341    }
342}
343
344/// Configuration for how JSON values should be compared.
345#[derive(Debug, Clone, Copy, PartialEq)]
346#[allow(missing_copy_implementations)]
347pub struct Config {
348    pub(crate) compare_mode: CompareMode,
349    pub(crate) numeric_mode: NumericMode,
350}
351
352impl Config {
353    /// Create a new [`Config`] using the given [`CompareMode`].
354    ///
355    /// The default `numeric_mode` is be [`NumericMode::Strict`].
356    pub fn new(compare_mode: CompareMode) -> Self {
357        Self {
358            compare_mode,
359            numeric_mode: NumericMode::Strict,
360        }
361    }
362
363    /// Change the config's numeric mode.
364    ///
365    /// The default `numeric_mode` is be [`NumericMode::Strict`].
366    pub fn numeric_mode(mut self, numeric_mode: NumericMode) -> Self {
367        self.numeric_mode = numeric_mode;
368        self
369    }
370
371    /// Change the config's compare mode.
372    pub fn compare_mode(mut self, compare_mode: CompareMode) -> Self {
373        self.compare_mode = compare_mode;
374        self
375    }
376}
377
378/// Mode for how JSON values should be compared.
379#[derive(Debug, Copy, Clone, PartialEq, Eq)]
380pub enum CompareMode {
381    /// The two JSON values don't have to be exactly equal. The "actual" value is only required to
382    /// be "contained" inside "expected". See [crate documentation](index.html) for examples.
383    ///
384    /// The mode used with [`assert_json_include`].
385    Inclusive,
386    /// The two JSON values must be exactly equal.
387    ///
388    /// The mode used with [`assert_json_eq`].
389    Strict,
390}
391
392/// How should numbers be compared.
393#[derive(Debug, Copy, Clone, PartialEq)]
394pub enum NumericMode {
395    /// Different numeric types aren't considered equal.
396    Strict,
397    /// All numeric types are converted to float before comparison.
398    AssumeFloat,
399
400    /// All numeric types are converted to integer before comparison, floats are considered equal if they differ by at most this epsilon value.
401    AssumeFloatEpsilon(f64),
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use serde_json::{json, Value};
408    use std::fmt::Write;
409
410    #[test]
411    fn boolean_root() {
412        let result = test_partial_match(json!(true), json!(true));
413        assert_output_eq(result, Ok(()));
414
415        let result = test_partial_match(json!(false), json!(false));
416        assert_output_eq(result, Ok(()));
417
418        let result = test_partial_match(json!(false), json!(true));
419        assert_output_eq(
420            result,
421            Err(r#"json atoms at path "(root)" are not equal:
422    expected:
423        true
424    actual:
425        false"#),
426        );
427
428        let result = test_partial_match(json!(true), json!(false));
429        assert_output_eq(
430            result,
431            Err(r#"json atoms at path "(root)" are not equal:
432    expected:
433        false
434    actual:
435        true"#),
436        );
437    }
438
439    #[test]
440    fn string_root() {
441        let result = test_partial_match(json!("true"), json!("true"));
442        assert_output_eq(result, Ok(()));
443
444        let result = test_partial_match(json!("false"), json!("false"));
445        assert_output_eq(result, Ok(()));
446
447        let result = test_partial_match(json!("false"), json!("true"));
448        assert_output_eq(
449            result,
450            Err(r#"json atoms at path "(root)" are not equal:
451    expected:
452        "true"
453    actual:
454        "false""#),
455        );
456
457        let result = test_partial_match(json!("true"), json!("false"));
458        assert_output_eq(
459            result,
460            Err(r#"json atoms at path "(root)" are not equal:
461    expected:
462        "false"
463    actual:
464        "true""#),
465        );
466    }
467
468    #[test]
469    fn number_root() {
470        let result = test_partial_match(json!(1), json!(1));
471        assert_output_eq(result, Ok(()));
472
473        let result = test_partial_match(json!(0), json!(0));
474        assert_output_eq(result, Ok(()));
475
476        let result = test_partial_match(json!(0), json!(1));
477        assert_output_eq(
478            result,
479            Err(r#"json atoms at path "(root)" are not equal:
480    expected:
481        1
482    actual:
483        0"#),
484        );
485
486        let result = test_partial_match(json!(1), json!(0));
487        assert_output_eq(
488            result,
489            Err(r#"json atoms at path "(root)" are not equal:
490    expected:
491        0
492    actual:
493        1"#),
494        );
495    }
496
497    #[test]
498    fn null_root() {
499        let result = test_partial_match(json!(null), json!(null));
500        assert_output_eq(result, Ok(()));
501
502        let result = test_partial_match(json!(null), json!(1));
503        assert_output_eq(
504            result,
505            Err(r#"json atoms at path "(root)" are not equal:
506    expected:
507        1
508    actual:
509        null"#),
510        );
511
512        let result = test_partial_match(json!(1), json!(null));
513        assert_output_eq(
514            result,
515            Err(r#"json atoms at path "(root)" are not equal:
516    expected:
517        null
518    actual:
519        1"#),
520        );
521    }
522
523    #[test]
524    fn into_object() {
525        let result = test_partial_match(json!({ "a": true }), json!({ "a": true }));
526        assert_output_eq(result, Ok(()));
527
528        let result = test_partial_match(json!({ "a": false }), json!({ "a": true }));
529        assert_output_eq(
530            result,
531            Err(r#"json atoms at path ".a" are not equal:
532    expected:
533        true
534    actual:
535        false"#),
536        );
537
538        let result =
539            test_partial_match(json!({ "a": { "b": true } }), json!({ "a": { "b": true } }));
540        assert_output_eq(result, Ok(()));
541
542        let result = test_partial_match(json!({ "a": true }), json!({ "a": { "b": true } }));
543        assert_output_eq(
544            result,
545            Err(r#"json atoms at path ".a" are not equal:
546    expected:
547        {
548          "b": true
549        }
550    actual:
551        true"#),
552        );
553
554        let result = test_partial_match(json!({}), json!({ "a": true }));
555        assert_output_eq(
556            result,
557            Err(r#"json atom at path ".a" is missing from actual"#),
558        );
559
560        let result = test_partial_match(json!({ "a": { "b": true } }), json!({ "a": true }));
561        assert_output_eq(
562            result,
563            Err(r#"json atoms at path ".a" are not equal:
564    expected:
565        true
566    actual:
567        {
568          "b": true
569        }"#),
570        );
571    }
572
573    #[test]
574    fn into_array() {
575        let result = test_partial_match(json!([1]), json!([1]));
576        assert_output_eq(result, Ok(()));
577
578        let result = test_partial_match(json!([2]), json!([1]));
579        assert_output_eq(
580            result,
581            Err(r#"json atoms at path "[0]" are not equal:
582    expected:
583        1
584    actual:
585        2"#),
586        );
587
588        let result = test_partial_match(json!([1, 2, 4]), json!([1, 2, 3]));
589        assert_output_eq(
590            result,
591            Err(r#"json atoms at path "[2]" are not equal:
592    expected:
593        3
594    actual:
595        4"#),
596        );
597
598        let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2, 4]}));
599        assert_output_eq(
600            result,
601            Err(r#"json atoms at path ".a[2]" are not equal:
602    expected:
603        4
604    actual:
605        3"#),
606        );
607
608        let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2]}));
609        assert_output_eq(result, Ok(()));
610
611        let result = test_partial_match(json!({ "a": [1, 2]}), json!({ "a": [1, 2, 3]}));
612        assert_output_eq(
613            result,
614            Err(r#"json atom at path ".a[2]" is missing from actual"#),
615        );
616    }
617
618    #[test]
619    fn exact_matching() {
620        let result = test_exact_match(json!(true), json!(true));
621        assert_output_eq(result, Ok(()));
622
623        let result = test_exact_match(json!("s"), json!("s"));
624        assert_output_eq(result, Ok(()));
625
626        let result = test_exact_match(json!("a"), json!("b"));
627        assert_output_eq(
628            result,
629            Err(r#"json atoms at path "(root)" are not equal:
630    lhs:
631        "a"
632    rhs:
633        "b""#),
634        );
635
636        let result = test_exact_match(
637            json!({ "a": [1, { "b": 2 }] }),
638            json!({ "a": [1, { "b": 3 }] }),
639        );
640        assert_output_eq(
641            result,
642            Err(r#"json atoms at path ".a[1].b" are not equal:
643    lhs:
644        2
645    rhs:
646        3"#),
647        );
648    }
649
650    #[test]
651    fn exact_match_output_message() {
652        let result = test_exact_match(json!({ "a": { "b": 1 } }), json!({ "a": {} }));
653        assert_output_eq(
654            result,
655            Err(r#"json atom at path ".a.b" is missing from rhs"#),
656        );
657
658        let result = test_exact_match(json!({ "a": {} }), json!({ "a": { "b": 1 } }));
659        assert_output_eq(
660            result,
661            Err(r#"json atom at path ".a.b" is missing from lhs"#),
662        );
663    }
664
665    fn assert_output_eq(actual: Result<(), String>, expected: Result<(), &str>) {
666        match (actual, expected) {
667            (Ok(()), Ok(())) => {}
668
669            (Err(actual_error), Ok(())) => {
670                let mut f = String::new();
671                writeln!(f, "Did not expect error, but got").unwrap();
672                writeln!(f, "{}", actual_error).unwrap();
673                panic!("{}", f);
674            }
675
676            (Ok(()), Err(expected_error)) => {
677                let expected_error = expected_error.to_string();
678                let mut f = String::new();
679                writeln!(f, "Expected error, but did not get one. Expected error:").unwrap();
680                writeln!(f, "{}", expected_error).unwrap();
681                panic!("{}", f);
682            }
683
684            (Err(actual_error), Err(expected_error)) => {
685                let expected_error = expected_error.to_string();
686                if actual_error != expected_error {
687                    let mut f = String::new();
688                    writeln!(f, "Errors didn't match").unwrap();
689                    writeln!(f, "Expected:").unwrap();
690                    writeln!(f, "{}", expected_error).unwrap();
691                    writeln!(f, "Got:").unwrap();
692                    writeln!(f, "{}", actual_error).unwrap();
693                    panic!("{}", f);
694                }
695            }
696        }
697    }
698
699    fn test_partial_match(lhs: Value, rhs: Value) -> Result<(), String> {
700        assert_json_matches_no_panic_to_string(&lhs, &rhs, Config::new(CompareMode::Inclusive))
701    }
702
703    fn test_exact_match(lhs: Value, rhs: Value) -> Result<(), String> {
704        assert_json_matches_no_panic_to_string(&lhs, &rhs, Config::new(CompareMode::Strict))
705    }
706}