diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 0000000..e367da5
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,44 @@
+name: coverage instrument based
+
+on: [ push, pull_request ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install latest nightly
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: nightly
+ override: true
+ components: rustfmt, clippy, llvm-tools-preview
+
+ - name: Install lcov
+ run: sudo apt-get install lcov
+
+ - name: install grcov
+ run: cargo install grcov
+
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Run grcov
+ env:
+ PROJECT_NAME: "json-diff"
+ RUSTDOCFLAGS: "-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
+ RUSTFLAGS: "-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
+ CARGO_INCREMENTAL: 0
+ run: |
+ cargo +nightly build --verbose --no-default-features
+ cargo +nightly test --verbose --no-default-features
+ grcov . -s . --binary-path ./target/debug/ -t lcov --llvm --branch --ignore-not-existing --ignore="/*" --ignore="target/*" --ignore="tests/*" -o lcov.info
+
+ - name: Push grcov results to Coveralls via GitHub Action
+ uses: coverallsapp/github-action@v1.0.1
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path-to-lcov: "lcov.info"
diff --git a/.gitignore b/.gitignore
index 53eaa21..bb421d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
/target
**/*.rs.bk
+.idea
+Cargo.lock
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..7d355e2
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,64 @@
+{
+ // Verwendet IntelliSense zum Ermitteln möglicher Attribute.
+ // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
+ // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Debug unit tests in library 'json_diff'",
+ "cargo": {
+ "args": [
+ "test",
+ "--no-run",
+ "--lib",
+ "--package=json_diff_ng"
+ ],
+ "filter": {
+ "name": "json_diff",
+ "kind": "lib"
+ }
+ },
+ "args": [],
+ "cwd": "${workspaceFolder}"
+ },
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Debug executable 'json_diff'",
+ "cargo": {
+ "args": [
+ "build",
+ "--bin=json_diff",
+ "--package=json_diff_ng"
+ ],
+ "filter": {
+ "name": "json_diff",
+ "kind": "bin"
+ }
+ },
+ "args": [],
+ "cwd": "${workspaceFolder}"
+ },
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Debug unit tests in executable 'json_diff'",
+ "cargo": {
+ "args": [
+ "test",
+ "--no-run",
+ "--bin=json_diff",
+ "--package=json_diff_ng"
+ ],
+ "filter": {
+ "name": "json_diff",
+ "kind": "bin"
+ }
+ },
+ "args": [],
+ "cwd": "${workspaceFolder}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
deleted file mode 100644
index caff2f6..0000000
--- a/Cargo.lock
+++ /dev/null
@@ -1,244 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-[[package]]
-name = "ansi_term"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "atty"
-version = "0.2.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "bitflags"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "clap"
-version = "2.33.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "colored"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "heck"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "itoa"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "json_diff"
-version = "0.1.2"
-dependencies = [
- "colored 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)",
- "structopt 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "libc"
-version = "0.2.65"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "maplit"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "proc-macro-error"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "ryu"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "serde"
-version = "1.0.102"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "serde_json"
-version = "1.0.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "strsim"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "structopt"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "structopt-derive 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "structopt-derive"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro-error 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "syn"
-version = "1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "textwrap"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "vec_map"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "winapi"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[metadata]
-"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
-"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
-"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
-"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
-"checksum colored 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "433e7ac7d511768127ed85b0c4947f47a254131e37864b2dc13f52aa32cd37e5"
-"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
-"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
-"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-"checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8"
-"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
-"checksum proc-macro-error 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097"
-"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
-"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
-"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
-"checksum serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4b39bd9b0b087684013a792c59e3e07a46a01d2322518d8a1104641a0b1be0"
-"checksum serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)" = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2"
-"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-"checksum structopt 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "30b3a3e93f5ad553c38b3301c8a0a0cec829a36783f6a0c467fc4bf553a5f5bf"
-"checksum structopt-derive 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea692d40005b3ceba90a9fe7a78fa8d4b82b0ce627eebbffc329aab850f3410e"
-"checksum syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "661641ea2aa15845cddeb97dad000d22070bb5c1fb456b96c1cba883ec691e92"
-"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
-"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20"
-"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
-"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
-"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
-"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/Cargo.toml b/Cargo.toml
index a124a1b..56b6355 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,29 +1,39 @@
[package]
-name = "json_diff"
-version = "0.1.2"
-authors = ["ksceriath"]
-edition = "2018"
+name = "json_diff_ng"
+version = "0.6.0"
+authors = ["ChrisRega", "ksceriath"]
+edition = "2021"
license = "Unlicense"
-description = "A small diff tool utility for comparing jsons"
+description = "A JSON diff library, featuring deep-sorting and key exclusion by regex. CLI is included."
readme = "README.md"
-homepage = "https://github.com/ksceriath/json-diff"
-repository = "https://github.com/ksceriath/json-diff"
-keywords = ["cli", "diff", "json"]
-categories = ["command-line-utilities"]
+homepage = "https://github.com/ChrisRega/json-diff"
+repository = "https://github.com/ChrisRega/json-diff"
+categories = ["command-line-utilities", "development-tools"]
+keywords = ["json-structural-diff", "json-diff", "diff", "json", "cli"]
[lib]
-name = "json_diff"
+name = "json_diff_ng"
path = "src/lib.rs"
crate-type = ["lib"]
[[bin]]
-name = "json_diff"
+name = "json_diff_ng"
path = "src/main.rs"
+required-features = ["CLI"]
+
+[features]
+default = ["CLI"]
+CLI = ["dep:clap"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-serde_json = "1.0.41"
-maplit = "1.0.2"
-colored = "1.9.0"
-structopt = "0.3.5"
+thiserror = "1.0"
+vg_errortools = "0.1"
+serde_json = { version = "1.0", features = ["preserve_order"] }
+diffs = "0.5"
+regex = "1.10"
+clap = { version = "4.5", features = ["derive"], optional = true }
+
+[dev-dependencies]
+maplit = "1.0"
diff --git a/README.md b/README.md
index 0cb5852..03f239a 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,59 @@
-# json-diff
+# json-diff-ng
-json-diff is a command line utility to compare two jsons.
+[](https://crates.io/crates/json_diff_ng)
+[](https://docs.rs/json_diff_ng)
+
+[](https://coveralls.io/github/ChrisRega/json-diff?branch=master)
+[](LICENSE)
-Input can be fed as inline strings or through files.
-For readability, output is neatly differentiated into three categories: keys with different values, and keys not present in either of the objects.
-Only missing or unequal keys are printed in output to reduce the verbosity.
+## Contributors:
+
+
+
+
+
+## Library
+
+json_diff_ng can be used to get diffs of json-serializable structures in rust.
+
+### Usage example
-## Screenshot of diff results
+```rust
+use json_diff::compare_strs;
+let data1 = r#"["a",{"c": ["d","f"] },"b"]"#;
+let data2 = r#"["b",{"c": ["e","d"] },"a"]"#;
+let diffs = compare_strs(data1, data2, true, & []).unwrap();
+assert!(!diffs.is_empty());
+let diffs = diffs.unequal_values.get_diffs();
+assert_eq!(diffs.len(), 1);
+assert_eq!(
+ diffs.first().unwrap().to_string(),
+ r#".[0].c.[1].("f" != "e")"#
+);
+```
-[](https://github.com/ksceriath/json-diff/blob/master/Screenshot.png)
+See [docs.rs](https://docs.rs/json_diff_ng) for more details.
+
+## CLI
+
+json-diff is a command line utility to compare two jsons.
+
+Input can be fed as inline strings or through files.
+For readability, output is neatly differentiated into three categories: keys with different values, and keys not present
+in either of the objects.
+Only missing or unequal keys are printed in output to reduce the verbosity.
Usage Example:
-`$ json_diff f source1.json source2.json`
-`$ json_diff d '{...}' '{...}'`
+`$ json_diff file source1.json source2.json`
+`$ json_diff direct '{...}' '{...}'`
Option:
-f : read input from json files
-d : read input from command line
+file : read input from json files
+direct : read input from command line
### Installation
-Currently, json-diff is available through crates.io (apart from building this repo directly). For crate installation,
-* Install cargo, through rustup
-`$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
-* Install json-diff
-`$ cargo install json_diff`
-
+`$ cargo install json_diff_ng`
diff --git a/src/constants.rs b/src/constants.rs
deleted file mode 100644
index 1074046..0000000
--- a/src/constants.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use colored::*;
-use std::fmt;
-
-// PartialEq is added for the sake of Test case that uses assert_eq
-#[derive(Debug, PartialEq)]
-pub enum Message {
- BadOption,
- SOURCE1,
- SOURCE2,
- JSON1,
- JSON2,
- UnknownError,
- NoMismatch,
- RootMismatch,
- LeftExtra,
- RightExtra,
- Mismatch,
-}
-
-impl fmt::Display for Message {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let message = match self {
- Message::BadOption => "Invalid option.",
- Message::SOURCE1 => "Could not read source1.",
- Message::SOURCE2 => "Could not read source2.",
- Message::JSON1 => "Could not parse source1.",
- Message::JSON2 => "Could not parse source2.",
- Message::UnknownError => "",
- Message::NoMismatch => "No mismatch was found.",
- Message::RootMismatch => "Mismatch at root.",
- Message::LeftExtra => "Extra on left",
- Message::RightExtra => "Extra on right",
- Message::Mismatch => "Mismatched",
- };
-
- write!(f, "{}", message.bold())
- }
-}
diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs
deleted file mode 100644
index 20fb04c..0000000
--- a/src/ds/key_node.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-use colored::*;
-use serde_json::Value;
-use std::collections::HashMap;
-
-#[derive(Debug, PartialEq)] // TODO check: do we need PartiaEq ?
-pub enum KeyNode {
- Nil,
- Value(Value, Value),
- Node(HashMap),
-}
-
-impl KeyNode {
- pub fn absolute_keys(&self, keys: &mut Vec, key_from_root: Option) {
- let val_key = |key: Option| {
- key.map(|mut s| {
- s.push_str(" ->");
- s
- })
- .unwrap_or(String::new())
- };
- let nil_key = |key: Option| key.unwrap_or(String::new());
- match self {
- KeyNode::Nil => keys.push(nil_key(key_from_root)),
- KeyNode::Value(a, b) => keys.push(format!(
- "{} [ {} :: {} ]",
- val_key(key_from_root),
- a.to_string().blue().bold(),
- b.to_string().cyan().bold()
- )),
- KeyNode::Node(map) => {
- for (key, value) in map {
- value.absolute_keys(
- keys,
- Some(format!("{} {}", val_key(key_from_root.clone()), key)),
- )
- }
- }
- }
- }
-}
diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs
deleted file mode 100644
index 649739f..0000000
--- a/src/ds/mismatch.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use crate::ds::key_node::KeyNode;
-
-#[derive(Debug, PartialEq)]
-pub struct Mismatch {
- pub left_only_keys: KeyNode,
- pub right_only_keys: KeyNode,
- pub keys_in_both: KeyNode,
-}
-
-impl Mismatch {
- pub fn new(l: KeyNode, r: KeyNode, u: KeyNode) -> Mismatch {
- Mismatch {
- left_only_keys: l,
- right_only_keys: r,
- keys_in_both: u,
- }
- }
-}
diff --git a/src/ds/mod.rs b/src/ds/mod.rs
deleted file mode 100644
index c61babc..0000000
--- a/src/ds/mod.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod key_node;
-pub mod mismatch;
diff --git a/src/enums.rs b/src/enums.rs
new file mode 100644
index 0000000..2e95d9e
--- /dev/null
+++ b/src/enums.rs
@@ -0,0 +1,196 @@
+use std::collections::HashMap;
+use std::fmt::{Display, Formatter};
+
+use serde_json::Value;
+use thiserror::Error;
+use vg_errortools::FatIOError;
+
+#[derive(Debug, Error)]
+pub enum Error {
+ #[error("Misc error: {0}")]
+ Misc(String),
+ #[error("Error opening file: {0}")]
+ IOError(#[from] FatIOError),
+ #[error("Error parsing first json: {0}")]
+ JSON(#[from] serde_json::Error),
+ #[error("Regex compilation error: {0}")]
+ Regex(#[from] regex::Error),
+}
+
+impl From for Error {
+ fn from(value: String) -> Self {
+ Self::Misc(value)
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum DiffTreeNode {
+ Null,
+ Value(Value, Value),
+ Node(HashMap),
+ Array(Vec<(usize, DiffTreeNode)>),
+}
+
+impl<'a> DiffTreeNode {
+ pub fn get_diffs(&'a self) -> Vec> {
+ let mut buf = Vec::new();
+ self.follow_path(&mut buf, &[]);
+ buf
+ }
+
+ pub fn follow_path<'b>(
+ &'a self,
+ diffs: &mut Vec>,
+ offset: &'b [PathElement<'a>],
+ ) {
+ match self {
+ DiffTreeNode::Null => {
+ let is_map_child = offset
+ .last()
+ .map(|o| matches!(o, PathElement::Object(_)))
+ .unwrap_or_default();
+ if is_map_child {
+ diffs.push(DiffEntry {
+ path: offset.to_vec(),
+ values: None,
+ });
+ }
+ }
+ DiffTreeNode::Value(l, r) => diffs.push(DiffEntry {
+ path: offset.to_vec(),
+ values: Some((l, r)),
+ }),
+ DiffTreeNode::Node(o) => {
+ for (k, v) in o {
+ let mut new_offset = offset.to_vec();
+ new_offset.push(PathElement::Object(k));
+ v.follow_path(diffs, &new_offset);
+ }
+ }
+ DiffTreeNode::Array(v) => {
+ for (l, k) in v {
+ let mut new_offset = offset.to_vec();
+ new_offset.push(PathElement::ArrayEntry(*l));
+ k.follow_path(diffs, &new_offset);
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum DiffType {
+ RootMismatch,
+ LeftExtra,
+ RightExtra,
+ Mismatch,
+}
+
+impl Display for DiffType {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ let msg = match self {
+ DiffType::RootMismatch => "Mismatch at root.",
+ DiffType::LeftExtra => "Extra on left",
+ DiffType::RightExtra => "Extra on right",
+ DiffType::Mismatch => "Mismatched",
+ };
+ write!(f, "{}", msg)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum PathElement<'a> {
+ Object(&'a str),
+ ArrayEntry(usize),
+}
+
+impl<'a> PathElement<'a> {
+ pub fn resolve<'b>(&self, v: &'b serde_json::Value) -> Option<&'b serde_json::Value> {
+ match self {
+ PathElement::Object(o) => v.get(o),
+ PathElement::ArrayEntry(i) => v.get(*i),
+ }
+ }
+
+ pub fn resolve_mut<'b>(
+ &self,
+ v: &'b mut serde_json::Value,
+ ) -> Option<&'b mut serde_json::Value> {
+ match self {
+ PathElement::Object(o) => v.get_mut(o),
+ PathElement::ArrayEntry(i) => v.get_mut(*i),
+ }
+ }
+}
+
+/// A view on a single end-node of the [`DiffTreeNode`] tree.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct DiffEntry<'a> {
+ pub path: Vec>,
+ pub values: Option<(&'a serde_json::Value, &'a serde_json::Value)>,
+}
+
+impl<'a> DiffEntry<'a> {
+ pub fn resolve<'b>(&'a self, value: &'b serde_json::Value) -> Option<&'b serde_json::Value> {
+ let mut return_value = value;
+ for a in &self.path {
+ return_value = a.resolve(return_value)?;
+ }
+ Some(return_value)
+ }
+}
+
+impl Display for DiffEntry<'_> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ for element in &self.path {
+ write!(f, ".{element}")?;
+ }
+ if let Some((l, r)) = &self.values {
+ if l != r {
+ write!(f, ".({l} != {r})")?;
+ } else {
+ write!(f, ".({l})")?;
+ }
+ }
+ Ok(())
+ }
+}
+
+impl Display for PathElement<'_> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ PathElement::Object(o) => {
+ write!(f, "{o}")
+ }
+ PathElement::ArrayEntry(l) => {
+ write!(f, "[{l}]")
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use serde_json::json;
+
+ use crate::compare_serde_values;
+ use crate::sort::sort_value;
+
+ #[test]
+ fn test_resolve() {
+ let data1 = json! {["a",{"c": ["d","f"] },"b"]};
+ let data2 = json! {["b",{"c": ["e","d"] },"a"]};
+ let diffs = compare_serde_values(&data1, &data2, true, &[]).unwrap();
+ assert!(!diffs.is_empty());
+ let data1_sorted = sort_value(&data1, &[]);
+ let data2_sorted = sort_value(&data2, &[]);
+
+ let all_diffs = diffs.all_diffs();
+ assert_eq!(all_diffs.len(), 1);
+ let (_type, diff) = all_diffs.first().unwrap();
+ let val = diff.resolve(&data1_sorted);
+ assert_eq!(val.unwrap().as_str().unwrap(), "f");
+ let val = diff.resolve(&data2_sorted);
+ assert_eq!(val.unwrap().as_str().unwrap(), "e");
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 339800d..f181439 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,76 @@
-pub mod constants;
-pub mod ds;
+//! # Library for comparing JSON data structures
+//! ## Summary
+//! Main entry points are [`compare_strs`] to compare string slices and [`compare_serde_values`] to compare already parse [`serde_json::Value`]
+//! ## Example:
+//! ```rust
+//! use json_diff_ng::compare_strs;
+//! let data1 = r#"["a",{"c": ["d","f"] },"b"]"#;
+//! let data2 = r#"["b",{"c": ["e","d"] },"a"]"#;
+//! let diffs = compare_strs(data1, data2, true, &[]).unwrap();
+//! assert!(!diffs.is_empty());
+//! let diffs = diffs.unequal_values.get_diffs();
+//!
+//! assert_eq!(diffs.len(), 1);
+//! assert_eq!(
+//! diffs.first().unwrap().to_string(),
+//! r#".[0].c.[1].("f" != "e")"#
+//! );
+//! ```
+//! ## How to handle the results
+//! Results are returned in a triple of [`DiffTreeNode`] called [`Mismatch`].
+//! The triple consists of values only on the left side, values only on the right side and values on both sides that differ.
+//! Since tree traversal is not usually what you want to do on client side, [`DiffTreeNode`] offers [`DiffTreeNode::get_diffs`] to retrieve
+//! a flat list of [`DiffEntry`] which is more easily usable. The path in the json is collapsed into a vector of [`PathElement`] which can be used to follow the diff.
+//! Similarly, all diffs after an operation can be collected using [`Mismatch::all_diffs`].
+//!
+//! ### Just print everything
+//!
+//! ```rust
+//! use serde_json::json;
+//! use json_diff_ng::compare_serde_values;
+//! use json_diff_ng::sort::sort_value;
+//! let data1 = json! {["a",{"c": ["d","f"] },"b"]};
+//! let data2 = json! {["b",{"c": ["e","d"] },"a"]};
+//! let diffs = compare_serde_values(&data1, &data2, true, &[]).unwrap();
+//! for (d_type, d_path) in diffs.all_diffs() {
+//! let _message = format!("{d_type}: {d_path}");
+//! }
+//! ```
+//!
+//! ### Traversing the diff result JSONs
+//! ```rust
+//! use serde_json::json;
+//! use json_diff_ng::compare_serde_values;
+//! use json_diff_ng::sort::sort_value;
+//! let data1 = json! {["a",{"c": ["d","f"] },"b"]};
+//! let data2 = json! {["b",{"c": ["e","d"] },"a"]};
+//! let diffs = compare_serde_values(&data1, &data2, true, &[]).unwrap();
+//! assert!(!diffs.is_empty());
+//! // since we sorted for comparison, if we want to resolve the path, we need a sorted result as well.
+//! let data1_sorted = sort_value(&data1, &[]);
+//! let data2_sorted = sort_value(&data2, &[]);
+//! let all_diffs = diffs.all_diffs();
+//! assert_eq!(all_diffs.len(), 1);
+//! let (_type, diff) = all_diffs.first().unwrap();
+//! let val = diff.resolve(&data1_sorted);
+//! assert_eq!(val.unwrap().as_str().unwrap(), "f");
+//! let val = diff.resolve(&data2_sorted);
+//! assert_eq!(val.unwrap().as_str().unwrap(), "e");
+//! ```
+//!
+
+pub use enums::DiffEntry;
+pub use enums::DiffTreeNode;
+pub use enums::DiffType;
+pub use enums::Error;
+pub use enums::PathElement;
+pub use mismatch::Mismatch;
+pub use process::compare_serde_values;
+pub use process::compare_strs;
+
+pub mod enums;
+pub mod mismatch;
pub mod process;
+pub mod sort;
+
+pub type Result = std::result::Result;
diff --git a/src/main.rs b/src/main.rs
index 135b5fc..31b0497 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,145 +1,70 @@
-use std::{
- fmt, fs,
- io::{self, Write},
- process as proc,
- str::FromStr,
-};
+use clap::Parser;
+use clap::Subcommand;
-use colored::*;
-use structopt::StructOpt;
+use json_diff_ng::{compare_strs, Mismatch, Result};
-use json_diff::{
- constants::Message,
- ds::{key_node::KeyNode, mismatch::Mismatch},
- process::compare_jsons,
-};
-
-const HELP: &str = r#"
-Example:
-json_diff f source1.json source2.json
-json_diff d '{...}' '{...}'
-
-Option:
-f : read input from json files
-d : read input from command line"#;
-
-#[derive(Debug)]
-struct AppError {
- message: Message,
-}
-impl fmt::Display for AppError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}", self.message)
- }
+#[derive(Subcommand, Clone)]
+/// Input selection
+enum Mode {
+ /// File input
+ #[clap(short_flag = 'f')]
+ File { file_1: String, file_2: String },
+ /// Read from CLI
+ #[clap(short_flag = 'd')]
+ Direct { json_1: String, json_2: String },
}
-enum InputReadMode {
- D,
- F,
-}
-impl FromStr for InputReadMode {
- type Err = AppError;
- fn from_str(s: &str) -> Result {
- match s {
- "d" => Ok(InputReadMode::D),
- "f" => Ok(InputReadMode::F),
- _ => Err(Self::Err {
- message: Message::BadOption,
- }),
- }
- }
-}
+#[derive(Parser)]
+struct Args {
+ #[command(subcommand)]
+ cmd: Mode,
-#[derive(StructOpt)]
-#[structopt(about = HELP)]
-struct Cli {
- read_mode: InputReadMode,
- source1: String,
- source2: String,
-}
+ #[clap(short, long)]
+ /// deep-sort arrays before comparing
+ sort_arrays: bool,
-fn main() {
- let args = Cli::from_args();
+ #[clap(short, long)]
+ /// Exclude a given list of keys by regex.
+ exclude_keys: Option>,
+}
- let (data1, data2) = match args.read_mode {
- InputReadMode::D => (args.source1, args.source2),
- InputReadMode::F => {
- if let Ok(d1) = fs::read_to_string(args.source1) {
- if let Ok(d2) = fs::read_to_string(args.source2) {
- (d1, d2)
- } else {
- error_exit(Message::SOURCE2);
- }
- } else {
- error_exit(Message::SOURCE1);
- }
+fn main() -> Result<()> {
+ let args = Args::parse();
+ println!("Getting input");
+ let (json_1, json_2) = match args.cmd {
+ Mode::Direct { json_2, json_1 } => (json_1, json_2),
+ Mode::File { file_2, file_1 } => {
+ let d1 = vg_errortools::fat_io_wrap_std(file_1, &std::fs::read_to_string)?;
+ let d2 = vg_errortools::fat_io_wrap_std(file_2, &std::fs::read_to_string)?;
+ (d1, d2)
}
};
- let mismatch = match compare_jsons(&data1, &data2) {
- Ok(mismatch) => mismatch,
- Err(err) => {
- eprintln!("{}", err);
- proc::exit(1)
- }
- };
- match display_output(mismatch) {
- Ok(_) => (),
- Err(err) => eprintln!("{}", err),
- };
-}
-
-fn error_exit(message: Message) -> ! {
- eprintln!("{}", message);
- proc::exit(1);
+ println!("Evaluation exclusion regex list");
+ let exclusion_keys = args
+ .exclude_keys
+ .as_ref()
+ .map(|v| {
+ v.iter()
+ .map(|k| regex::Regex::new(k).map_err(|e| e.into()))
+ .collect::>>()
+ .unwrap_or_default()
+ })
+ .unwrap_or_default();
+ println!("Comparing");
+ let mismatch = compare_strs(&json_1, &json_2, args.sort_arrays, &exclusion_keys)?;
+ println!("Printing results");
+ let comparison_result = check_diffs(mismatch)?;
+ if !comparison_result {
+ std::process::exit(1);
+ }
+ Ok(())
}
-pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> {
- let no_mismatch = Mismatch {
- left_only_keys: KeyNode::Nil,
- right_only_keys: KeyNode::Nil,
- keys_in_both: KeyNode::Nil,
- };
-
- let stdout = io::stdout();
- let mut handle = io::BufWriter::new(stdout.lock());
- Ok(if no_mismatch == result {
- writeln!(handle, "\n{}", Message::NoMismatch)?;
- } else {
- match result.keys_in_both {
- KeyNode::Node(_) => {
- let mut keys = Vec::new();
- result.keys_in_both.absolute_keys(&mut keys, None);
- writeln!(handle, "\n{}:", Message::Mismatch)?;
- for key in keys {
- writeln!(handle, "{}", key)?;
- }
- }
- KeyNode::Value(_, _) => writeln!(handle, "{}", Message::RootMismatch)?,
- KeyNode::Nil => (),
- }
- match result.left_only_keys {
- KeyNode::Node(_) => {
- let mut keys = Vec::new();
- result.left_only_keys.absolute_keys(&mut keys, None);
- writeln!(handle, "\n{}:", Message::LeftExtra)?;
- for key in keys {
- writeln!(handle, "{}", key.red().bold())?;
- }
- }
- KeyNode::Value(_, _) => (),
- KeyNode::Nil => (),
- }
- match result.right_only_keys {
- KeyNode::Node(_) => {
- let mut keys = Vec::new();
- result.right_only_keys.absolute_keys(&mut keys, None);
- writeln!(handle, "\n{}:", Message::RightExtra)?;
- for key in keys {
- writeln!(handle, "{}", key.green().bold())?;
- }
- }
- KeyNode::Value(_, _) => (),
- KeyNode::Nil => (),
- }
- })
+pub fn check_diffs(result: Mismatch) -> Result {
+ let mismatches = result.all_diffs();
+ let is_good = mismatches.is_empty();
+ for (d_type, key) in mismatches {
+ println!("{d_type}: {key}");
+ }
+ Ok(is_good)
}
diff --git a/src/mismatch.rs b/src/mismatch.rs
new file mode 100644
index 0000000..8d1da21
--- /dev/null
+++ b/src/mismatch.rs
@@ -0,0 +1,67 @@
+use crate::enums::{DiffEntry, DiffType};
+use crate::DiffTreeNode;
+
+/// Structure holding the differences after a compare operation.
+/// For more readable access use the [`Mismatch::all_diffs`] method that yields a [`DiffEntry`] per diff.
+#[derive(Debug, PartialEq)]
+pub struct Mismatch {
+ pub left_only: DiffTreeNode,
+ pub right_only: DiffTreeNode,
+ pub unequal_values: DiffTreeNode,
+}
+
+impl Mismatch {
+ pub fn new(l: DiffTreeNode, r: DiffTreeNode, u: DiffTreeNode) -> Mismatch {
+ Mismatch {
+ left_only: l,
+ right_only: r,
+ unequal_values: u,
+ }
+ }
+
+ pub fn empty() -> Self {
+ Mismatch {
+ left_only: DiffTreeNode::Null,
+ unequal_values: DiffTreeNode::Null,
+ right_only: DiffTreeNode::Null,
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.left_only == DiffTreeNode::Null
+ && self.unequal_values == DiffTreeNode::Null
+ && self.right_only == DiffTreeNode::Null
+ }
+
+ pub fn all_diffs(&self) -> Vec<(DiffType, DiffEntry)> {
+ let both = self
+ .unequal_values
+ .get_diffs()
+ .into_iter()
+ .map(|k| (DiffType::Mismatch, k));
+ let left = self
+ .left_only
+ .get_diffs()
+ .into_iter()
+ .map(|k| (DiffType::LeftExtra, k));
+ let right = self
+ .right_only
+ .get_diffs()
+ .into_iter()
+ .map(|k| (DiffType::RightExtra, k));
+
+ both.chain(left).chain(right).collect()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn empty_diffs() {
+ let empty = Mismatch::empty();
+ let all_diffs = empty.all_diffs();
+ assert!(all_diffs.is_empty());
+ }
+}
diff --git a/src/process.rs b/src/process.rs
index c34e627..ee56f97 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -1,129 +1,529 @@
use std::collections::HashMap;
use std::collections::HashSet;
+use diffs::{Diff, myers, Replace};
+use regex::Regex;
use serde_json::Map;
use serde_json::Value;
-use crate::constants::Message;
-use crate::ds::key_node::KeyNode;
-use crate::ds::mismatch::Mismatch;
-
-pub fn compare_jsons(a: &str, b: &str) -> Result {
- let value1 = match serde_json::from_str(a) {
- Ok(val1) => val1,
- Err(_) => return Err(Message::JSON1),
- };
- let value2 = match serde_json::from_str(b) {
- Ok(val2) => val2,
- Err(_) => return Err(Message::JSON2),
- };
- Ok(match_json(&value1, &value2))
+use crate::DiffTreeNode;
+use crate::Mismatch;
+use crate::Result;
+use crate::sort::preprocess_array;
+
+/// Compares two string slices containing serialized json with each other, returns an error or a [`Mismatch`] structure holding all differences.
+/// Internally this calls into [`compare_serde_values`] after deserializing the string slices into [`serde_json::Value`].
+/// Arguments are the string slices, a bool to trigger deep sorting of arrays and ignored_keys as a list of regex to match keys against.
+/// Ignoring a regex from comparison will also ignore the key from having an impact on sorting arrays.
+pub fn compare_strs(
+ a: &str,
+ b: &str,
+ sort_arrays: bool,
+ ignore_keys: &[Regex],
+) -> Result {
+ let value1 = serde_json::from_str(a)?;
+ let value2 = serde_json::from_str(b)?;
+ compare_serde_values(&value1, &value2, sort_arrays, ignore_keys)
}
-pub fn match_json(value1: &Value, value2: &Value) -> Mismatch {
- match (value1, value2) {
- (Value::Object(a), Value::Object(b)) => {
- let (left_only_keys, right_only_keys, intersection_keys) = intersect_maps(&a, &b);
-
- let mut unequal_keys = KeyNode::Nil;
- let mut left_only_keys = get_map_of_keys(left_only_keys);
- let mut right_only_keys = get_map_of_keys(right_only_keys);
-
- if let Some(intersection_keys) = intersection_keys {
- for key in intersection_keys {
- let Mismatch {
- left_only_keys: l,
- right_only_keys: r,
- keys_in_both: u,
- } = match_json(&a.get(&key).unwrap(), &b.get(&key).unwrap());
- left_only_keys = insert_child_key_map(left_only_keys, l, &key);
- right_only_keys = insert_child_key_map(right_only_keys, r, &key);
- unequal_keys = insert_child_key_map(unequal_keys, u, &key);
- }
- }
- Mismatch::new(left_only_keys, right_only_keys, unequal_keys)
+/// Compares two [`serde_json::Value`] items with each other, returns an error or a [`Mismatch`] structure holding all differences.
+/// Arguments are the values, a bool to trigger deep sorting of arrays and ignored_keys as a list of regex to match keys against.
+/// Ignoring a regex from comparison will also ignore the key from having an impact on sorting arrays.
+pub fn compare_serde_values(
+ a: &Value,
+ b: &Value,
+ sort_arrays: bool,
+ ignore_keys: &[Regex],
+) -> Result {
+ match_json(a, b, sort_arrays, ignore_keys)
+}
+
+fn values_to_node(vec: Vec<(usize, &Value)>) -> DiffTreeNode {
+ if vec.is_empty() {
+ DiffTreeNode::Null
+ } else {
+ DiffTreeNode::Array(
+ vec.into_iter()
+ .map(|(l, v)| (l, DiffTreeNode::Value(v.clone(), v.clone())))
+ .collect(),
+ )
+ }
+}
+
+struct ListDiffHandler<'a> {
+ replaced: &'a mut Vec<(usize, usize, usize, usize)>,
+ deletion: &'a mut Vec<(usize, usize)>,
+ insertion: &'a mut Vec<(usize, usize)>,
+}
+impl<'a> ListDiffHandler<'a> {
+ pub fn new(
+ replaced: &'a mut Vec<(usize, usize, usize, usize)>,
+ deletion: &'a mut Vec<(usize, usize)>,
+ insertion: &'a mut Vec<(usize, usize)>,
+ ) -> Self {
+ Self {
+ replaced,
+ deletion,
+ insertion,
}
- (a, b) => {
- if a == b {
- Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil)
- } else {
- Mismatch::new(
- KeyNode::Nil,
- KeyNode::Nil,
- KeyNode::Value(a.clone(), b.clone()),
- )
- }
+ }
+}
+impl<'a> Diff for ListDiffHandler<'a> {
+ type Error = ();
+ fn delete(&mut self, old: usize, len: usize, _new: usize) -> std::result::Result<(), ()> {
+ self.deletion.push((old, len));
+ Ok(())
+ }
+ fn insert(&mut self, _o: usize, new: usize, len: usize) -> std::result::Result<(), ()> {
+ self.insertion.push((new, len));
+ Ok(())
+ }
+ fn replace(
+ &mut self,
+ old: usize,
+ len: usize,
+ new: usize,
+ new_len: usize,
+ ) -> std::result::Result<(), ()> {
+ self.replaced.push((old, len, new, new_len));
+ Ok(())
+ }
+}
+
+fn match_json(
+ value1: &Value,
+ value2: &Value,
+ sort_arrays: bool,
+ ignore_keys: &[Regex],
+) -> Result {
+ match (value1, value2) {
+ (Value::Object(a), Value::Object(b)) => process_objects(a, b, ignore_keys, sort_arrays),
+ (Value::Array(a), Value::Array(b)) => process_arrays(sort_arrays, a, ignore_keys, b),
+ (a, b) => process_values(a, b),
+ }
+}
+
+fn process_values(a: &Value, b: &Value) -> Result {
+ if a == b {
+ Ok(Mismatch::empty())
+ } else {
+ Ok(Mismatch::new(
+ DiffTreeNode::Null,
+ DiffTreeNode::Null,
+ DiffTreeNode::Value(a.clone(), b.clone()),
+ ))
+ }
+}
+
+fn process_objects(
+ a: &Map,
+ b: &Map,
+ ignore_keys: &[Regex],
+ sort_arrays: bool,
+) -> Result {
+ let diff = intersect_maps(a, b, ignore_keys);
+ let mut left_only_keys = get_map_of_keys(diff.left_only);
+ let mut right_only_keys = get_map_of_keys(diff.right_only);
+ let intersection_keys = diff.intersection;
+
+ let mut unequal_keys = DiffTreeNode::Null;
+
+ for key in intersection_keys {
+ let Mismatch {
+ left_only: l,
+ right_only: r,
+ unequal_values: u,
+ } = match_json(
+ a.get(&key).unwrap(),
+ b.get(&key).unwrap(),
+ sort_arrays,
+ ignore_keys,
+ )?;
+ left_only_keys = insert_child_key_map(left_only_keys, l, &key)?;
+ right_only_keys = insert_child_key_map(right_only_keys, r, &key)?;
+ unequal_keys = insert_child_key_map(unequal_keys, u, &key)?;
+ }
+
+ Ok(Mismatch::new(left_only_keys, right_only_keys, unequal_keys))
+}
+
+fn process_arrays(
+ sort_arrays: bool,
+ a: &Vec,
+ ignore_keys: &[Regex],
+ b: &Vec,
+) -> Result {
+ let a = preprocess_array(sort_arrays, a, ignore_keys);
+ let b = preprocess_array(sort_arrays, b, ignore_keys);
+
+ let mut replaced = Vec::new();
+ let mut deleted = Vec::new();
+ let mut inserted = Vec::new();
+
+ let mut diff = Replace::new(ListDiffHandler::new(
+ &mut replaced,
+ &mut deleted,
+ &mut inserted,
+ ));
+ myers::diff(
+ &mut diff,
+ a.as_slice(),
+ 0,
+ a.len(),
+ b.as_slice(),
+ 0,
+ b.len(),
+ )
+ .unwrap();
+
+ fn extract_one_sided_values(v: Vec<(usize, usize)>, vals: &[Value]) -> Vec<(usize, &Value)> {
+ v.into_iter()
+ .flat_map(|(o, ol)| (o..o + ol).map(|i| (i, &vals[i])))
+ .collect::>()
+ }
+
+ let left_only_values: Vec<_> = extract_one_sided_values(deleted, a.as_slice());
+ let right_only_values: Vec<_> = extract_one_sided_values(inserted, b.as_slice());
+
+ let mut left_only_nodes = values_to_node(left_only_values);
+ let mut right_only_nodes = values_to_node(right_only_values);
+ let mut diff = DiffTreeNode::Null;
+
+ for (o, ol, n, nl) in replaced {
+ let max_length = ol.max(nl);
+ for i in 0..max_length {
+ let inner_a = a.get(o + i).unwrap_or(&Value::Null);
+ let inner_b = b.get(n + i).unwrap_or(&Value::Null);
+ let cdiff = match_json(inner_a, inner_b, sort_arrays, ignore_keys)?;
+ let position = o + i;
+ let Mismatch {
+ left_only: l,
+ right_only: r,
+ unequal_values: u,
+ } = cdiff;
+ left_only_nodes = insert_child_key_diff(left_only_nodes, l, position)?;
+ right_only_nodes = insert_child_key_diff(right_only_nodes, r, position)?;
+ diff = insert_child_key_diff(diff, u, position)?;
}
}
+
+ Ok(Mismatch::new(left_only_nodes, right_only_nodes, diff))
}
-fn get_map_of_keys(set: Option>) -> KeyNode {
- if let Some(set) = set {
- KeyNode::Node(
+fn get_map_of_keys(set: HashSet) -> DiffTreeNode {
+ if !set.is_empty() {
+ DiffTreeNode::Node(
set.iter()
- .map(|key| (String::from(key), KeyNode::Nil))
+ .map(|key| (String::from(key), DiffTreeNode::Null))
.collect(),
)
} else {
- KeyNode::Nil
+ DiffTreeNode::Null
+ }
+}
+
+fn insert_child_key_diff(
+ parent: DiffTreeNode,
+ child: DiffTreeNode,
+ line: usize,
+) -> Result {
+ if child == DiffTreeNode::Null {
+ return Ok(parent);
+ }
+ if let DiffTreeNode::Array(mut array) = parent {
+ array.push((line, child));
+ Ok(DiffTreeNode::Array(array))
+ } else if let DiffTreeNode::Null = parent {
+ Ok(DiffTreeNode::Array(vec![(line, child)]))
+ } else {
+ Err(format!("Tried to insert child: {child:?} into parent {parent:?} - structure incoherent, expected a parent array - somehow json structure seems broken").into())
}
}
-fn insert_child_key_map(parent: KeyNode, child: KeyNode, key: &String) -> KeyNode {
- if child == KeyNode::Nil {
- return parent;
+fn insert_child_key_map(
+ parent: DiffTreeNode,
+ child: DiffTreeNode,
+ key: &String,
+) -> Result {
+ if child == DiffTreeNode::Null {
+ return Ok(parent);
}
- if let KeyNode::Node(mut map) = parent {
+ if let DiffTreeNode::Node(mut map) = parent {
map.insert(String::from(key), child);
- KeyNode::Node(map) // This is weird! I just wanted to return back `parent` here
- } else if let KeyNode::Nil = parent {
+ Ok(DiffTreeNode::Node(map))
+ } else if let DiffTreeNode::Null = parent {
let mut map = HashMap::new();
map.insert(String::from(key), child);
- KeyNode::Node(map)
+ Ok(DiffTreeNode::Node(map))
} else {
- parent // TODO Trying to insert child node in a Value variant : Should not happen => Throw an error instead.
+ Err(format!("Tried to insert child: {child:?} into parent {parent:?} - structure incoherent, expected a parent object - somehow json structure seems broken").into())
+ }
+}
+
+struct MapDifference {
+ left_only: HashSet,
+ right_only: HashSet,
+ intersection: HashSet,
+}
+
+impl MapDifference {
+ pub fn new(
+ left_only: HashSet,
+ right_only: HashSet,
+ intersection: HashSet,
+ ) -> Self {
+ Self {
+ right_only,
+ left_only,
+ intersection,
+ }
}
}
fn intersect_maps(
a: &Map,
b: &Map,
-) -> (
- Option>,
- Option>,
- Option>,
-) {
+ ignore_keys: &[Regex],
+) -> MapDifference {
let mut intersection = HashSet::new();
let mut left = HashSet::new();
+
let mut right = HashSet::new();
- for a_key in a.keys() {
+ for a_key in a
+ .keys()
+ .filter(|k| ignore_keys.iter().all(|r| !r.is_match(k.as_str())))
+ {
if b.contains_key(a_key) {
intersection.insert(String::from(a_key));
} else {
left.insert(String::from(a_key));
}
}
- for b_key in b.keys() {
+ for b_key in b
+ .keys()
+ .filter(|k| ignore_keys.iter().all(|r| !r.is_match(k.as_str())))
+ {
if !a.contains_key(b_key) {
right.insert(String::from(b_key));
}
}
- let left = if left.len() == 0 { None } else { Some(left) };
- let right = if right.len() == 0 { None } else { Some(right) };
- let intersection = if intersection.len() == 0 {
- None
- } else {
- Some(intersection)
- };
- (left, right, intersection)
+
+ MapDifference::new(left, right, intersection)
}
#[cfg(test)]
mod tests {
- use super::*;
use maplit::hashmap;
use serde_json::json;
+ use super::*;
+
+ #[test]
+ fn sorting_ignores_ignored_keys() {
+ let data1: Value =
+ serde_json::from_str(r#"[{"a": 1, "b":2 }, { "a": 2, "b" : 1 }]"#).unwrap();
+ let ignore = [Regex::new("a").unwrap()];
+ let sorted_ignores = preprocess_array(true, data1.as_array().unwrap(), &ignore);
+ let sorted_no_ignores = preprocess_array(true, data1.as_array().unwrap(), &[]);
+
+ assert_eq!(
+ sorted_ignores
+ .first()
+ .unwrap()
+ .as_object()
+ .unwrap()
+ .get("b")
+ .unwrap()
+ .as_i64()
+ .unwrap(),
+ 1
+ );
+ assert_eq!(
+ sorted_no_ignores
+ .first()
+ .unwrap()
+ .as_object()
+ .unwrap()
+ .get("b")
+ .unwrap()
+ .as_i64()
+ .unwrap(),
+ 2
+ );
+ }
+
+ #[test]
+ fn test_arrays_sorted_objects_ignored() {
+ let data1 = r#"[{"c": {"d": "e"} },"b","c"]"#;
+ let data2 = r#"["b","c",{"c": {"d": "f"} }]"#;
+ let ignore = Regex::new("d").unwrap();
+ let diff = compare_strs(data1, data2, true, &[ignore]).unwrap();
+ assert!(diff.is_empty());
+ }
+
+ #[test]
+ fn test_arrays_sorted_simple() {
+ let data1 = r#"["a","b","c"]"#;
+ let data2 = r#"["b","c","a"]"#;
+ let diff = compare_strs(data1, data2, true, &[]).unwrap();
+ assert!(diff.is_empty());
+ }
+
+ #[test]
+ fn test_arrays_sorted_objects() {
+ let data1 = r#"[{"c": {"d": "e"} },"b","c"]"#;
+ let data2 = r#"["b","c",{"c": {"d": "e"} }]"#;
+ let diff = compare_strs(data1, data2, true, &[]).unwrap();
+ assert!(diff.is_empty());
+ }
+
+ #[test]
+ fn test_arrays_deep_sorted_objects() {
+ let data1 = r#"[{"c": ["d","e"] },"b","c"]"#;
+ let data2 = r#"["b","c",{"c": ["e", "d"] }]"#;
+ let diff = compare_strs(data1, data2, true, &[]).unwrap();
+ assert!(diff.is_empty());
+ }
+
+ #[test]
+ fn test_arrays_deep_sorted_objects_with_arrays() {
+ let data1 = r#"[{"a": [{"b": ["3", "1"]}] }, {"a": [{"b": ["2", "3"]}] }]"#;
+ let data2 = r#"[{"a": [{"b": ["2", "3"]}] }, {"a": [{"b": ["1", "3"]}] }]"#;
+ let diff = compare_strs(data1, data2, true, &[]).unwrap();
+ assert!(diff.is_empty());
+ }
+
+ #[test]
+ fn test_arrays_deep_sorted_objects_with_outer_diff() {
+ let data1 = r#"[{"c": ["d","e"] },"b"]"#;
+ let data2 = r#"["b","c",{"c": ["e", "d"] }]"#;
+ let diff = compare_strs(data1, data2, true, &[]).unwrap();
+ assert!(!diff.is_empty());
+ let insertions = diff.right_only.get_diffs();
+ assert_eq!(insertions.len(), 1);
+ assert_eq!(insertions.first().unwrap().to_string(), r#".[2].("c")"#);
+ }
+
+ #[test]
+ fn test_arrays_deep_sorted_objects_with_inner_diff() {
+ let data1 = r#"["a",{"c": ["d","e", "f"] },"b"]"#;
+ let data2 = r#"["b",{"c": ["e","d"] },"a"]"#;
+ let diff = compare_strs(data1, data2, true, &[]).unwrap();
+ assert!(!diff.is_empty());
+ let deletions = diff.left_only.get_diffs();
+
+ assert_eq!(deletions.len(), 1);
+ assert_eq!(
+ deletions.first().unwrap().to_string(),
+ r#".[0].c.[2].("f")"#
+ );
+ }
+
+ #[test]
+ fn test_arrays_deep_sorted_objects_with_inner_diff_mutation() {
+ let data1 = r#"["a",{"c": ["d", "f"] },"b"]"#;
+ let data2 = r#"["b",{"c": ["e","d"] },"a"]"#;
+ let diffs = compare_strs(data1, data2, true, &[]).unwrap();
+ assert!(!diffs.is_empty());
+ let diffs = diffs.unequal_values.get_diffs();
+
+ assert_eq!(diffs.len(), 1);
+ assert_eq!(
+ diffs.first().unwrap().to_string(),
+ r#".[0].c.[1].("f" != "e")"#
+ );
+ }
+
+ #[test]
+ fn test_arrays_simple_diff() {
+ let data1 = r#"["a","b","c"]"#;
+ let data2 = r#"["a","b","d"]"#;
+ let diff = compare_strs(data1, data2, false, &[]).unwrap();
+ assert_eq!(diff.left_only, DiffTreeNode::Null);
+ assert_eq!(diff.right_only, DiffTreeNode::Null);
+ let diff = diff.unequal_values.get_diffs();
+ assert_eq!(diff.len(), 1);
+ assert_eq!(diff.first().unwrap().to_string(), r#".[2].("c" != "d")"#);
+ }
+
+ #[test]
+ fn test_arrays_more_complex_diff() {
+ let data1 = r#"["a","b","c"]"#;
+ let data2 = r#"["a","a","b","d"]"#;
+ let diff = compare_strs(data1, data2, false, &[]).unwrap();
+
+ let changes_diff = diff.unequal_values.get_diffs();
+ assert_eq!(diff.left_only, DiffTreeNode::Null);
+
+ assert_eq!(changes_diff.len(), 1);
+ assert_eq!(
+ changes_diff.first().unwrap().to_string(),
+ r#".[2].("c" != "d")"#
+ );
+ let insertions = diff.right_only.get_diffs();
+ assert_eq!(insertions.len(), 1);
+ assert_eq!(insertions.first().unwrap().to_string(), r#".[0].("a")"#);
+ }
+
+ #[test]
+ fn test_arrays_extra_left() {
+ let data1 = r#"["a","b","c"]"#;
+ let data2 = r#"["a","b"]"#;
+ let diff = compare_strs(data1, data2, false, &[]).unwrap();
+
+ let diffs = diff.left_only.get_diffs();
+ assert_eq!(diffs.len(), 1);
+ assert_eq!(diffs.first().unwrap().to_string(), r#".[2].("c")"#);
+ assert_eq!(diff.unequal_values, DiffTreeNode::Null);
+ assert_eq!(diff.right_only, DiffTreeNode::Null);
+ }
+
+ #[test]
+ fn test_arrays_extra_right() {
+ let data1 = r#"["a","b"]"#;
+ let data2 = r#"["a","b","c"]"#;
+ let diff = compare_strs(data1, data2, false, &[]).unwrap();
+
+ let diffs = diff.right_only.get_diffs();
+ assert_eq!(diffs.len(), 1);
+ assert_eq!(diffs.first().unwrap().to_string(), r#".[2].("c")"#);
+ assert_eq!(diff.unequal_values, DiffTreeNode::Null);
+ assert_eq!(diff.left_only, DiffTreeNode::Null);
+ }
+
+ #[test]
+ fn long_insertion_modification() {
+ let data1 = r#"["a","b","a"]"#;
+ let data2 = r#"["a","c","c","c","a"]"#;
+ let diff = compare_strs(data1, data2, false, &[]).unwrap();
+ let diffs = diff.unequal_values.get_diffs();
+
+ assert_eq!(diffs.len(), 3);
+ let diffs: Vec<_> = diffs.into_iter().map(|d| d.to_string()).collect();
+
+ assert!(diffs.contains(&r#".[3].(null != "c")"#.to_string()));
+ assert!(diffs.contains(&r#".[1].("b" != "c")"#.to_string()));
+ assert!(diffs.contains(&r#".[2].("a" != "c")"#.to_string()));
+ assert_eq!(diff.right_only, DiffTreeNode::Null);
+ assert_eq!(diff.left_only, DiffTreeNode::Null);
+ }
+
+ #[test]
+ fn test_arrays_object_extra() {
+ let data1 = r#"["a","b"]"#;
+ let data2 = r#"["a","b", {"c": {"d": "e"} }]"#;
+ let diff = compare_strs(data1, data2, false, &[]).unwrap();
+
+ let diffs = diff.right_only.get_diffs();
+ assert_eq!(diffs.len(), 1);
+ assert_eq!(
+ diffs.first().unwrap().to_string(),
+ r#".[2].({"c":{"d":"e"}})"#
+ );
+ assert_eq!(diff.unequal_values, DiffTreeNode::Null);
+ assert_eq!(diff.left_only, DiffTreeNode::Null);
+ }
+
#[test]
fn nested_diff() {
let data1 = r#"{
@@ -155,24 +555,24 @@ mod tests {
}
}"#;
- let expected_left = KeyNode::Node(hashmap! {
- "b".to_string() => KeyNode::Node(hashmap! {
- "c".to_string() => KeyNode::Node(hashmap! {
- "f".to_string() => KeyNode::Nil,
- "h".to_string() => KeyNode::Node( hashmap! {
- "j".to_string() => KeyNode::Nil,
+ let expected_left = DiffTreeNode::Node(hashmap! {
+ "b".to_string() => DiffTreeNode::Node(hashmap! {
+ "c".to_string() => DiffTreeNode::Node(hashmap! {
+ "f".to_string() => DiffTreeNode::Null,
+ "h".to_string() => DiffTreeNode::Node( hashmap! {
+ "j".to_string() => DiffTreeNode::Null,
}
),
}
),
}),
});
- let expected_right = KeyNode::Node(hashmap! {
- "b".to_string() => KeyNode::Node(hashmap! {
- "c".to_string() => KeyNode::Node(hashmap! {
- "g".to_string() => KeyNode::Nil,
- "h".to_string() => KeyNode::Node(hashmap! {
- "k".to_string() => KeyNode::Nil,
+ let expected_right = DiffTreeNode::Node(hashmap! {
+ "b".to_string() => DiffTreeNode::Node(hashmap! {
+ "c".to_string() => DiffTreeNode::Node(hashmap! {
+ "g".to_string() => DiffTreeNode::Null,
+ "h".to_string() => DiffTreeNode::Node(hashmap! {
+ "k".to_string() => DiffTreeNode::Null,
}
)
}
@@ -180,12 +580,12 @@ mod tests {
}
)
});
- let expected_uneq = KeyNode::Node(hashmap! {
- "b".to_string() => KeyNode::Node(hashmap! {
- "c".to_string() => KeyNode::Node(hashmap! {
- "e".to_string() => KeyNode::Value(json!(5), json!(6)),
- "h".to_string() => KeyNode::Node(hashmap! {
- "i".to_string() => KeyNode::Value(json!(true), json!(false)),
+ let expected_uneq = DiffTreeNode::Node(hashmap! {
+ "b".to_string() => DiffTreeNode::Node(hashmap! {
+ "c".to_string() => DiffTreeNode::Node(hashmap! {
+ "e".to_string() => DiffTreeNode::Value(json!(5), json!(6)),
+ "h".to_string() => DiffTreeNode::Node(hashmap! {
+ "i".to_string() => DiffTreeNode::Value(json!(true), json!(false)),
}
)
}
@@ -195,7 +595,7 @@ mod tests {
});
let expected = Mismatch::new(expected_left, expected_right, expected_uneq);
- let mismatch = compare_jsons(data1, data2).unwrap();
+ let mismatch = compare_strs(data1, data2, false, &[]).unwrap();
assert_eq!(mismatch, expected, "Diff was incorrect.");
}
@@ -231,8 +631,8 @@ mod tests {
}"#;
assert_eq!(
- compare_jsons(data1, data2).unwrap(),
- Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil)
+ compare_strs(data1, data2, false, &[]).unwrap(),
+ Mismatch::new(DiffTreeNode::Null, DiffTreeNode::Null, DiffTreeNode::Null)
);
}
@@ -242,8 +642,8 @@ mod tests {
let data2 = r#"{}"#;
assert_eq!(
- compare_jsons(data1, data2).unwrap(),
- Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil)
+ compare_strs(data1, data2, false, &[]).unwrap(),
+ Mismatch::empty()
);
}
@@ -251,23 +651,15 @@ mod tests {
fn parse_err_source_one() {
let invalid_json1 = r#"{invalid: json}"#;
let valid_json2 = r#"{"a":"b"}"#;
- match compare_jsons(invalid_json1, valid_json2) {
- Ok(_) => panic!("This shouldn't be an Ok"),
- Err(err) => {
- assert_eq!(Message::JSON1, err);
- }
- };
+ compare_strs(invalid_json1, valid_json2, false, &[])
+ .expect_err("Parsing invalid JSON didn't throw an error");
}
#[test]
fn parse_err_source_two() {
let valid_json1 = r#"{"a":"b"}"#;
let invalid_json2 = r#"{invalid: json}"#;
- match compare_jsons(valid_json1, invalid_json2) {
- Ok(_) => panic!("This shouldn't be an Ok"),
- Err(err) => {
- assert_eq!(Message::JSON2, err);
- }
- };
+ compare_strs(valid_json1, invalid_json2, false, &[])
+ .expect_err("Parsing invalid JSON didn't throw an err");
}
}
diff --git a/src/sort.rs b/src/sort.rs
new file mode 100644
index 0000000..2edf8e9
--- /dev/null
+++ b/src/sort.rs
@@ -0,0 +1,103 @@
+use std::borrow::Cow;
+
+use regex::Regex;
+use serde_json::Value;
+
+/// Returns a deep-sorted copy of the [`serde_json::Value`]
+pub fn sort_value(v: &Value, ignore_keys: &[Regex]) -> Value {
+ match v {
+ Value::Array(a) => Value::Array(
+ preprocess_array(
+ true,
+ &a.iter().map(|e| sort_value(e, ignore_keys)).collect(),
+ ignore_keys,
+ )
+ .into_owned(),
+ ),
+ Value::Object(a) => Value::Object(
+ a.iter()
+ .map(|(k, v)| (k.clone(), sort_value(v, ignore_keys)))
+ .collect(),
+ ),
+ v => v.clone(),
+ }
+}
+
+pub(crate) fn preprocess_array<'a>(
+ sort_arrays: bool,
+ a: &'a Vec,
+ ignore_keys: &[Regex],
+) -> Cow<'a, Vec> {
+ if sort_arrays || !ignore_keys.is_empty() {
+ let mut owned = a.to_owned();
+ owned.sort_by(|a, b| compare_values(a, b, ignore_keys));
+ Cow::Owned(owned)
+ } else {
+ Cow::Borrowed(a)
+ }
+}
+fn compare_values(a: &Value, b: &Value, ignore_keys: &[Regex]) -> std::cmp::Ordering {
+ match (a, b) {
+ (Value::Null, Value::Null) => std::cmp::Ordering::Equal,
+ (Value::Null, _) => std::cmp::Ordering::Less,
+ (_, Value::Null) => std::cmp::Ordering::Greater,
+ (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
+ (Value::Number(a), Value::Number(b)) => {
+ if let (Some(a), Some(b)) = (a.as_i64(), b.as_i64()) {
+ return a.cmp(&b);
+ }
+ if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) {
+ return a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal);
+ }
+ // Handle other number types if needed
+ std::cmp::Ordering::Equal
+ }
+ (Value::String(a), Value::String(b)) => a.cmp(b),
+ (Value::Array(a), Value::Array(b)) => {
+ let a = preprocess_array(true, a, ignore_keys);
+ let b = preprocess_array(true, b, ignore_keys);
+ for (a, b) in a.iter().zip(b.iter()) {
+ let cmp = compare_values(a, b, ignore_keys);
+ if cmp != std::cmp::Ordering::Equal {
+ return cmp;
+ }
+ }
+ a.len().cmp(&b.len())
+ }
+ (Value::Object(a), Value::Object(b)) => {
+ let mut keys_a: Vec<_> = a.keys().collect();
+ let mut keys_b: Vec<_> = b.keys().collect();
+ keys_a.sort();
+ keys_b.sort();
+ for (key_a, key_b) in keys_a
+ .iter()
+ .filter(|a| ignore_keys.iter().all(|r| !r.is_match(a)))
+ .zip(
+ keys_b
+ .iter()
+ .filter(|a| ignore_keys.iter().all(|r| !r.is_match(a))),
+ )
+ {
+ let cmp = key_a.cmp(key_b);
+ if cmp != std::cmp::Ordering::Equal {
+ return cmp;
+ }
+ let value_a = &a[*key_a];
+ let value_b = &b[*key_b];
+ let cmp = compare_values(value_a, value_b, ignore_keys);
+ if cmp != std::cmp::Ordering::Equal {
+ return cmp;
+ }
+ }
+ keys_a.len().cmp(&keys_b.len())
+ }
+ (Value::Object(_), _) => std::cmp::Ordering::Less,
+ (_, Value::Object(_)) => std::cmp::Ordering::Greater,
+ (Value::Bool(_), _) => std::cmp::Ordering::Less,
+ (_, Value::Bool(_)) => std::cmp::Ordering::Greater,
+ (Value::Number(_), _) => std::cmp::Ordering::Less,
+ (_, Value::Number(_)) => std::cmp::Ordering::Greater,
+ (Value::String(_), _) => std::cmp::Ordering::Less,
+ (_, Value::String(_)) => std::cmp::Ordering::Greater,
+ }
+}