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. +[![Crates.io](https://img.shields.io/crates/d/json_diff_ng?style=flat)](https://crates.io/crates/json_diff_ng) +[![Documentation](https://docs.rs/json_diff_ng/badge.svg)](https://docs.rs/json_diff_ng) +![CI](https://github.com/ChrisRega/json-diff/actions/workflows/rust.yml/badge.svg?branch=master "CI") +[![Coverage Status](https://coveralls.io/repos/github/ChrisRega/json-diff/badge.svg?branch=master)](https://coveralls.io/github/ChrisRega/json-diff?branch=master) +[![License](https://img.shields.io/github/license/ChrisRega/json-diff)](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: + + + 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")"# +); +``` -[![A screenshot of a sample diff with json_diff](https://github.com/ksceriath/json-diff/blob/master/Screenshot.png)](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, + } +}