pax_global_header00006660000000000000000000000064142571432570014524gustar00rootroot0000000000000052 comment=bca0d2c590808274298d939e0533da79cd09076d assert-json-diff-2.0.2/000077500000000000000000000000001425714325700147035ustar00rootroot00000000000000assert-json-diff-2.0.2/.github/000077500000000000000000000000001425714325700162435ustar00rootroot00000000000000assert-json-diff-2.0.2/.github/workflows/000077500000000000000000000000001425714325700203005ustar00rootroot00000000000000assert-json-diff-2.0.2/.github/workflows/rust-ci.yml000066400000000000000000000046741425714325700224240ustar00rootroot00000000000000on: push: branches: - main tags: - "*" pull_request: name: CI jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true # make sure all code has been formatted with rustfmt - run: rustup component add rustfmt - name: check rustfmt uses: actions-rs/cargo@v1 with: command: fmt args: -- --check --color always # run clippy to verify we have no warnings - run: rustup component add clippy - name: cargo fetch uses: actions-rs/cargo@v1 with: command: fetch - name: cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: --lib --tests -- -D warnings test: name: Test strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: cargo fetch uses: actions-rs/cargo@v1 with: command: fetch - name: cargo test uses: actions-rs/cargo@v1 with: command: test args: --release publish-check: name: Publish Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: cargo fetch uses: actions-rs/cargo@v1 with: command: fetch - name: cargo publish check uses: actions-rs/cargo@v1 with: command: publish args: --dry-run # Remove this job if you don't publish the crate(s) from this repo # You must add a crates.io API token to your GH secrets called CRATES_IO_TOKEN publish: name: Publish needs: [test, publish-check] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: cargo fetch uses: actions-rs/cargo@v1 with: command: fetch - name: cargo publish uses: actions-rs/cargo@v1 env: CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} with: command: publish assert-json-diff-2.0.2/.gitignore000066400000000000000000000000361425714325700166720ustar00rootroot00000000000000/target **/*.rs.bk Cargo.lock assert-json-diff-2.0.2/CHANGELOG.md000066400000000000000000000041631425714325700165200ustar00rootroot00000000000000# Change Log All user visible changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/), as described for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/text/1105-api-evolution.md) ## Unreleased - None. ### Breaking changes None. ## 2.0.2 - 2022-06-29 - Don't move the `Value`s being compared in `assert_json_matches` ## 2.0.1 - 2021-02-14 - Add maintenance status to readme and `Cargo.toml`. ## 2.0.0 - 2021-01-23 ## Unreleased - A less strict numeric mode for comparisons is now supported. The `AssumeFloat` mode will make `1 == 1.0`. This mode can be set via `Config::numeric_mode`. - A panicking `assert_json_matches` macro has been added which takes a `Config`. - Remove dependency on "extend". ### Breaking changes - Some breaking changes have been made to support customizing how the JSON values are compared: - `assert_json_eq_no_panic` and `assert_json_include_no_panic` have been replaced by `assert_json_matches_no_panic` which takes a `Config` that describes how the comparison should work. - This setup will support adding further customizations without more breaking changes. ## 1.1.0 - 2020-07-12 - All methods now accept any `T: Serialize` rather than just `serde_json::Value`. ## 1.0.3 - 2020-02-21 - Introduce non-panicking functions with `assert_json_include_no_panic` and `assert_json_eq_no_panic`. ## 1.0.2 - 2020-02-19 - Internal diffing algorithm simplified. There should be no external changes. Some error messages might have changed, but everything that passed/failed before should still do the same. ## 1.0.1 - 2019-10-24 - Update to 2018 edition ## 1.0.0 - 2019-02-15 ### Fixed - Make macros work with trailing comma ## 0.2.1 - 2018-11-15 ### Fixed - Fix wrong error message when a JSON atom was missing from actual. ## 0.2.0 - 2018-11-16 ### Added - Add `assert_json_include`. It does partial matching the same way the old `assert_json_eq` did. ### Changed - Change `assert_json_eq` do exact matching. If the two values are not exactly the same, it'll panic. ## 0.1.0 - 2018-10-17 Initial release. assert-json-diff-2.0.2/Cargo.toml000066400000000000000000000012511425714325700166320ustar00rootroot00000000000000[package] version = "2.0.2" authors = ["David Pedersen "] categories = ["development-tools"] description = "Easily compare two JSON values and get great output" homepage = "https://github.com/davidpdrsn/assert-json-diff" keywords = ["serde_json", "json", "testing"] license = "MIT" name = "assert-json-diff" readme = "README.md" repository = "https://github.com/davidpdrsn/assert-json-diff.git" documentation = "https://docs.rs/assert-json-diff" edition = "2018" [dependencies] serde_json = "1" serde = "1" [dev-dependencies] version-sync = "0.8" serde = { version = "1", features = ["derive"] } [badges] maintenance = { status = "passively-maintained" } assert-json-diff-2.0.2/LICENSE000066400000000000000000000020421425714325700157060ustar00rootroot00000000000000Copyright (c) 2019 David Pedersen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. assert-json-diff-2.0.2/README.md000066400000000000000000000071751425714325700161740ustar00rootroot00000000000000[![Crates.io](https://img.shields.io/crates/v/assert-json-diff.svg)](https://crates.io/crates/assert-json-diff) [![Docs](https://docs.rs/assert-json-diff/badge.svg)](https://docs.rs/assert-json-diff) [![dependency status](https://deps.rs/repo/github/davidpdrsn/assert-json-diff/status.svg)](https://deps.rs/repo/github/davidpdrsn/assert-json-diff) [![Build status](https://github.com/davidpdrsn/assert-json-diff/workflows/CI/badge.svg)](https://github.com/davidpdrsn/assert-json-diff/actions) ![maintenance-status](https://img.shields.io/badge/maintenance-passively--maintained-yellowgreen.svg) # assert-json-diff This crate includes macros for comparing two serializable values by diffing their JSON representations. It is designed to give much more helpful error messages than the standard [`assert_eq!`]. It basically does a diff of the two objects and tells you the exact differences. This is useful when asserting that two large JSON objects are the same. It uses the [serde] and [serde_json] to perform the serialization. [serde]: https://crates.io/crates/serde [serde_json]: https://crates.io/crates/serde_json [`assert_eq!`]: https://doc.rust-lang.org/std/macro.assert_eq.html ### Partial matching If you want to assert that one JSON value is "included" in another use [`assert_json_include`](macro.assert_json_include.html): ```rust use assert_json_diff::assert_json_include; use serde_json::json; let a = json!({ "data": { "users": [ { "id": 1, "country": { "name": "Denmark" } }, { "id": 24, "country": { "name": "Denmark" } } ] } }); let b = json!({ "data": { "users": [ { "id": 1, "country": { "name": "Sweden" } }, { "id": 2, "country": { "name": "Denmark" } } ] } }); assert_json_include!(actual: a, expected: b) ``` This will panic with the error message: ``` json atoms at path ".data.users[0].country.name" are not equal: expected: "Sweden" actual: "Denmark" json atoms at path ".data.users[1].id" are not equal: expected: 2 actual: 24 ``` [`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 of the JSON without having to specify the whole thing. For example this test passes: ```rust use assert_json_diff::assert_json_include; use serde_json::json; assert_json_include!( actual: json!({ "a": { "b": 1 }, }), expected: json!({ "a": {}, }) ) ``` However `expected` cannot contain additional data so this test fails: ```rust use assert_json_diff::assert_json_include; use serde_json::json; assert_json_include!( actual: json!({ "a": {}, }), expected: json!({ "a": { "b": 1 }, }) ) ``` That will print ``` json atom at path ".a.b" is missing from actual ``` ### Exact matching If you want to ensure two JSON values are *exactly* the same, use [`assert_json_eq`](macro.assert_json_eq.html). ```rust use assert_json_diff::assert_json_eq; use serde_json::json; assert_json_eq!( json!({ "a": { "b": 1 } }), json!({ "a": {} }) ) ``` This will panic with the error message: ``` json atom at path ".a.b" is missing from lhs ``` ### Further customization You can use [`assert_json_matches`] to further customize the comparison. License: MIT assert-json-diff-2.0.2/bin/000077500000000000000000000000001425714325700154535ustar00rootroot00000000000000assert-json-diff-2.0.2/bin/release000077500000000000000000000021421425714325700170200ustar00rootroot00000000000000#!/bin/bash set -e confirm() { while true; do read -p "$1? Please double check. y/n? " yn case $yn in [Yy]* ) break;; [Nn]* ) exit 1;; * ) echo "Please answer yes or no.";; esac done } cargo fmt --all -- --check echo "✔ code formatting looks good!" cargo check echo "✔ types look good" cargo readme > README.md echo "✔ README.md compiled" cargo test > /dev/null echo "✔ tests are passing" confirm "Updated Cargo.toml" confirm "Updated CHANGELOG.md" version="$1" version_without_v="`sed \"s/v//g\" <(echo $version)`" if (echo $version | egrep "v\d+\.\d+\.\d+" > /dev/null) then confirm "Ready to release $version (as $version_without_v)?" else echo "Invalid version number: $1" exit 1 fi version_in_toml=$(cat Cargo.toml | egrep "^version = \"$version_without_v\"") if [[ "$version_in_toml" == "version = \"$version_without_v\"" ]] then true else echo "Cargo.toml isn't set to version $version_without_v" fi GIT_COMMITTER_DATE=$(git log -n1 --pretty=%aD) git tag -a -m "Release $version" $version git push --tags cargo publish --dry-run cargo publish || true assert-json-diff-2.0.2/src/000077500000000000000000000000001425714325700154725ustar00rootroot00000000000000assert-json-diff-2.0.2/src/core_ext.rs000066400000000000000000000021361425714325700176520ustar00rootroot00000000000000pub trait Indent { fn indent(&self, level: u32) -> String; } impl Indent for T where T: ToString, { fn indent(&self, level: u32) -> String { let mut indent = String::new(); for _ in 0..level { indent.push(' '); } self.to_string() .lines() .map(|line| format!("{}{}", indent, line)) .collect::>() .join("\n") } } pub trait Indexes { fn indexes(&self) -> Vec; } impl Indexes for Vec { fn indexes(&self) -> Vec { if self.is_empty() { vec![] } else { (0..=self.len() - 1).collect() } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_indent() { assert_eq!(" foo", "foo".indent(2)); assert_eq!(" foo\n bar", "foo\nbar".indent(2)); } #[test] fn test_indexes() { let empty: Vec = vec![]; let empty_indexes: Vec = vec![]; assert_eq!(empty.indexes(), empty_indexes); assert_eq!(vec!['a', 'b'].indexes(), vec![0, 1]); } } assert-json-diff-2.0.2/src/diff.rs000066400000000000000000000427661425714325700167670ustar00rootroot00000000000000use crate::core_ext::{Indent, Indexes}; use crate::{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> { let mut acc = vec![]; diff_with(lhs, rhs, config, Path::Root, &mut acc); acc } fn diff_with<'a>( lhs: &'a Value, rhs: &'a Value, config: Config, path: Path<'a>, acc: &mut Vec>, ) { let mut folder = DiffFolder { rhs, path, acc, config, }; fold_json(lhs, &mut folder); } #[derive(Debug)] struct DiffFolder<'a, 'b> { rhs: &'a Value, path: Path<'a>, acc: &'b mut Vec>, config: Config, } macro_rules! direct_compare { ($name:ident) => { fn $name(&mut self, lhs: &'a Value) { if self.rhs != lhs { self.acc.push(Difference { lhs: Some(lhs), rhs: Some(&self.rhs), path: self.path.clone(), config: self.config.clone(), }); } } }; } impl<'a, 'b> DiffFolder<'a, 'b> { direct_compare!(on_null); direct_compare!(on_bool); direct_compare!(on_string); fn on_number(&mut self, lhs: &'a Value) { let is_equal = match self.config.numeric_mode { NumericMode::Strict => self.rhs == lhs, NumericMode::AssumeFloat => self.rhs.as_f64() == lhs.as_f64(), }; if !is_equal { 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 let Some(rhs) = self.rhs.as_array() { let lhs = lhs.as_array().unwrap(); match self.config.compare_mode { CompareMode::Inclusive => { for (idx, rhs) in rhs.iter().enumerate() { 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) } else { self.acc.push(Difference { lhs: None, rhs: Some(&self.rhs), path, config: self.config.clone(), }); } } } CompareMode::Strict => { let all_keys = rhs .indexes() .into_iter() .chain(lhs.indexes()) .collect::>(); for key in all_keys { let path = self.path.append(Key::Idx(key)); match (lhs.get(key), rhs.get(key)) { (Some(lhs), Some(rhs)) => { diff_with(lhs, rhs, self.config.clone(), path, self.acc); } (None, Some(rhs)) => { self.acc.push(Difference { lhs: None, rhs: Some(rhs), path, config: self.config.clone(), }); } (Some(lhs), None) => { self.acc.push(Difference { lhs: Some(lhs), rhs: None, path, config: self.config.clone(), }); } (None, None) => { unreachable!("at least one of the maps should have the key") } } } } } } else { self.acc.push(Difference { lhs: Some(lhs), rhs: Some(&self.rhs), path: self.path.clone(), config: self.config.clone(), }); } } fn on_object(&mut self, lhs: &'a Value) { if let Some(rhs) = self.rhs.as_object() { let lhs = lhs.as_object().unwrap(); match self.config.compare_mode { CompareMode::Inclusive => { for (key, rhs) in rhs.iter() { 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) } else { self.acc.push(Difference { lhs: None, rhs: Some(&self.rhs), path, config: self.config.clone(), }); } } } CompareMode::Strict => { let all_keys = rhs.keys().chain(lhs.keys()).collect::>(); for key in all_keys { let path = self.path.append(Key::Field(key)); match (lhs.get(key), rhs.get(key)) { (Some(lhs), Some(rhs)) => { diff_with(lhs, rhs, self.config.clone(), path, self.acc); } (None, Some(rhs)) => { self.acc.push(Difference { lhs: None, rhs: Some(rhs), path, config: self.config.clone(), }); } (Some(lhs), None) => { self.acc.push(Difference { lhs: Some(lhs), rhs: None, path, config: self.config.clone(), }); } (None, None) => { unreachable!("at least one of the maps should have the key") } } } } } } else { self.acc.push(Difference { lhs: Some(lhs), rhs: Some(&self.rhs), path: self.path.clone(), config: self.config.clone(), }); } } } #[derive(Debug, PartialEq)] pub(crate) struct Difference<'a> { path: Path<'a>, lhs: Option<&'a Value>, rhs: Option<&'a Value>, config: Config, } impl<'a> fmt::Display for Difference<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let json_to_string = |json: &Value| serde_json::to_string_pretty(json).unwrap(); match (&self.config.compare_mode, &self.lhs, &self.rhs) { (CompareMode::Inclusive, Some(actual), Some(expected)) => { writeln!(f, "json atoms at path \"{}\" are not equal:", self.path)?; writeln!(f, " expected:")?; writeln!(f, "{}", json_to_string(expected).indent(8))?; writeln!(f, " actual:")?; write!(f, "{}", json_to_string(actual).indent(8))?; } (CompareMode::Inclusive, None, Some(_expected)) => { write!( f, "json atom at path \"{}\" is missing from actual", self.path )?; } (CompareMode::Inclusive, Some(_actual), None) => { unreachable!("stuff missing actual wont produce an error") } (CompareMode::Inclusive, None, None) => unreachable!("can't both be missing"), (CompareMode::Strict, Some(lhs), Some(rhs)) => { writeln!(f, "json atoms at path \"{}\" are not equal:", self.path)?; writeln!(f, " lhs:")?; writeln!(f, "{}", json_to_string(lhs).indent(8))?; writeln!(f, " rhs:")?; write!(f, "{}", json_to_string(rhs).indent(8))?; } (CompareMode::Strict, None, Some(_)) => { write!(f, "json atom at path \"{}\" is missing from lhs", self.path)?; } (CompareMode::Strict, Some(_), None) => { write!(f, "json atom at path \"{}\" is missing from rhs", self.path)?; } (CompareMode::Strict, None, None) => unreachable!("can't both be missing"), } Ok(()) } } #[derive(Debug, Clone, PartialEq)] enum Path<'a> { Root, Keys(Vec>), } impl<'a> Path<'a> { fn append(&self, next: Key<'a>) -> Path<'a> { match self { Path::Root => Path::Keys(vec![next]), Path::Keys(list) => { let mut copy = list.clone(); copy.push(next); Path::Keys(copy) } } } } impl<'a> fmt::Display for Path<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Path::Root => write!(f, "(root)"), Path::Keys(keys) => { for key in keys { write!(f, "{}", key)?; } Ok(()) } } } } #[derive(Debug, Copy, Clone, PartialEq)] enum Key<'a> { Idx(usize), Field(&'a str), } impl<'a> fmt::Display for Key<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Key::Idx(idx) => write!(f, "[{}]", idx), Key::Field(key) => write!(f, ".{}", key), } } } fn fold_json<'a>(json: &'a Value, folder: &mut DiffFolder<'a, '_>) { match json { Value::Null => folder.on_null(json), Value::Bool(_) => folder.on_bool(json), Value::Number(_) => folder.on_number(json), Value::String(_) => folder.on_string(json), Value::Array(_) => folder.on_array(json), Value::Object(_) => folder.on_object(json), } } #[cfg(test)] mod test { #[allow(unused_imports)] use super::*; use serde_json::json; #[test] fn test_diffing_leaf_json() { let diffs = diff( &json!(null), &json!(null), Config::new(CompareMode::Inclusive), ); assert_eq!(diffs, vec![]); let diffs = diff( &json!(false), &json!(false), Config::new(CompareMode::Inclusive), ); assert_eq!(diffs, vec![]); let diffs = diff( &json!(true), &json!(true), Config::new(CompareMode::Inclusive), ); assert_eq!(diffs, vec![]); let diffs = diff( &json!(false), &json!(true), Config::new(CompareMode::Inclusive), ); assert_eq!(diffs.len(), 1); let diffs = diff( &json!(true), &json!(false), Config::new(CompareMode::Inclusive), ); assert_eq!(diffs.len(), 1); let actual = json!(1); let expected = json!(1); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs, vec![]); let actual = json!(2); let expected = json!(1); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); let actual = json!(1); let expected = json!(2); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); let actual = json!(1.0); let expected = json!(1.0); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs, vec![]); let actual = json!(1); let expected = json!(1.0); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); let actual = json!(1.0); let expected = json!(1); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); let actual = json!(1); let expected = json!(1.0); let diffs = diff( &actual, &expected, Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat), ); 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), ); assert_eq!(diffs, vec![]); } #[test] fn test_diffing_array() { // empty let actual = json!([]); let expected = json!([]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs, vec![]); let actual = json!([1]); let expected = json!([]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 0); let actual = json!([]); let expected = json!([1]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); // eq let actual = json!([1]); let expected = json!([1]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs, vec![]); // actual longer let actual = json!([1, 2]); let expected = json!([1]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs, vec![]); // expected longer let actual = json!([1]); let expected = json!([1, 2]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 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)); assert_eq!(diffs.len(), 1); // different types let actual = json!(1); let expected = json!([1]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); let actual = json!([1]); let expected = json!(1); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); } #[test] fn test_array_strict() { let actual = json!([]); let expected = json!([]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); assert_eq!(diffs.len(), 0); let actual = json!([1, 2]); let expected = json!([1, 2]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); assert_eq!(diffs.len(), 0); let actual = json!([1]); let expected = json!([1, 2]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); assert_eq!(diffs.len(), 1); let actual = json!([1, 2]); let expected = json!([1]); let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); assert_eq!(diffs.len(), 1); } #[test] fn test_object() { let actual = json!({}); let expected = json!({}); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs, vec![]); let actual = json!({ "a": 1 }); let expected = json!({ "a": 1 }); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 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)); assert_eq!(diffs, vec![]); let actual = json!({ "a": 1 }); let expected = json!({ "b": 1 }); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); let actual = json!({ "a": 1 }); let expected = json!({ "a": 2 }); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs.len(), 1); let actual = json!({ "a": { "b": true } }); let expected = json!({ "a": {} }); let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); assert_eq!(diffs, vec![]); } #[test] fn test_object_strict() { let lhs = json!({}); let rhs = json!({ "a": 1 }); let diffs = diff(&lhs, &rhs, Config::new(CompareMode::Strict)); assert_eq!(diffs.len(), 1); let lhs = json!({ "a": 1 }); let rhs = json!({}); let diffs = diff(&lhs, &rhs, Config::new(CompareMode::Strict)); assert_eq!(diffs.len(), 1); let json = json!({ "a": 1 }); let diffs = diff(&json, &json, Config::new(CompareMode::Strict)); assert_eq!(diffs, vec![]); } } assert-json-diff-2.0.2/src/lib.rs000066400000000000000000000430641425714325700166150ustar00rootroot00000000000000//! This crate includes macros for comparing two serializable values by diffing their JSON //! representations. It is designed to give much more helpful error messages than the standard //! [`assert_eq!`]. It basically does a diff of the two objects and tells you the exact //! differences. This is useful when asserting that two large JSON objects are the same. //! //! It uses the [serde] and [serde_json] to perform the serialization. //! //! [serde]: https://crates.io/crates/serde //! [serde_json]: https://crates.io/crates/serde_json //! [`assert_eq!`]: https://doc.rust-lang.org/std/macro.assert_eq.html //! //! ## Partial matching //! //! If you want to assert that one JSON value is "included" in another use //! [`assert_json_include`](macro.assert_json_include.html): //! //! ```should_panic //! use assert_json_diff::assert_json_include; //! use serde_json::json; //! //! let a = json!({ //! "data": { //! "users": [ //! { //! "id": 1, //! "country": { //! "name": "Denmark" //! } //! }, //! { //! "id": 24, //! "country": { //! "name": "Denmark" //! } //! } //! ] //! } //! }); //! //! let b = json!({ //! "data": { //! "users": [ //! { //! "id": 1, //! "country": { //! "name": "Sweden" //! } //! }, //! { //! "id": 2, //! "country": { //! "name": "Denmark" //! } //! } //! ] //! } //! }); //! //! assert_json_include!(actual: a, expected: b) //! ``` //! //! This will panic with the error message: //! //! ```text //! json atoms at path ".data.users[0].country.name" are not equal: //! expected: //! "Sweden" //! actual: //! "Denmark" //! //! json atoms at path ".data.users[1].id" are not equal: //! expected: //! 2 //! actual: //! 24 //! ``` //! //! [`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 //! of the JSON without having to specify the whole thing. For example this test passes: //! //! ``` //! use assert_json_diff::assert_json_include; //! use serde_json::json; //! //! assert_json_include!( //! actual: json!({ //! "a": { "b": 1 }, //! }), //! expected: json!({ //! "a": {}, //! }) //! ) //! ``` //! //! However `expected` cannot contain additional data so this test fails: //! //! ```should_panic //! use assert_json_diff::assert_json_include; //! use serde_json::json; //! //! assert_json_include!( //! actual: json!({ //! "a": {}, //! }), //! expected: json!({ //! "a": { "b": 1 }, //! }) //! ) //! ``` //! //! That will print //! //! ```text //! json atom at path ".a.b" is missing from actual //! ``` //! //! ## Exact matching //! //! If you want to ensure two JSON values are *exactly* the same, use [`assert_json_eq`](macro.assert_json_eq.html). //! //! ```rust,should_panic //! use assert_json_diff::assert_json_eq; //! use serde_json::json; //! //! assert_json_eq!( //! json!({ "a": { "b": 1 } }), //! json!({ "a": {} }) //! ) //! ``` //! //! This will panic with the error message: //! //! ```text //! json atom at path ".a.b" is missing from lhs //! ``` //! //! ## Further customization //! //! You can use [`assert_json_matches`] to further customize the comparison. #![deny( missing_docs, unused_imports, missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, unused_qualifications, unknown_lints )] use diff::diff; use serde::Serialize; mod core_ext; mod diff; /// Compare two JSON values for an inclusive match. /// /// It allows `actual` to contain additional data. If you want an exact match use /// [`assert_json_eq`](macro.assert_json_eq.html) instead. /// /// See [crate documentation](index.html) for examples. #[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) ) }}; (expected: $expected:expr, actual: $actual:expr $(,)?) => {{ $crate::assert_json_include!(actual: $actual, expected: $expected) }}; } /// Compare two JSON values for an exact match. /// /// If you want an inclusive match use [`assert_json_include`](macro.assert_json_include.html) instead. /// /// See [crate documentation](index.html) for examples. #[macro_export] macro_rules! assert_json_eq { ($lhs:expr, $rhs:expr $(,)?) => {{ $crate::assert_json_matches!($lhs, $rhs, $crate::Config::new($crate::CompareMode::Strict)) }}; } /// Compare two JSON values according to a configuration. /// /// ``` /// use assert_json_diff::{ /// CompareMode, /// Config, /// NumericMode, /// assert_json_matches, /// }; /// use serde_json::json; /// /// let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat); /// /// assert_json_matches!( /// json!({ /// "a": { "b": [1, 2, 3.0] }, /// }), /// json!({ /// "a": { "b": [1, 2.0, 3] }, /// }), /// config, /// ) /// ``` /// /// When using `CompareMode::Inclusive` the first argument is `actual` and the second argument is /// `expected`. Example: /// /// ``` /// # use assert_json_diff::{ /// # CompareMode, /// # Config, /// # NumericMode, /// # assert_json_matches, /// # assert_json_include, /// # }; /// # use serde_json::json; /// # /// // This /// assert_json_matches!( /// json!({ /// "a": { "b": 1 }, /// }), /// json!({ /// "a": {}, /// }), /// Config::new(CompareMode::Inclusive), /// ); /// /// // Is the same as this /// assert_json_include!( /// actual: json!({ /// "a": { "b": 1 }, /// }), /// expected: json!({ /// "a": {}, /// }), /// ); /// ``` #[macro_export] macro_rules! assert_json_matches { ($lhs:expr, $rhs:expr, $config:expr $(,)?) => {{ if let Err(error) = $crate::assert_json_matches_no_panic(&$lhs, &$rhs, $config) { panic!("\n\n{}\n\n", error); } }}; } /// Compares two JSON values without panicking. /// /// Instead it returns a `Result` where the error is the message that would be passed to `panic!`. /// This is might be useful if you want to control how failures are reported and don't want to deal /// with panics. pub fn assert_json_matches_no_panic( lhs: &Lhs, rhs: &Rhs, config: Config, ) -> Result<(), String> where Lhs: Serialize, Rhs: Serialize, { let lhs = serde_json::to_value(lhs).unwrap_or_else(|err| { panic!( "Couldn't convert left hand side value to JSON. Serde error: {}", err ) }); let rhs = serde_json::to_value(rhs).unwrap_or_else(|err| { panic!( "Couldn't convert right hand side value to JSON. Serde error: {}", err ) }); let diffs = diff(&lhs, &rhs, config); if diffs.is_empty() { Ok(()) } else { let msg = diffs .into_iter() .map(|d| d.to_string()) .collect::>() .join("\n\n"); Err(msg) } } /// Configuration for how JSON values should be compared. #[derive(Debug, Clone, PartialEq, Eq)] #[allow(missing_copy_implementations)] pub struct Config { pub(crate) compare_mode: CompareMode, pub(crate) numeric_mode: NumericMode, } impl Config { /// Create a new [`Config`] using the given [`CompareMode`]. /// /// The default `numeric_mode` is be [`NumericMode::Strict`]. pub fn new(compare_mode: CompareMode) -> Self { Self { compare_mode, numeric_mode: NumericMode::Strict, } } /// Change the config's numeric mode. /// /// The default `numeric_mode` is be [`NumericMode::Strict`]. pub fn numeric_mode(mut self, numeric_mode: NumericMode) -> Self { self.numeric_mode = numeric_mode; self } /// Change the config's compare mode. pub fn compare_mode(mut self, compare_mode: CompareMode) -> Self { self.compare_mode = compare_mode; self } } /// Mode for how JSON values should be compared. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum CompareMode { /// The two JSON values don't have to be exactly equal. The "actual" value is only required to /// be "contained" inside "expected". See [crate documentation](index.html) for examples. /// /// The mode used with [`assert_json_include`]. Inclusive, /// The two JSON values must be exactly equal. /// /// The mode used with [`assert_json_eq`]. Strict, } /// How should numbers be compared. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum NumericMode { /// Different numeric types aren't considered equal. Strict, /// All numeric types are converted to float before comparison. AssumeFloat, } #[cfg(test)] mod tests { use super::*; use serde_json::{json, Value}; use std::fmt::Write; #[test] fn boolean_root() { let result = test_partial_match(json!(true), json!(true)); assert_output_eq(result, Ok(())); let result = test_partial_match(json!(false), json!(false)); assert_output_eq(result, Ok(())); let result = test_partial_match(json!(false), json!(true)); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: true actual: false"#), ); let result = test_partial_match(json!(true), json!(false)); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: false actual: true"#), ); } #[test] fn string_root() { let result = test_partial_match(json!("true"), json!("true")); assert_output_eq(result, Ok(())); let result = test_partial_match(json!("false"), json!("false")); assert_output_eq(result, Ok(())); let result = test_partial_match(json!("false"), json!("true")); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: "true" actual: "false""#), ); let result = test_partial_match(json!("true"), json!("false")); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: "false" actual: "true""#), ); } #[test] fn number_root() { let result = test_partial_match(json!(1), json!(1)); assert_output_eq(result, Ok(())); let result = test_partial_match(json!(0), json!(0)); assert_output_eq(result, Ok(())); let result = test_partial_match(json!(0), json!(1)); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: 1 actual: 0"#), ); let result = test_partial_match(json!(1), json!(0)); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: 0 actual: 1"#), ); } #[test] fn null_root() { let result = test_partial_match(json!(null), json!(null)); assert_output_eq(result, Ok(())); let result = test_partial_match(json!(null), json!(1)); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: 1 actual: null"#), ); let result = test_partial_match(json!(1), json!(null)); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: expected: null actual: 1"#), ); } #[test] fn into_object() { let result = test_partial_match(json!({ "a": true }), json!({ "a": true })); assert_output_eq(result, Ok(())); let result = test_partial_match(json!({ "a": false }), json!({ "a": true })); assert_output_eq( result, Err(r#"json atoms at path ".a" are not equal: expected: true actual: false"#), ); let result = test_partial_match(json!({ "a": { "b": true } }), json!({ "a": { "b": true } })); assert_output_eq(result, Ok(())); let result = test_partial_match(json!({ "a": true }), json!({ "a": { "b": true } })); assert_output_eq( result, Err(r#"json atoms at path ".a" are not equal: expected: { "b": true } actual: true"#), ); let result = test_partial_match(json!({}), json!({ "a": true })); assert_output_eq( result, Err(r#"json atom at path ".a" is missing from actual"#), ); let result = test_partial_match(json!({ "a": { "b": true } }), json!({ "a": true })); assert_output_eq( result, Err(r#"json atoms at path ".a" are not equal: expected: true actual: { "b": true }"#), ); } #[test] fn into_array() { let result = test_partial_match(json!([1]), json!([1])); assert_output_eq(result, Ok(())); let result = test_partial_match(json!([2]), json!([1])); assert_output_eq( result, Err(r#"json atoms at path "[0]" are not equal: expected: 1 actual: 2"#), ); let result = test_partial_match(json!([1, 2, 4]), json!([1, 2, 3])); assert_output_eq( result, Err(r#"json atoms at path "[2]" are not equal: expected: 3 actual: 4"#), ); let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2, 4]})); assert_output_eq( result, Err(r#"json atoms at path ".a[2]" are not equal: expected: 4 actual: 3"#), ); let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2]})); assert_output_eq(result, Ok(())); let result = test_partial_match(json!({ "a": [1, 2]}), json!({ "a": [1, 2, 3]})); assert_output_eq( result, Err(r#"json atom at path ".a[2]" is missing from actual"#), ); } #[test] fn exact_matching() { let result = test_exact_match(json!(true), json!(true)); assert_output_eq(result, Ok(())); let result = test_exact_match(json!("s"), json!("s")); assert_output_eq(result, Ok(())); let result = test_exact_match(json!("a"), json!("b")); assert_output_eq( result, Err(r#"json atoms at path "(root)" are not equal: lhs: "a" rhs: "b""#), ); let result = test_exact_match( json!({ "a": [1, { "b": 2 }] }), json!({ "a": [1, { "b": 3 }] }), ); assert_output_eq( result, Err(r#"json atoms at path ".a[1].b" are not equal: lhs: 2 rhs: 3"#), ); } #[test] fn exact_match_output_message() { let result = test_exact_match(json!({ "a": { "b": 1 } }), json!({ "a": {} })); assert_output_eq( result, Err(r#"json atom at path ".a.b" is missing from rhs"#), ); let result = test_exact_match(json!({ "a": {} }), json!({ "a": { "b": 1 } })); assert_output_eq( result, Err(r#"json atom at path ".a.b" is missing from lhs"#), ); } fn assert_output_eq(actual: Result<(), String>, expected: Result<(), &str>) { match (actual, expected) { (Ok(()), Ok(())) => {} (Err(actual_error), Ok(())) => { let mut f = String::new(); writeln!(f, "Did not expect error, but got").unwrap(); writeln!(f, "{}", actual_error).unwrap(); panic!("{}", f); } (Ok(()), Err(expected_error)) => { let expected_error = expected_error.to_string(); let mut f = String::new(); writeln!(f, "Expected error, but did not get one. Expected error:").unwrap(); writeln!(f, "{}", expected_error).unwrap(); panic!("{}", f); } (Err(actual_error), Err(expected_error)) => { let expected_error = expected_error.to_string(); if actual_error != expected_error { let mut f = String::new(); writeln!(f, "Errors didn't match").unwrap(); writeln!(f, "Expected:").unwrap(); writeln!(f, "{}", expected_error).unwrap(); writeln!(f, "Got:").unwrap(); writeln!(f, "{}", actual_error).unwrap(); panic!("{}", f); } } } } fn test_partial_match(lhs: Value, rhs: Value) -> Result<(), String> { 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-diff-2.0.2/tests/000077500000000000000000000000001425714325700160455ustar00rootroot00000000000000assert-json-diff-2.0.2/tests/integration_test.rs000066400000000000000000000102441425714325700217760ustar00rootroot00000000000000use assert_json_diff::{ assert_json_eq, assert_json_include, assert_json_matches, assert_json_matches_no_panic, CompareMode, Config, NumericMode, }; use serde::Serialize; use serde_json::json; #[test] fn can_pass() { assert_json_include!( actual: json!({ "a": { "b": true }, "c": [true, null, 1] }), expected: json!({ "a": { "b": true }, "c": [true, null, 1] }) ); assert_json_include!( actual: json!({ "a": { "b": true } }), expected: json!({ "a": {} }) ); assert_json_include!( actual: json!({ "a": { "b": true } }), expected: json!({ "a": {} }), ); assert_json_include!( expected: json!({ "a": {} }), actual: json!({ "a": { "b": true } }), ); } #[test] #[should_panic] fn can_fail() { assert_json_include!( actual: json!({ "a": { "b": true }, "c": [true, null, 1] }), expected: json!({ "a": { "b": false }, "c": [false, null, {}] }) ); } #[test] #[should_panic] fn different_numeric_types_include_should_fail() { assert_json_include!( actual: json!({ "a": { "b": true }, "c": 1 }), expected: json!({ "a": { "b": true }, "c": 1.0 }) ); } #[test] #[should_panic] fn different_numeric_types_eq_should_fail() { assert_json_eq!( json!({ "a": { "b": true }, "c": 1 }), json!({ "a": { "b": true }, "c": 1.0 }) ); } #[test] 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.compare_mode(CompareMode::Strict)) } #[test] fn can_pass_with_exact_match() { assert_json_eq!(json!({ "a": { "b": true } }), json!({ "a": { "b": true } })); assert_json_eq!(json!({ "a": { "b": true } }), json!({ "a": { "b": true } }),); } #[test] #[should_panic] fn can_fail_with_exact_match() { assert_json_eq!(json!({ "a": { "b": true } }), json!({ "a": {} })); } #[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()); } #[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()); } #[derive(Serialize)] struct User { id: i32, username: String, } #[test] fn include_with_serializable() { let user = User { id: 1, username: "bob".to_string(), }; assert_json_include!( actual: json!({ "id": 1, "username": "bob", "email": "bob@example.com" }), expected: user, ); } #[test] fn include_with_serializable_ref() { let user = User { id: 1, username: "bob".to_string(), }; assert_json_include!( actual: &json!({ "id": 1, "username": "bob", "email": "bob@example.com" }), expected: &user, ); } #[test] fn eq_with_serializable() { let user = User { id: 1, username: "bob".to_string(), }; assert_json_eq!( json!({ "id": 1, "username": "bob" }), user, ); } #[test] fn eq_with_serializable_ref() { let user = User { id: 1, username: "bob".to_string(), }; assert_json_eq!( &json!({ "id": 1, "username": "bob" }), &user, ); } assert-json-diff-2.0.2/tests/version-numbers.rs000066400000000000000000000003211425714325700215450ustar00rootroot00000000000000#[macro_use] extern crate version_sync; #[test] fn test_readme_deps() { assert_markdown_deps_updated!("README.md"); } #[test] fn test_html_root_url() { assert_html_root_url_updated!("src/lib.rs"); }