diff --git a/src/diff.rs b/src/diff.rs index de97a0d..20981e9 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -1,9 +1,9 @@ use crate::core_ext::{Indent, Indexes}; -use crate::{CompareMode, Config, NumericMode}; +use crate::{ArraySortingMode, CompareMode, Config, NumericMode}; use serde_json::Value; use std::{collections::HashSet, fmt}; -pub(crate) fn diff<'a>(lhs: &'a Value, rhs: &'a Value, config: Config) -> Vec> { +pub(crate) fn diff<'a>(lhs: &'a Value, rhs: &'a Value, config: &'a Config) -> Vec> { let mut acc = vec![]; diff_with(lhs, rhs, config, Path::Root, &mut acc); acc @@ -12,7 +12,7 @@ pub(crate) fn diff<'a>(lhs: &'a Value, rhs: &'a Value, config: Config) -> Vec( lhs: &'a Value, rhs: &'a Value, - config: Config, + config: &'a Config, path: Path<'a>, acc: &mut Vec>, ) { @@ -31,7 +31,7 @@ struct DiffFolder<'a, 'b> { rhs: &'a Value, path: Path<'a>, acc: &'b mut Vec>, - config: Config, + config: &'a Config, } macro_rules! direct_compare { @@ -69,7 +69,46 @@ impl<'a, 'b> DiffFolder<'a, 'b> { } } + fn on_array_contains(&mut self, lhs: &'a Value) { + if let Some(rhs) = self.rhs.as_array() { + let lhs_array = lhs.as_array().unwrap(); + + for rhs_item in rhs.iter() { + // research number of repeated items in rhs for this item + let rhs_item_count = rhs + .iter() + .filter(|i| diff(rhs_item, i, self.config).len() == 0) + .count(); + // now, make sure that lhs has at least as many items matching this item + let lhs_matching_items_count = lhs_array + .iter() + .filter(|lhs_item| diff(rhs_item, lhs_item, self.config).len() == 0) + .count(); + if lhs_matching_items_count < rhs_item_count { + self.acc.push(Difference { + lhs: Some(lhs), + rhs: Some(&self.rhs), + path: self.path.clone(), + config: self.config.clone(), + }); + break; + } + } + } else { + self.acc.push(Difference { + lhs: Some(lhs), + rhs: Some(&self.rhs), + path: self.path.clone(), + config: self.config.clone(), + }); + } + } + fn on_array(&mut self, lhs: &'a Value) { + if self.config.array_sorting_mode == ArraySortingMode::Ignore { + return self.on_array_contains(lhs); + } + if let Some(rhs) = self.rhs.as_array() { let lhs = lhs.as_array().unwrap(); @@ -79,7 +118,7 @@ impl<'a, 'b> DiffFolder<'a, 'b> { let path = self.path.append(Key::Idx(idx)); if let Some(lhs) = lhs.get(idx) { - diff_with(lhs, rhs, self.config.clone(), path, self.acc) + diff_with(lhs, rhs, self.config, path, self.acc) } else { self.acc.push(Difference { lhs: None, @@ -101,7 +140,7 @@ impl<'a, 'b> DiffFolder<'a, 'b> { match (lhs.get(key), rhs.get(key)) { (Some(lhs), Some(rhs)) => { - diff_with(lhs, rhs, self.config.clone(), path, self.acc); + diff_with(lhs, rhs, self.config, path, self.acc); } (None, Some(rhs)) => { self.acc.push(Difference { @@ -146,7 +185,7 @@ impl<'a, 'b> DiffFolder<'a, 'b> { let path = self.path.append(Key::Field(key)); if let Some(lhs) = lhs.get(key) { - diff_with(lhs, rhs, self.config.clone(), path, self.acc) + diff_with(lhs, rhs, self.config, path, self.acc) } else { self.acc.push(Difference { lhs: None, @@ -164,7 +203,7 @@ impl<'a, 'b> DiffFolder<'a, 'b> { match (lhs.get(key), rhs.get(key)) { (Some(lhs), Some(rhs)) => { - diff_with(lhs, rhs, self.config.clone(), path, self.acc); + diff_with(lhs, rhs, self.config, path, self.acc); } (None, Some(rhs)) => { self.acc.push(Difference { @@ -319,214 +358,193 @@ mod test { #[test] fn test_diffing_leaf_json() { - let diffs = diff( - &json!(null), - &json!(null), - Config::new(CompareMode::Inclusive), - ); + let config = Config::new(CompareMode::Inclusive); + let diffs = diff(&json!(null), &json!(null), &config); assert_eq!(diffs, vec![]); - let diffs = diff( - &json!(false), - &json!(false), - Config::new(CompareMode::Inclusive), - ); + let diffs = diff(&json!(false), &json!(false), &config); assert_eq!(diffs, vec![]); - let diffs = diff( - &json!(true), - &json!(true), - Config::new(CompareMode::Inclusive), - ); + let diffs = diff(&json!(true), &json!(true), &config); assert_eq!(diffs, vec![]); - let diffs = diff( - &json!(false), - &json!(true), - Config::new(CompareMode::Inclusive), - ); + let diffs = diff(&json!(false), &json!(true), &config); assert_eq!(diffs.len(), 1); - let diffs = diff( - &json!(true), - &json!(false), - Config::new(CompareMode::Inclusive), - ); + let diffs = diff(&json!(true), &json!(false), &config); assert_eq!(diffs.len(), 1); let actual = json!(1); let expected = json!(1); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); let actual = json!(2); let expected = json!(1); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); let actual = json!(1); let expected = json!(2); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); let actual = json!(1.0); let expected = json!(1.0); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); let actual = json!(1); let expected = json!(1.0); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); let actual = json!(1.0); let expected = json!(1); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); + let config_assume_float = config.numeric_mode(NumericMode::AssumeFloat); + let actual = json!(1); let expected = json!(1.0); - let diffs = diff( - &actual, - &expected, - Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat), - ); + let diffs = diff(&actual, &expected, &config_assume_float); assert_eq!(diffs, vec![]); let actual = json!(1.0); let expected = json!(1); - let diffs = diff( - &actual, - &expected, - Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat), - ); + let diffs = diff(&actual, &expected, &config_assume_float); assert_eq!(diffs, vec![]); } #[test] fn test_diffing_array() { + let config = Config::new(CompareMode::Inclusive); // empty let actual = json!([]); let expected = json!([]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); let actual = json!([1]); let expected = json!([]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 0); let actual = json!([]); let expected = json!([1]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); // eq let actual = json!([1]); let expected = json!([1]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); // actual longer let actual = json!([1, 2]); let expected = json!([1]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); // expected longer let actual = json!([1]); let expected = json!([1, 2]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); // eq length but different let actual = json!([1, 3]); let expected = json!([1, 2]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); // different types let actual = json!(1); let expected = json!([1]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); let actual = json!([1]); let expected = json!(1); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); } #[test] fn test_array_strict() { + let config = Config::new(CompareMode::Strict); let actual = json!([]); let expected = json!([]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 0); let actual = json!([1, 2]); let expected = json!([1, 2]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 0); let actual = json!([1]); let expected = json!([1, 2]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); let actual = json!([1, 2]); let expected = json!([1]); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); } #[test] fn test_object() { + let config = Config::new(CompareMode::Inclusive); let actual = json!({}); let expected = json!({}); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); let actual = json!({ "a": 1 }); let expected = json!({ "a": 1 }); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); let actual = json!({ "a": 1, "b": 123 }); let expected = json!({ "a": 1 }); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); let actual = json!({ "a": 1 }); let expected = json!({ "b": 1 }); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); let actual = json!({ "a": 1 }); let expected = json!({ "a": 2 }); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs.len(), 1); let actual = json!({ "a": { "b": true } }); let expected = json!({ "a": {} }); - let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); + let diffs = diff(&actual, &expected, &config); assert_eq!(diffs, vec![]); } #[test] fn test_object_strict() { + let config = Config::new(CompareMode::Strict); let lhs = json!({}); let rhs = json!({ "a": 1 }); - let diffs = diff(&lhs, &rhs, Config::new(CompareMode::Strict)); + let diffs = diff(&lhs, &rhs, &config); assert_eq!(diffs.len(), 1); let lhs = json!({ "a": 1 }); let rhs = json!({}); - let diffs = diff(&lhs, &rhs, Config::new(CompareMode::Strict)); + let diffs = diff(&lhs, &rhs, &config); assert_eq!(diffs.len(), 1); let json = json!({ "a": 1 }); - let diffs = diff(&json, &json, Config::new(CompareMode::Strict)); + let diffs = diff(&json, &json, &config); assert_eq!(diffs, vec![]); } } diff --git a/src/lib.rs b/src/lib.rs index 961b349..cc2cdd7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,18 @@ use serde::Serialize; mod core_ext; mod diff; +/// Assert that a JSON value contains other JSON value +/// +/// See [crate documentation](index.html) for examples. +#[macro_export] +macro_rules! assert_json_contains { + (container: $container:expr, contained: $contained:expr $(,)?) => {{ + let config = + $crate::Config::new($crate::CompareMode::Inclusive).consider_array_sorting(false); + $crate::assert_json_matches!($container, $contained, &config) + }}; +} + /// Compare two JSON values for an inclusive match. /// /// It allows `actual` to contain additional data. If you want an exact match use @@ -167,11 +179,8 @@ mod diff; #[macro_export] macro_rules! assert_json_include { (actual: $actual:expr, expected: $expected:expr $(,)?) => {{ - $crate::assert_json_matches!( - $actual, - $expected, - $crate::Config::new($crate::CompareMode::Inclusive) - ) + let config = $crate::Config::new($crate::CompareMode::Inclusive); + $crate::assert_json_matches!($actual, $expected, &config) }}; (expected: $expected:expr, actual: $actual:expr $(,)?) => {{ $crate::assert_json_include!(actual: $actual, expected: $expected) @@ -186,7 +195,8 @@ macro_rules! assert_json_include { #[macro_export] macro_rules! assert_json_eq { ($lhs:expr, $rhs:expr $(,)?) => {{ - $crate::assert_json_matches!($lhs, $rhs, $crate::Config::new($crate::CompareMode::Strict)) + let config = $crate::Config::new($crate::CompareMode::Strict); + $crate::assert_json_matches!($lhs, $rhs, &config) }}; } @@ -210,7 +220,7 @@ macro_rules! assert_json_eq { /// json!({ /// "a": { "b": [1, 2.0, 3] }, /// }), -/// config, +/// &config, /// ) /// ``` /// @@ -228,6 +238,7 @@ macro_rules! assert_json_eq { /// # use serde_json::json; /// # /// // This +/// let config = Config::new(CompareMode::Inclusive); /// assert_json_matches!( /// json!({ /// "a": { "b": 1 }, @@ -235,7 +246,7 @@ macro_rules! assert_json_eq { /// json!({ /// "a": {}, /// }), -/// Config::new(CompareMode::Inclusive), +/// &config, /// ); /// /// // Is the same as this @@ -265,7 +276,7 @@ macro_rules! assert_json_matches { pub fn assert_json_matches_no_panic( lhs: &Lhs, rhs: &Rhs, - config: Config, + config: &Config, ) -> Result<(), String> where Lhs: Serialize, @@ -302,6 +313,7 @@ where #[derive(Debug, Clone, PartialEq, Eq)] #[allow(missing_copy_implementations)] pub struct Config { + pub(crate) array_sorting_mode: ArraySortingMode, pub(crate) compare_mode: CompareMode, pub(crate) numeric_mode: NumericMode, } @@ -312,6 +324,7 @@ impl Config { /// The default `numeric_mode` is be [`NumericMode::Strict`]. pub fn new(compare_mode: CompareMode) -> Self { Self { + array_sorting_mode: ArraySortingMode::Consider, compare_mode, numeric_mode: NumericMode::Strict, } @@ -330,6 +343,19 @@ impl Config { self.compare_mode = compare_mode; self } + + /// configure array sorting mode + pub fn consider_array_sorting(mut self, consider: bool) -> Self { + if consider { + if self.compare_mode == CompareMode::Strict { + panic!("strict comparison does not allow array ordering to be ignored"); + } + self.array_sorting_mode = ArraySortingMode::Consider; + } else { + self.array_sorting_mode = ArraySortingMode::Ignore; + } + self + } } /// Mode for how JSON values should be compared. @@ -346,6 +372,15 @@ pub enum CompareMode { Strict, } +/// Should array sorting be taken in consideration +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum ArraySortingMode { + ///consider + Consider, + /// ignore + Ignore, +} + /// How should numbers be compared. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum NumericMode { @@ -651,10 +686,10 @@ mod tests { } fn test_partial_match(lhs: Value, rhs: Value) -> Result<(), String> { - assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Inclusive)) + assert_json_matches_no_panic(&lhs, &rhs, &Config::new(CompareMode::Inclusive)) } fn test_exact_match(lhs: Value, rhs: Value) -> Result<(), String> { - assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Strict)) + assert_json_matches_no_panic(&lhs, &rhs, &Config::new(CompareMode::Strict)) } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 1bbe8ee..2b42713 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,6 +1,6 @@ use assert_json_diff::{ - assert_json_eq, assert_json_include, assert_json_matches, assert_json_matches_no_panic, - CompareMode, Config, NumericMode, + assert_json_contains, assert_json_eq, assert_json_include, assert_json_matches, + assert_json_matches_no_panic, CompareMode, Config, NumericMode, }; use serde::Serialize; use serde_json::json; @@ -60,9 +60,10 @@ fn different_numeric_types_assume_float() { let actual = json!({ "a": { "b": true }, "c": [true, null, 1] }); let expected = json!({ "a": { "b": true }, "c": [true, null, 1.0] }); let config = Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat); - assert_json_matches!(actual, expected, config.clone()); + assert_json_matches!(&actual, &expected, &config); - assert_json_matches!(actual, expected, config.compare_mode(CompareMode::Strict)) + let config = config.compare_mode(CompareMode::Strict); + assert_json_matches!(actual, expected, &config); } #[test] @@ -71,6 +72,54 @@ fn can_pass_with_exact_match() { assert_json_eq!(json!({ "a": { "b": true } }), json!({ "a": { "b": true } }),); } +#[test] +fn can_pass_with_contains_match() { + // null contains null + assert_json_contains!(container: json!(null), contained: json!(null)); + // numeric value contains numeric value + assert_json_contains!(container: json!(1), contained: json!(1)); + // string contains string + assert_json_contains!(container: json!("a"), contained: json!("a")); + // object 1 contains identical object 2 + assert_json_contains!( + container: json!({ "a": { "b": true } }), + contained: json!({ "a": { "b": true } }) + ); + // object 1 has more keys than object 2, but the keys on object 2 match the keys on object 1 + assert_json_contains!( + container: json!({ "a": { "b": true }, "c": 1}), + contained: json!({ "a": { "b": true } }) + ); + // array 1 contains identical array 2 + assert_json_contains!(container: json!([1, 2, 3]), contained: json!([1, 2, 3])); + // array 1 contains all items on array 2, even itens on array 2 being in different order than they are on array 1 + assert_json_contains!(container: json!([1, 2, 3]), contained: json!([2, 3, 1])); + // array 1 contains more items than array 2, but items on array 2 match items on array 1 in the same order + assert_json_contains!(container: json!([1, 2, 3, 4]), contained: json!([1, 2, 3])); + // array 1 contains more items than array 2, but items on array 2 match items on array 1 in diferent order + assert_json_contains!(container: json!([1, 2, 3, 4]), contained: json!([2, 3, 1])); + // array 1 contains all items on array2 with the same amount of repeated items on both, in the same order + assert_json_contains!( + container: json!([1, 2, 3, 1, 4]), + contained: json!([1, 2, 3, 1, 4]) + ); + // array 1 contains all items on array2 with the same amount of repeated items on both, in different order + assert_json_contains!( + container: json!([1, 2, 3, 1, 4]), + contained: json!([3, 1, 2, 1, 4]) + ); + // array 1 contains more items than array 2, but items on aray 2 match items on array 1 with repeated items on both in the same order + assert_json_contains!( + container: json!([1, 2, 3, 1, 4]), + contained: json!([1, 2, 3, 1]) + ); + // array 1 contains more items than array 2, but items on array 2 match items on array 1 with repeated items on both in different order + assert_json_contains!( + container: json!([1, 2, 3, 1, 4]), + contained: json!([2, 1, 3, 1]) + ); +} + #[test] #[should_panic] fn can_fail_with_exact_match() { @@ -79,36 +128,23 @@ fn can_fail_with_exact_match() { #[test] fn inclusive_match_without_panicking() { - assert!(assert_json_matches_no_panic( - &json!({ "a": 1, "b": 2 }), - &json!({ "b": 2}), - Config::new(CompareMode::Inclusive,).numeric_mode(NumericMode::Strict), - ) - .is_ok()); - - assert!(assert_json_matches_no_panic( - &json!({ "a": 1, "b": 2 }), - &json!("foo"), - Config::new(CompareMode::Inclusive,).numeric_mode(NumericMode::Strict), - ) - .is_err()); + let config = Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::Strict); + assert!( + assert_json_matches_no_panic(&json!({ "a": 1, "b": 2 }), &json!({ "b": 2}), &config) + .is_ok() + ); + + assert!( + assert_json_matches_no_panic(&json!({ "a": 1, "b": 2 }), &json!("foo"), &config,).is_err() + ); } #[test] fn exact_match_without_panicking() { - assert!(assert_json_matches_no_panic( - &json!([1, 2, 3]), - &json!([1, 2, 3]), - Config::new(CompareMode::Strict).numeric_mode(NumericMode::Strict) - ) - .is_ok()); - - assert!(assert_json_matches_no_panic( - &json!([1, 2, 3]), - &json!("foo"), - Config::new(CompareMode::Strict).numeric_mode(NumericMode::Strict) - ) - .is_err()); + let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::Strict); + assert!(assert_json_matches_no_panic(&json!([1, 2, 3]), &json!([1, 2, 3]), &config,).is_ok()); + + assert!(assert_json_matches_no_panic(&json!([1, 2, 3]), &json!("foo"), &config).is_err()); } #[derive(Serialize)]