From 1e99ac1004d0c802296877a58c4a4307a9445b47 Mon Sep 17 00:00:00 2001 From: strongchris Date: Mon, 27 Mar 2023 01:33:57 +0000 Subject: [PATCH 01/52] add truncation --- src/ds/key_node.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 20fb04c..78e2c4d 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -9,6 +9,18 @@ pub enum KeyNode { Node(HashMap), } +fn truncate(s: &str, max_chars: usize) -> String { + match s.char_indices().nth(max_chars) { + None => String::from(s), + Some((idx, _)) => { + let shorter = &s[..idx]; + let snip = "//SNIP//"; + let new_s = format!("{}{}", shorter, snip); + String::from(new_s) + } + } +} + impl KeyNode { pub fn absolute_keys(&self, keys: &mut Vec, key_from_root: Option) { let val_key = |key: Option| { @@ -24,8 +36,8 @@ impl KeyNode { KeyNode::Value(a, b) => keys.push(format!( "{} [ {} :: {} ]", val_key(key_from_root), - a.to_string().blue().bold(), - b.to_string().cyan().bold() + truncate(a.to_string().as_str(), 20).blue().bold(), + truncate(b.to_string().as_str(), 20).cyan().bold() )), KeyNode::Node(map) => { for (key, value) in map { @@ -38,3 +50,4 @@ impl KeyNode { } } } + From 3baa0053dd38c18c22364dd745f702f731b89bed Mon Sep 17 00:00:00 2001 From: strongchris Date: Mon, 27 Mar 2023 02:06:44 +0000 Subject: [PATCH 02/52] add mismatch counts --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 135b5fc..5acf214 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,7 +109,7 @@ pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { KeyNode::Node(_) => { let mut keys = Vec::new(); result.keys_in_both.absolute_keys(&mut keys, None); - writeln!(handle, "\n{}:", Message::Mismatch)?; + writeln!(handle, "\n{} ({}):", Message::Mismatch, keys.len())?; for key in keys { writeln!(handle, "{}", key)?; } @@ -121,7 +121,7 @@ pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { KeyNode::Node(_) => { let mut keys = Vec::new(); result.left_only_keys.absolute_keys(&mut keys, None); - writeln!(handle, "\n{}:", Message::LeftExtra)?; + writeln!(handle, "\n{} ({}):", Message::LeftExtra, keys.len())?; for key in keys { writeln!(handle, "{}", key.red().bold())?; } @@ -133,7 +133,7 @@ pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { KeyNode::Node(_) => { let mut keys = Vec::new(); result.right_only_keys.absolute_keys(&mut keys, None); - writeln!(handle, "\n{}:", Message::RightExtra)?; + writeln!(handle, "\n{} ({}):", Message::RightExtra, keys.len())?; for key in keys { writeln!(handle, "{}", key.green().bold())?; } From 77d3f45d67623126b8923e03473c2ea8076eff7d Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 4 Oct 2023 08:19:11 +0200 Subject: [PATCH 03/52] Update deps --- Cargo.lock | 377 ++++++++++++++++++++++++++++++++++++++++------------- Cargo.toml | 8 +- 2 files changed, 289 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index caff2f6..3c1c170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,244 +1,437 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "ansi_term" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "atty" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 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)", + "hermit-abi 0.1.19", + "libc", + "winapi", ] [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "clap" -version = "2.33.0" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 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)", + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", ] [[package]] name = "colored" -version = "1.9.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" 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)", + "is-terminal", + "lazy_static", + "windows-sys", +] + +[[package]] +name = "errno" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", ] [[package]] name = "heck" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ - "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix", + "windows-sys", ] [[package]] name = "itoa" -version = "0.4.4" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[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)", + "colored", + "maplit", + "serde_json", + "structopt", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.65" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "linux-raw-sys" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "proc-macro-error" -version = "0.2.6" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 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)", + "proc-macro2", + "quote", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.6" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" dependencies = [ - "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.2" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "serde" -version = "1.0.102" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] [[package]] name = "serde_json" -version = "1.0.41" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 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)", + "itoa", + "ryu", + "serde", ] [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.5" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 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)", + "clap", + "lazy_static", + "structopt-derive", ] [[package]] name = "structopt-derive" -version = "0.3.5" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 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)", + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "syn" -version = "1.0.8" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 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)", + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.6" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] -name = "unicode-xid" -version = "0.2.0" +name = "vec_map" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] -name = "vec_map" -version = "0.8.1" +name = "version_check" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 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)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[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" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml index a124a1b..cd68c3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ path = "src/main.rs" # 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" +serde_json = "1.0" +maplit = "1.0" +colored = "2.0" +structopt = "0.3" From f634b5cc0a6fd1bc3e268f9803a0e4f7efe537c7 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 4 Oct 2023 08:23:49 +0200 Subject: [PATCH 04/52] Reformat and make truncation length optional arg for the function --- src/ds/key_node.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 78e2c4d..335c5a6 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -2,7 +2,7 @@ use colored::*; use serde_json::Value; use std::collections::HashMap; -#[derive(Debug, PartialEq)] // TODO check: do we need PartiaEq ? +#[derive(Debug, PartialEq)] pub enum KeyNode { Nil, Value(Value, Value), @@ -22,7 +22,13 @@ fn truncate(s: &str, max_chars: usize) -> String { } impl KeyNode { - pub fn absolute_keys(&self, keys: &mut Vec, key_from_root: Option) { + pub fn absolute_keys( + &self, + keys: &mut Vec, + key_from_root: Option, + max_display_length: Option, + ) { + let max_display_length = max_display_length.unwrap_or(20); let val_key = |key: Option| { key.map(|mut s| { s.push_str(" ->"); @@ -36,18 +42,22 @@ impl KeyNode { KeyNode::Value(a, b) => keys.push(format!( "{} [ {} :: {} ]", val_key(key_from_root), - truncate(a.to_string().as_str(), 20).blue().bold(), - truncate(b.to_string().as_str(), 20).cyan().bold() + truncate(a.to_string().as_str(), max_display_length) + .blue() + .bold(), + truncate(b.to_string().as_str(), max_display_length) + .cyan() + .bold() )), KeyNode::Node(map) => { for (key, value) in map { value.absolute_keys( keys, Some(format!("{} {}", val_key(key_from_root.clone()), key)), + Some(max_display_length), ) } } } } } - From 9a8c03d0a2a782e5a2c8fe14d40d680ba07afa18 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 4 Oct 2023 08:31:58 +0200 Subject: [PATCH 05/52] Smaller code/internal cleanups Fix compilation --- src/ds/key_node.rs | 6 +++--- src/main.rs | 11 ++++++----- src/process.rs | 46 ++++++++++++++++++++++++++++++---------------- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 335c5a6..59fae44 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -16,7 +16,7 @@ fn truncate(s: &str, max_chars: usize) -> String { let shorter = &s[..idx]; let snip = "//SNIP//"; let new_s = format!("{}{}", shorter, snip); - String::from(new_s) + new_s } } } @@ -34,9 +34,9 @@ impl KeyNode { s.push_str(" ->"); s }) - .unwrap_or(String::new()) + .unwrap_or_default() }; - let nil_key = |key: Option| key.unwrap_or(String::new()); + let nil_key = |key: Option| key.unwrap_or_default(); match self { KeyNode::Nil => keys.push(nil_key(key_from_root)), KeyNode::Value(a, b) => keys.push(format!( diff --git a/src/main.rs b/src/main.rs index 5acf214..e80e6d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,13 +102,13 @@ pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { let stdout = io::stdout(); let mut handle = io::BufWriter::new(stdout.lock()); - Ok(if no_mismatch == result { + 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); + result.keys_in_both.absolute_keys(&mut keys, None, None); writeln!(handle, "\n{} ({}):", Message::Mismatch, keys.len())?; for key in keys { writeln!(handle, "{}", key)?; @@ -120,7 +120,7 @@ pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { match result.left_only_keys { KeyNode::Node(_) => { let mut keys = Vec::new(); - result.left_only_keys.absolute_keys(&mut keys, None); + result.left_only_keys.absolute_keys(&mut keys, None, None); writeln!(handle, "\n{} ({}):", Message::LeftExtra, keys.len())?; for key in keys { writeln!(handle, "{}", key.red().bold())?; @@ -132,7 +132,7 @@ pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { match result.right_only_keys { KeyNode::Node(_) => { let mut keys = Vec::new(); - result.right_only_keys.absolute_keys(&mut keys, None); + result.right_only_keys.absolute_keys(&mut keys, None, None); writeln!(handle, "\n{} ({}):", Message::RightExtra, keys.len())?; for key in keys { writeln!(handle, "{}", key.green().bold())?; @@ -141,5 +141,6 @@ pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { KeyNode::Value(_, _) => (), KeyNode::Nil => (), } - }) + }; + Ok(()) } diff --git a/src/process.rs b/src/process.rs index c34e627..b11e1ec 100644 --- a/src/process.rs +++ b/src/process.rs @@ -23,11 +23,12 @@ pub fn compare_jsons(a: &str, b: &str) -> Result { 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 diff = intersect_maps(a, b); + 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 = 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 { @@ -35,7 +36,7 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { left_only_keys: l, right_only_keys: r, keys_in_both: u, - } = match_json(&a.get(&key).unwrap(), &b.get(&key).unwrap()); + } = 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); @@ -85,14 +86,27 @@ fn insert_child_key_map(parent: KeyNode, child: KeyNode, key: &String) -> KeyNod } } -fn intersect_maps( - a: &Map, - b: &Map, -) -> ( - Option>, - Option>, - Option>, -) { +struct MapDifference { + left_only: Option>, + right_only: Option>, + intersection: Option>, +} + +impl MapDifference { + pub fn new( + left_only: Option>, + right_only: Option>, + intersection: Option>, + ) -> Self { + Self { + right_only, + left_only, + intersection, + } + } +} + +fn intersect_maps(a: &Map, b: &Map) -> MapDifference { let mut intersection = HashSet::new(); let mut left = HashSet::new(); let mut right = HashSet::new(); @@ -108,14 +122,14 @@ fn intersect_maps( 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 { + let left = if left.is_empty() { None } else { Some(left) }; + let right = if right.is_empty() { None } else { Some(right) }; + let intersection = if intersection.is_empty() { None } else { Some(intersection) }; - (left, right, intersection) + MapDifference::new(left, right, intersection) } #[cfg(test)] From 8bea37acfd18885000146dfb633ee565871c290c Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 4 Oct 2023 08:53:39 +0200 Subject: [PATCH 06/52] Add more convenient method of getting the absolute keys without external vector initialization --- src/ds/key_node.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 59fae44..772a171 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -22,6 +22,12 @@ fn truncate(s: &str, max_chars: usize) -> String { } impl KeyNode { + pub fn absolute_keys_to_vec(&self, max_display_length: Option) -> Vec { + let mut vec = Vec::new(); + self.absolute_keys(&mut vec, None, max_display_length); + vec + } + pub fn absolute_keys( &self, keys: &mut Vec, @@ -36,9 +42,8 @@ impl KeyNode { }) .unwrap_or_default() }; - let nil_key = |key: Option| key.unwrap_or_default(); match self { - KeyNode::Nil => keys.push(nil_key(key_from_root)), + KeyNode::Nil => keys.push(key_from_root.unwrap_or_default()), KeyNode::Value(a, b) => keys.push(format!( "{} [ {} :: {} ]", val_key(key_from_root), From f0cbf497021eef961a5d68a5a7931d4a168f80f2 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 4 Oct 2023 23:04:18 +0200 Subject: [PATCH 07/52] Add convenience API and use more modern clap package in arg parsing --- Cargo.lock | 242 +++++++++++++++++++-------------------------- Cargo.toml | 4 +- src/constants.rs | 49 ++++----- src/ds/mismatch.rs | 21 ++++ src/main.rs | 160 ++++++++---------------------- src/process.rs | 18 ++-- 6 files changed, 193 insertions(+), 301 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c1c170..885b15e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,30 +3,52 @@ version = 3 [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstream" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", ] [[package]] -name = "atty" -version = "0.2.14" +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", + "utf8parse", ] [[package]] -name = "bitflags" -version = "1.3.2" +name = "anstyle-query" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] [[package]] name = "bitflags" @@ -45,19 +67,50 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", + "anstream", + "anstyle", + "clap_lex", "strsim", - "textwrap", - "unicode-width", - "vec_map", ] +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "colored" version = "2.0.4" @@ -92,21 +145,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -120,7 +161,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi", "rustix", "windows-sys", ] @@ -135,10 +176,12 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" name = "json_diff" version = "0.1.2" dependencies = [ + "clap", "colored", "maplit", "serde_json", - "structopt", + "thiserror", + "vg_errortools", ] [[package]] @@ -165,30 +208,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.67" @@ -213,7 +232,7 @@ version = "0.38.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" dependencies = [ - "bitflags 2.4.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -243,7 +262,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn", ] [[package]] @@ -259,39 +278,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "structopt" -version = "0.3.26" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -299,23 +294,23 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.37" +name = "thiserror" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "thiserror-impl", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "thiserror-impl" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ - "unicode-width", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -325,51 +320,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.11" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] -name = "vec_map" -version = "0.8.2" +name = "vg_errortools" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "8f0d7932688493f8aac7d65e824820b6506fe68ae7450bf44ac7299b1841a4a6" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "thiserror", ] -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index cd68c3b..89dc5d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,9 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +thiserror = "1.0" +vg_errortools = "0.1" serde_json = "1.0" maplit = "1.0" colored = "2.0" -structopt = "0.3" +clap = {version = "4.4", features = ["derive"]} diff --git a/src/constants.rs b/src/constants.rs index 1074046..15cb8a8 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,38 +1,31 @@ -use colored::*; -use std::fmt; +use std::fmt::{Display, Formatter}; +use thiserror::Error; +use vg_errortools::FatIOError; -// 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, +#[derive(Debug, Error)] +pub enum Error { + #[error("Error opening file: {0}")] + IOError(#[from] FatIOError), + #[error("Error parsing first json: {0}")] + JSON(#[from] serde_json::Error), +} + +#[derive(Debug)] +pub enum DiffType { 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", +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, "{}", message.bold()) + write!(f, "{}", msg) } } diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 649739f..3dd7a98 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -1,3 +1,4 @@ +use crate::constants::DiffType; use crate::ds::key_node::KeyNode; #[derive(Debug, PartialEq)] @@ -15,4 +16,24 @@ impl Mismatch { keys_in_both: u, } } + + pub fn all_diffs(&self) -> Vec<(DiffType, String)> { + let both = self + .keys_in_both + .absolute_keys_to_vec(None) + .into_iter() + .map(|k| (DiffType::Mismatch, k)); + let left = self + .left_only_keys + .absolute_keys_to_vec(None) + .into_iter() + .map(|k| (DiffType::LeftExtra, k)); + let right = self + .right_only_keys + .absolute_keys_to_vec(None) + .into_iter() + .map(|k| (DiffType::RightExtra, k)); + + both.chain(left).chain(right).collect() + } } diff --git a/src/main.rs b/src/main.rs index e80e6d6..1a7b1f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,146 +1,64 @@ -use std::{ - fmt, fs, - io::{self, Write}, - process as proc, - str::FromStr, -}; - -use colored::*; -use structopt::StructOpt; +use clap::Parser; +use clap::Subcommand; +use json_diff::constants::Error; 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) - } -} - -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(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 }, } -#[derive(StructOpt)] -#[structopt(about = HELP)] -struct Cli { - read_mode: InputReadMode, - source1: String, - source2: String, +#[derive(Parser)] +struct Args { + #[command(subcommand)] + cmd: Mode, } -fn main() { - let args = Cli::from_args(); - - 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<(), Error> { + let args = Args::parse(); + 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); + let mismatch = compare_jsons(&json_1, &json_2)?; + + let comparison_result = check_diffs(mismatch)?; + if !comparison_result { + std::process::exit(1); + } + Ok(()) } -pub fn display_output(result: Mismatch) -> Result<(), std::io::Error> { +pub fn check_diffs(result: Mismatch) -> Result { 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()); if no_mismatch == result { - writeln!(handle, "\n{}", Message::NoMismatch)?; + println!("No mismatch"); + Ok(true) } else { - match result.keys_in_both { - KeyNode::Node(_) => { - let mut keys = Vec::new(); - result.keys_in_both.absolute_keys(&mut keys, None, None); - writeln!(handle, "\n{} ({}):", Message::Mismatch, keys.len())?; - 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, None); - writeln!(handle, "\n{} ({}):", Message::LeftExtra, keys.len())?; - 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, None); - writeln!(handle, "\n{} ({}):", Message::RightExtra, keys.len())?; - for key in keys { - writeln!(handle, "{}", key.green().bold())?; - } - } - KeyNode::Value(_, _) => (), - KeyNode::Nil => (), + let mismatches = result.all_diffs(); + for (d_type, key) in mismatches { + println!("{d_type}: {key}"); } - }; - Ok(()) + Ok(false) + } } diff --git a/src/process.rs b/src/process.rs index b11e1ec..4b26d78 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,22 +1,16 @@ use std::collections::HashMap; use std::collections::HashSet; +use crate::constants::Error; 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), - }; +pub fn compare_jsons(a: &str, b: &str) -> Result { + let value1 = serde_json::from_str(a)?; + let value2 = serde_json::from_str(b)?; Ok(match_json(&value1, &value2)) } @@ -268,7 +262,7 @@ mod tests { match compare_jsons(invalid_json1, valid_json2) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { - assert_eq!(Message::JSON1, err); + matches!(err, Error::JSON(_)); } }; } @@ -280,7 +274,7 @@ mod tests { match compare_jsons(valid_json1, invalid_json2) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { - assert_eq!(Message::JSON2, err); + matches!(err, Error::JSON(_)); } }; } From 0c8ac8f6b6bfa5c8285b4f384a653c9c82986f85 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 4 Oct 2023 23:37:20 +0200 Subject: [PATCH 08/52] Remove colored output --- Cargo.toml | 1 - src/ds/key_node.rs | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89dc5d1..166ada2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,5 +27,4 @@ thiserror = "1.0" vg_errortools = "0.1" serde_json = "1.0" maplit = "1.0" -colored = "2.0" clap = {version = "4.4", features = ["derive"]} diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 772a171..6ee7b64 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -1,4 +1,3 @@ -use colored::*; use serde_json::Value; use std::collections::HashMap; @@ -47,12 +46,8 @@ impl KeyNode { KeyNode::Value(a, b) => keys.push(format!( "{} [ {} :: {} ]", val_key(key_from_root), - truncate(a.to_string().as_str(), max_display_length) - .blue() - .bold(), + truncate(a.to_string().as_str(), max_display_length), truncate(b.to_string().as_str(), max_display_length) - .cyan() - .bold() )), KeyNode::Node(map) => { for (key, value) in map { From 19a288fcab9bd848f375ff08403008099ceb0f16 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 4 Oct 2023 23:37:27 +0200 Subject: [PATCH 09/52] Remove colored output --- Cargo.lock | 96 ------------------------------------------------------ 1 file changed, 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 885b15e..dc772d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,21 +50,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "bitflags" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - [[package]] name = "clap" version = "4.4.6" @@ -111,61 +96,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "colored" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" -dependencies = [ - "is-terminal", - "lazy_static", - "windows-sys", -] - -[[package]] -name = "errno" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - [[package]] name = "itoa" version = "1.0.9" @@ -177,31 +113,12 @@ name = "json_diff" version = "0.1.2" dependencies = [ "clap", - "colored", "maplit", "serde_json", "thiserror", "vg_errortools", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.148" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" - -[[package]] -name = "linux-raw-sys" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" - [[package]] name = "maplit" version = "1.0.2" @@ -226,19 +143,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rustix" -version = "0.38.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "ryu" version = "1.0.15" From 25675ec4889ee6ae21d7caea87601d510a928334 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 5 Oct 2023 20:20:55 +0200 Subject: [PATCH 10/52] Improve data structures for library access --- src/constants.rs | 31 --------------------- src/ds/key_node.rs | 12 ++++---- src/ds/mismatch.rs | 4 +-- src/enums.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/main.rs | 2 +- src/process.rs | 2 +- 7 files changed, 79 insertions(+), 42 deletions(-) delete mode 100644 src/constants.rs create mode 100644 src/enums.rs diff --git a/src/constants.rs b/src/constants.rs deleted file mode 100644 index 15cb8a8..0000000 --- a/src/constants.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::fmt::{Display, Formatter}; -use thiserror::Error; -use vg_errortools::FatIOError; - -#[derive(Debug, Error)] -pub enum Error { - #[error("Error opening file: {0}")] - IOError(#[from] FatIOError), - #[error("Error parsing first json: {0}")] - JSON(#[from] serde_json::Error), -} - -#[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) - } -} diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 6ee7b64..f89a3c8 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -1,3 +1,4 @@ +use crate::enums::ValueType; use serde_json::Value; use std::collections::HashMap; @@ -21,7 +22,7 @@ fn truncate(s: &str, max_chars: usize) -> String { } impl KeyNode { - pub fn absolute_keys_to_vec(&self, max_display_length: Option) -> Vec { + pub fn absolute_keys_to_vec(&self, max_display_length: Option) -> Vec { let mut vec = Vec::new(); self.absolute_keys(&mut vec, None, max_display_length); vec @@ -29,7 +30,7 @@ impl KeyNode { pub fn absolute_keys( &self, - keys: &mut Vec, + keys: &mut Vec, key_from_root: Option, max_display_length: Option, ) { @@ -42,12 +43,11 @@ impl KeyNode { .unwrap_or_default() }; match self { - KeyNode::Nil => keys.push(key_from_root.unwrap_or_default()), - KeyNode::Value(a, b) => keys.push(format!( - "{} [ {} :: {} ]", + KeyNode::Nil => keys.push(ValueType::new_key(key_from_root.unwrap_or_default())), + KeyNode::Value(a, b) => keys.push(ValueType::new_value( val_key(key_from_root), truncate(a.to_string().as_str(), max_display_length), - truncate(b.to_string().as_str(), max_display_length) + truncate(b.to_string().as_str(), max_display_length), )), KeyNode::Node(map) => { for (key, value) in map { diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 3dd7a98..225bf18 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -1,5 +1,5 @@ -use crate::constants::DiffType; use crate::ds::key_node::KeyNode; +use crate::enums::{DiffType, ValueType}; #[derive(Debug, PartialEq)] pub struct Mismatch { @@ -17,7 +17,7 @@ impl Mismatch { } } - pub fn all_diffs(&self) -> Vec<(DiffType, String)> { + pub fn all_diffs(&self) -> Vec<(DiffType, ValueType)> { let both = self .keys_in_both .absolute_keys_to_vec(None) diff --git a/src/enums.rs b/src/enums.rs new file mode 100644 index 0000000..1fb0dce --- /dev/null +++ b/src/enums.rs @@ -0,0 +1,68 @@ +use std::fmt::{Display, Formatter}; +use thiserror::Error; +use vg_errortools::FatIOError; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Error opening file: {0}")] + IOError(#[from] FatIOError), + #[error("Error parsing first json: {0}")] + JSON(#[from] serde_json::Error), +} + +#[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) + } +} + +pub enum ValueType { + Key(String), + Value { + key: String, + value_left: String, + value_right: String, + }, +} + +impl ValueType { + pub fn new_value(key: String, value_left: String, value_right: String) -> Self { + Self::Value { + value_right, + value_left, + key, + } + } + pub fn new_key(key: String) -> Self { + Self::Key(key) + } +} + +impl Display for ValueType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ValueType::Key(key) => write!(f, "{key}"), + ValueType::Value { + value_left, + key, + value_right, + } => { + write!(f, "{key} [ {value_left} :: {value_right} ]") + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 339800d..626d033 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ -pub mod constants; pub mod ds; +pub mod enums; pub mod process; diff --git a/src/main.rs b/src/main.rs index 1a7b1f7..25ef4b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use clap::Parser; use clap::Subcommand; -use json_diff::constants::Error; +use json_diff::enums::Error; use json_diff::{ ds::{key_node::KeyNode, mismatch::Mismatch}, process::compare_jsons, diff --git a/src/process.rs b/src/process.rs index 4b26d78..c6e86cf 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::collections::HashSet; -use crate::constants::Error; +use crate::enums::Error; use serde_json::Map; use serde_json::Value; From 59edca5b212df2f36edb1c79510c329118b7d34f Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 5 Oct 2023 20:47:43 +0200 Subject: [PATCH 11/52] Improve data structures for library access --- src/enums.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/enums.rs b/src/enums.rs index 1fb0dce..8deb5c3 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -50,6 +50,13 @@ impl ValueType { pub fn new_key(key: String) -> Self { Self::Key(key) } + + pub fn get_key(&self) -> &str { + match self { + ValueType::Value { key, .. } => key.as_str(), + ValueType::Key(key) => key.as_str(), + } + } } impl Display for ValueType { From 55fc1e4d3ada8dea17103526957ab1fc0a720991 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 5 Oct 2023 23:23:39 +0200 Subject: [PATCH 12/52] Make the truncation available in CLI --- src/ds/key_node.rs | 2 +- src/ds/mismatch.rs | 10 +++++++--- src/main.rs | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index f89a3c8..b2b051c 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -34,7 +34,7 @@ impl KeyNode { key_from_root: Option, max_display_length: Option, ) { - let max_display_length = max_display_length.unwrap_or(20); + let max_display_length = max_display_length.unwrap_or(4000); let val_key = |key: Option| { key.map(|mut s| { s.push_str(" ->"); diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 225bf18..8ee887a 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -18,19 +18,23 @@ impl Mismatch { } pub fn all_diffs(&self) -> Vec<(DiffType, ValueType)> { + self.all_diffs_trunc(None) + } + + pub fn all_diffs_trunc(&self, truncation_length: Option) -> Vec<(DiffType, ValueType)> { let both = self .keys_in_both - .absolute_keys_to_vec(None) + .absolute_keys_to_vec(truncation_length) .into_iter() .map(|k| (DiffType::Mismatch, k)); let left = self .left_only_keys - .absolute_keys_to_vec(None) + .absolute_keys_to_vec(truncation_length) .into_iter() .map(|k| (DiffType::LeftExtra, k)); let right = self .right_only_keys - .absolute_keys_to_vec(None) + .absolute_keys_to_vec(truncation_length) .into_iter() .map(|k| (DiffType::RightExtra, k)); diff --git a/src/main.rs b/src/main.rs index 25ef4b2..1ff7af1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,10 @@ enum Mode { struct Args { #[command(subcommand)] cmd: Mode, + + #[clap(short, long, default_value_t = 20)] + /// truncate keys with more chars then this parameter + truncation_length: usize, } fn main() -> Result<(), Error> { From 2413fdfbab8a210131f362578e6b637a577b56d3 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 5 Oct 2023 23:26:41 +0200 Subject: [PATCH 13/52] Bump version to 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc772d6..2796848 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "json_diff" -version = "0.1.2" +version = "0.2.0" dependencies = [ "clap", "maplit", diff --git a/Cargo.toml b/Cargo.toml index 166ada2..ef5a3b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "json_diff" -version = "0.1.2" +version = "0.2.0" authors = ["ksceriath"] -edition = "2018" +edition = "2021" license = "Unlicense" description = "A small diff tool utility for comparing jsons" readme = "README.md" From d81d4bfaf3611bcfebe50ce9dbac7d43f0d7b83c Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 6 Oct 2023 22:08:04 +0200 Subject: [PATCH 14/52] Fix issue with empty strings being propagated as diff --- Cargo.toml | 2 +- src/ds/key_node.rs | 6 +++++- src/ds/mismatch.rs | 19 +++++++++++++++++++ src/main.rs | 6 +----- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef5a3b7..e7f39b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff" -version = "0.2.0" +version = "0.2.1" authors = ["ksceriath"] edition = "2021" license = "Unlicense" diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index b2b051c..8e004a9 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -43,7 +43,11 @@ impl KeyNode { .unwrap_or_default() }; match self { - KeyNode::Nil => keys.push(ValueType::new_key(key_from_root.unwrap_or_default())), + KeyNode::Nil => { + if let Some(key) = key_from_root { + keys.push(ValueType::new_key(key)) + } + } KeyNode::Value(a, b) => keys.push(ValueType::new_value( val_key(key_from_root), truncate(a.to_string().as_str(), max_display_length), diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 8ee887a..2e71c5f 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -17,6 +17,14 @@ impl Mismatch { } } + pub fn empty() -> Self { + Mismatch { + left_only_keys: KeyNode::Nil, + keys_in_both: KeyNode::Nil, + right_only_keys: KeyNode::Nil, + } + } + pub fn all_diffs(&self) -> Vec<(DiffType, ValueType)> { self.all_diffs_trunc(None) } @@ -41,3 +49,14 @@ impl Mismatch { 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/main.rs b/src/main.rs index 1ff7af1..7715d44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,11 +49,7 @@ fn main() -> Result<(), Error> { } pub fn check_diffs(result: Mismatch) -> Result { - let no_mismatch = Mismatch { - left_only_keys: KeyNode::Nil, - right_only_keys: KeyNode::Nil, - keys_in_both: KeyNode::Nil, - }; + let no_mismatch = Mismatch::empty(); if no_mismatch == result { println!("No mismatch"); From 7d96bafed5bd12f063e37d4d84774f421f496545 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 6 Oct 2023 22:09:26 +0200 Subject: [PATCH 15/52] Cleanup main.rs --- Cargo.lock | 2 +- src/main.rs | 21 ++++++--------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2796848..31be554 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "json_diff" -version = "0.2.0" +version = "0.2.1" dependencies = [ "clap", "maplit", diff --git a/src/main.rs b/src/main.rs index 7715d44..56abcb5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,7 @@ use clap::Parser; use clap::Subcommand; use json_diff::enums::Error; -use json_diff::{ - ds::{key_node::KeyNode, mismatch::Mismatch}, - process::compare_jsons, -}; +use json_diff::{ds::mismatch::Mismatch, process::compare_jsons}; #[derive(Subcommand, Clone)] /// Input selection @@ -49,16 +46,10 @@ fn main() -> Result<(), Error> { } pub fn check_diffs(result: Mismatch) -> Result { - let no_mismatch = Mismatch::empty(); - - if no_mismatch == result { - println!("No mismatch"); - Ok(true) - } else { - let mismatches = result.all_diffs(); - for (d_type, key) in mismatches { - println!("{d_type}: {key}"); - } - Ok(false) + let mismatches = result.all_diffs(); + let is_good = mismatches.is_empty(); + for (d_type, key) in mismatches { + println!("{d_type}: {key}"); } + Ok(is_good) } From 3b60e7c17bedd29eb7bd7645c08a21a4d90bebb4 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 6 Oct 2023 22:10:36 +0200 Subject: [PATCH 16/52] bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e7f39b8..84c65c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff" -version = "0.2.1" +version = "0.2.0-rc2" authors = ["ksceriath"] edition = "2021" license = "Unlicense" From ff53f6bfbce0bb83bfd858245dba9ea708f9401c Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 6 Oct 2023 22:22:28 +0200 Subject: [PATCH 17/52] bump lockfile --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 31be554..23bf733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "json_diff" -version = "0.2.1" +version = "0.2.0-rc2" dependencies = [ "clap", "maplit", From 055c05a87ecdc66c687f793c4898535b4755ac28 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Mon, 9 Oct 2023 08:59:33 +0200 Subject: [PATCH 18/52] Fork to new package for publishing of new hvc version --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 84c65c7..df32059 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "json_diff" -version = "0.2.0-rc2" -authors = ["ksceriath"] +name = "json_diff_ng" +version = "0.2.0" +authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" -description = "A small diff tool utility for comparing jsons" +description = "A small diff tool utility for comparing jsons. Forked from ksceriath but improved for usage as a library." readme = "README.md" -homepage = "https://github.com/ksceriath/json-diff" -repository = "https://github.com/ksceriath/json-diff" +homepage = "https://github.com/ChrisRega/json-diff" +repository = "https://github.com/ChrisRega/json-diff" keywords = ["cli", "diff", "json"] categories = ["command-line-utilities"] From 8bd74d1cbce87e70a1ef5e2cc842c49b164dd44b Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Mon, 9 Oct 2023 09:03:55 +0200 Subject: [PATCH 19/52] Update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 53eaa21..bb421d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target **/*.rs.bk +.idea +Cargo.lock From b88550470c05cb79f423838276345ead2f7beef8 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Mon, 9 Oct 2023 09:05:01 +0200 Subject: [PATCH 20/52] Remove Cargo.lock --- Cargo.lock | 305 ----------------------------------------------------- 1 file changed, 305 deletions(-) delete mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 23bf733..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,305 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anstream" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" - -[[package]] -name = "anstyle-parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "clap" -version = "4.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "json_diff" -version = "0.2.0-rc2" -dependencies = [ - "clap", - "maplit", - "serde_json", - "thiserror", - "vg_errortools", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "proc-macro2" -version = "1.0.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "serde" -version = "1.0.188" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.188" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "2.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "vg_errortools" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0d7932688493f8aac7d65e824820b6506fe68ae7450bf44ac7299b1841a4a6" -dependencies = [ - "thiserror", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" From 425d4143d7be8bbc5f003198cdeae341db8dfe9e Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Tue, 10 Oct 2023 23:07:35 +0200 Subject: [PATCH 21/52] Add array support --- Cargo.toml | 2 +- README.md | 4 +- src/ds/key_node.rs | 10 ++++ src/enums.rs | 2 +- src/process.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index df32059..b6f93f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff_ng" -version = "0.2.0" +version = "0.3.0-rc1" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" diff --git a/README.md b/README.md index 0cb5852..de60bb1 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ 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: diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 8e004a9..dd07987 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; pub enum KeyNode { Nil, Value(Value, Value), + Array(Vec<(usize, KeyNode)>), Node(HashMap), } @@ -62,6 +63,15 @@ impl KeyNode { ) } } + KeyNode::Array(vec) => { + for (idx, value) in vec { + value.absolute_keys( + keys, + Some(format!("[l: {}] {}", idx, val_key(key_from_root.clone()))), + Some(max_display_length), + ) + } + } } } } diff --git a/src/enums.rs b/src/enums.rs index 8deb5c3..da15cc7 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -68,7 +68,7 @@ impl Display for ValueType { key, value_right, } => { - write!(f, "{key} [ {value_left} :: {value_right} ]") + write!(f, "{key} {{ {value_left} != {value_right} }}") } } } diff --git a/src/process.rs b/src/process.rs index c6e86cf..b1100a4 100644 --- a/src/process.rs +++ b/src/process.rs @@ -13,6 +13,17 @@ pub fn compare_jsons(a: &str, b: &str) -> Result { let value2 = serde_json::from_str(b)?; Ok(match_json(&value1, &value2)) } +fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { + if vec.is_empty() { + KeyNode::Nil + } else { + KeyNode::Node( + vec.into_iter() + .map(|(id, val)| (format!("[l: {id}] - {}", val.to_string()), KeyNode::Nil)) + .collect(), + ) + } +} pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { match (value1, value2) { @@ -38,6 +49,52 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { } Mismatch::new(left_only_keys, right_only_keys, unequal_keys) } + // this clearly needs to be improved! myers algorithm or whatever? + (Value::Array(a), Value::Array(b)) => { + let mut mismatch = Vec::new(); + let mut left_only_values = Vec::new(); + let mut right_only_values = Vec::new(); + + let max_len = a.len().max(b.len()); + for idx in 0..max_len { + let a = a.get(idx); + let b = b.get(idx); + match (a, b) { + (Some(a), Some(b)) => { + if a != b { + let Mismatch { + left_only_keys: l, + right_only_keys: r, + keys_in_both: u, + } = match_json(a, b); + for node in [l, r, u] { + if !matches!(node, KeyNode::Nil) { + mismatch.push((idx, node)); + } + } + } + } + (Some(a), None) => { + left_only_values.push((idx, a)); + } + (None, Some(b)) => { + right_only_values.push((idx, b)); + } + (None, None) => { + unreachable!(); + } + } + } + + let left_only_nodes = values_to_node(left_only_values); + let right_only_nodes = values_to_node(right_only_values); + let mismatch = if mismatch.is_empty() { + KeyNode::Nil + } else { + KeyNode::Array(mismatch) + }; + Mismatch::new(left_only_nodes, right_only_nodes, mismatch) + } (a, b) => { if a == b { Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil) @@ -132,6 +189,63 @@ mod tests { use maplit::hashmap; use serde_json::json; + #[test] + fn test_arrays_simple_diff() { + let data1 = r#"["a","b","c"]"#; + let data2 = r#"["a","b","d"]"#; + let diff = compare_jsons(data1, data2).unwrap(); + assert_eq!(diff.left_only_keys, KeyNode::Nil); + assert_eq!(diff.right_only_keys, KeyNode::Nil); + let diff = diff.keys_in_both.absolute_keys_to_vec(None); + assert_eq!(diff.len(), 1); + assert_eq!( + diff.first().unwrap().to_string(), + r#"[l: 2] -> { "c" != "d" }"# + ); + } + + #[test] + fn test_arrays_extra_left() { + let data1 = r#"["a","b","c"]"#; + let data2 = r#"["a","b"]"#; + let diff = compare_jsons(data1, data2).unwrap(); + + let diffs = diff.left_only_keys.absolute_keys_to_vec(None); + assert_eq!(diffs.len(), 1); + assert_eq!(diffs.first().unwrap().to_string(), r#" [l: 2] - "c""#); + assert_eq!(diff.keys_in_both, KeyNode::Nil); + assert_eq!(diff.right_only_keys, KeyNode::Nil); + } + + #[test] + fn test_arrays_extra_right() { + let data1 = r#"["a","b"]"#; + let data2 = r#"["a","b","c"]"#; + let diff = compare_jsons(data1, data2).unwrap(); + + let diffs = diff.right_only_keys.absolute_keys_to_vec(None); + assert_eq!(diffs.len(), 1); + assert_eq!(diffs.first().unwrap().to_string(), r#" [l: 2] - "c""#); + assert_eq!(diff.keys_in_both, KeyNode::Nil); + assert_eq!(diff.left_only_keys, KeyNode::Nil); + } + + #[test] + fn test_arrays_object_extra() { + let data1 = r#"["a","b"]"#; + let data2 = r#"["a","b", {"c": {"d": "e"} }]"#; + let diff = compare_jsons(data1, data2).unwrap(); + + let diffs = diff.right_only_keys.absolute_keys_to_vec(None); + assert_eq!(diffs.len(), 1); + assert_eq!( + diffs.first().unwrap().to_string(), + r#" [l: 2] - {"c":{"d":"e"}}"# + ); + assert_eq!(diff.keys_in_both, KeyNode::Nil); + assert_eq!(diff.left_only_keys, KeyNode::Nil); + } + #[test] fn nested_diff() { let data1 = r#"{ From 238ad5bd9a4ea59c747c336e9dbb0684c61e5372 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sat, 28 Oct 2023 22:45:57 +0200 Subject: [PATCH 22/52] Add myers diff for handling arrays --- Cargo.toml | 5 ++- README.md | 4 -- src/process.rs | 115 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 84 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b6f93f1..6f1200d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "json_diff_ng" -version = "0.3.0-rc1" +version = "0.3.0" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" -description = "A small diff tool utility for comparing jsons. Forked from ksceriath but improved for usage as a library." +description = "A small diff tool utility for comparing jsons. Forked from ksceriath and improved for usage as a library and with good support for array diffs." readme = "README.md" homepage = "https://github.com/ChrisRega/json-diff" repository = "https://github.com/ChrisRega/json-diff" @@ -28,3 +28,4 @@ vg_errortools = "0.1" serde_json = "1.0" maplit = "1.0" clap = {version = "4.4", features = ["derive"]} +diffs = "0.5" \ No newline at end of file diff --git a/README.md b/README.md index de60bb1..ab54ce8 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,6 @@ 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. -## Screenshot of diff results - -[![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) - Usage Example: `$ json_diff file source1.json source2.json` diff --git a/src/process.rs b/src/process.rs index b1100a4..a83b5d7 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,3 +1,4 @@ +use diffs::{myers, Diff, Replace}; use std::collections::HashMap; use std::collections::HashSet; @@ -25,6 +26,40 @@ fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { } } +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, + } + } +} +impl<'a> Diff for ListDiffHandler<'a> { + type Error = (); + fn delete(&mut self, old: usize, len: usize, _new: usize) -> Result<(), ()> { + self.deletion.push((old, len)); + Ok(()) + } + fn insert(&mut self, _o: usize, new: usize, len: usize) -> Result<(), ()> { + self.insertion.push((new, len)); + Ok(()) + } + fn replace(&mut self, old: usize, len: usize, new: usize, new_len: usize) -> Result<(), ()> { + self.replaced.push((old, len, new, new_len)); + Ok(()) + } +} + pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { match (value1, value2) { (Value::Object(a), Value::Object(b)) => { @@ -51,40 +86,33 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { } // this clearly needs to be improved! myers algorithm or whatever? (Value::Array(a), Value::Array(b)) => { - let mut mismatch = Vec::new(); - let mut left_only_values = Vec::new(); - let mut right_only_values = Vec::new(); - - let max_len = a.len().max(b.len()); - for idx in 0..max_len { - let a = a.get(idx); - let b = b.get(idx); - match (a, b) { - (Some(a), Some(b)) => { - if a != b { - let Mismatch { - left_only_keys: l, - right_only_keys: r, - keys_in_both: u, - } = match_json(a, b); - for node in [l, r, u] { - if !matches!(node, KeyNode::Nil) { - mismatch.push((idx, node)); - } - } - } - } - (Some(a), None) => { - left_only_values.push((idx, a)); - } - (None, Some(b)) => { - right_only_values.push((idx, b)); - } - (None, None) => { - unreachable!(); - } - } - } + 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, 0, a.len(), b, 0, b.len()).unwrap(); + + let mismatch: Vec<_> = replaced + .into_iter() + .flat_map(|(o, ol, n, _nl)| { + (0..ol).map(move |i| (o + i, match_json(&a[o + i], &b[n + i]).keys_in_both)) + }) + .collect(); + + let left_only_values: Vec<_> = deleted + .into_iter() + .flat_map(|(o, ol)| (o..o + ol).map(|i| (i, &a[i]))) + .collect(); + + let right_only_values: Vec<_> = inserted + .into_iter() + .flat_map(|(n, nl)| (n..n + nl).map(|i| (i, &b[i]))) + .collect(); let left_only_nodes = values_to_node(left_only_values); let right_only_nodes = values_to_node(right_only_values); @@ -204,6 +232,25 @@ mod tests { ); } + #[test] + fn test_arrays_more_complex_diff() { + let data1 = r#"["a","b","c"]"#; + let data2 = r#"["a","a","b","d"]"#; + let diff = compare_jsons(data1, data2).unwrap(); + + let changes_diff = diff.keys_in_both.absolute_keys_to_vec(None); + assert_eq!(diff.left_only_keys, KeyNode::Nil); + + assert_eq!(changes_diff.len(), 1); + assert_eq!( + changes_diff.first().unwrap().to_string(), + r#"[l: 2] -> { "c" != "d" }"# + ); + let insertions = diff.right_only_keys.absolute_keys_to_vec(None); + assert_eq!(insertions.len(), 1); + assert_eq!(insertions.first().unwrap().to_string(), r#" [l: 0] - "a""#); + } + #[test] fn test_arrays_extra_left() { let data1 = r#"["a","b","c"]"#; From 057c0ca7e93440663ace945285437955aa09b609 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 10 Nov 2023 22:32:09 +0100 Subject: [PATCH 23/52] Fix some problematic case for myers diff --- src/process.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/process.rs b/src/process.rs index a83b5d7..4730be6 100644 --- a/src/process.rs +++ b/src/process.rs @@ -99,8 +99,18 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { let mismatch: Vec<_> = replaced .into_iter() - .flat_map(|(o, ol, n, _nl)| { - (0..ol).map(move |i| (o + i, match_json(&a[o + i], &b[n + i]).keys_in_both)) + .flat_map(|(o, ol, n, nl)| { + let max_length = ol.max(nl); + (0..max_length).map(move |i| { + ( + o + i, + match_json( + a.get(o + i).unwrap_or(&Value::Null), + b.get(n + i).unwrap_or(&Value::Null), + ) + .keys_in_both, + ) + }) }) .collect(); @@ -277,6 +287,22 @@ mod tests { assert_eq!(diff.left_only_keys, KeyNode::Nil); } + #[test] + fn long_insertion_modification() { + let data1 = r#"["a","b","a"]"#; + let data2 = r#"["a","c","c","c","a"]"#; + let diff = compare_jsons(data1, data2).unwrap(); + let diffs = diff.keys_in_both.absolute_keys_to_vec(None); + // 1. is b!=c, second is a!=c, third is missing in data1 but c in data2 + assert_eq!(diffs.len(), 3); + assert_eq!( + diffs.last().unwrap().to_string(), + r#"[l: 3] -> { null != "c" }"# + ); + assert_eq!(diff.right_only_keys, KeyNode::Nil); + assert_eq!(diff.left_only_keys, KeyNode::Nil); + } + #[test] fn test_arrays_object_extra() { let data1 = r#"["a","b"]"#; From 1d7ea797d6cc5039ded8f87f82de66ce9fffa9c4 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 10 Nov 2023 22:32:37 +0100 Subject: [PATCH 24/52] Bump version to 0.3.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6f1200d..d76fcfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff_ng" -version = "0.3.0" +version = "0.3.1" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" From 6ab8672fa44ef872dd211a0ac6baa1f686d50aa3 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 10 Nov 2023 22:33:00 +0100 Subject: [PATCH 25/52] Fixup README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab54ce8..ec10cbf 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Usage Example: 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 From 6c4f589e9f299c23a931204a04d09da9650fa6df Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Wed, 14 Feb 2024 23:44:30 +0100 Subject: [PATCH 26/52] Add deep-sorting as an option to the API. --- Cargo.toml | 7 +- README.md | 2 - src/ds/mismatch.rs | 6 ++ src/main.rs | 6 +- src/process.rs | 188 ++++++++++++++++++++++++++++++++++++--------- 5 files changed, 168 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d76fcfc..df03e6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff_ng" -version = "0.3.1" +version = "0.4.0" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" @@ -25,7 +25,8 @@ path = "src/main.rs" [dependencies] thiserror = "1.0" vg_errortools = "0.1" -serde_json = "1.0" +serde_json = { version = "1.0", features = ["preserve_order"] } maplit = "1.0" clap = {version = "4.4", features = ["derive"]} -diffs = "0.5" \ No newline at end of file +diffs = "0.5" +closure = "0.3.0" \ No newline at end of file diff --git a/README.md b/README.md index ec10cbf..3b895d5 100644 --- a/README.md +++ b/README.md @@ -23,5 +23,3 @@ Currently, json-diff is available through crates.io (apart from building this re `$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` * Install json-diff `$ cargo install json_diff` - - diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 2e71c5f..e3f3376 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -25,6 +25,12 @@ impl Mismatch { } } + pub fn is_empty(&self) -> bool { + self.left_only_keys == KeyNode::Nil + && self.keys_in_both == KeyNode::Nil + && self.right_only_keys == KeyNode::Nil + } + pub fn all_diffs(&self) -> Vec<(DiffType, ValueType)> { self.all_diffs_trunc(None) } diff --git a/src/main.rs b/src/main.rs index 56abcb5..36a1efb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,10 @@ struct Args { #[command(subcommand)] cmd: Mode, + #[clap(short, long)] + /// deep-sort arrays before comparing + sort_arrays: bool, + #[clap(short, long, default_value_t = 20)] /// truncate keys with more chars then this parameter truncation_length: usize, @@ -36,7 +40,7 @@ fn main() -> Result<(), Error> { } }; - let mismatch = compare_jsons(&json_1, &json_2)?; + let mismatch = compare_jsons(&json_1, &json_2, args.sort_arrays)?; let comparison_result = check_diffs(mismatch)?; if !comparison_result { diff --git a/src/process.rs b/src/process.rs index 4730be6..64f35b2 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,4 +1,6 @@ +use closure::closure; use diffs::{myers, Diff, Replace}; +use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; @@ -9,10 +11,10 @@ use serde_json::Value; use crate::ds::key_node::KeyNode; use crate::ds::mismatch::Mismatch; -pub fn compare_jsons(a: &str, b: &str) -> Result { +pub fn compare_jsons(a: &str, b: &str, sort_arrays: bool) -> Result { let value1 = serde_json::from_str(a)?; let value2 = serde_json::from_str(b)?; - Ok(match_json(&value1, &value2)) + Ok(match_json(&value1, &value2, sort_arrays)) } fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { if vec.is_empty() { @@ -20,7 +22,7 @@ fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { } else { KeyNode::Node( vec.into_iter() - .map(|(id, val)| (format!("[l: {id}] - {}", val.to_string()), KeyNode::Nil)) + .map(|(id, val)| (format!("[l: {id}] - {}", val), KeyNode::Nil)) .collect(), ) } @@ -60,7 +62,7 @@ impl<'a> Diff for ListDiffHandler<'a> { } } -pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { +pub fn match_json(value1: &Value, value2: &Value, sort_arrays: bool) -> Mismatch { match (value1, value2) { (Value::Object(a), Value::Object(b)) => { let diff = intersect_maps(a, b); @@ -76,7 +78,7 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { left_only_keys: l, right_only_keys: r, keys_in_both: u, - } = match_json(a.get(&key).unwrap(), b.get(&key).unwrap()); + } = match_json(a.get(&key).unwrap(), b.get(&key).unwrap(), sort_arrays); 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); @@ -86,6 +88,9 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { } // this clearly needs to be improved! myers algorithm or whatever? (Value::Array(a), Value::Array(b)) => { + let a = preprocess_array(sort_arrays, a); + let b = preprocess_array(sort_arrays, b); + let mut replaced = Vec::new(); let mut deleted = Vec::new(); let mut inserted = Vec::new(); @@ -95,34 +100,46 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { &mut deleted, &mut inserted, )); - myers::diff(&mut diff, a, 0, a.len(), b, 0, b.len()).unwrap(); + myers::diff( + &mut diff, + a.as_slice(), + 0, + a.len(), + b.as_slice(), + 0, + b.len(), + ) + .unwrap(); let mismatch: Vec<_> = replaced .into_iter() .flat_map(|(o, ol, n, nl)| { let max_length = ol.max(nl); - (0..max_length).map(move |i| { - ( - o + i, - match_json( - a.get(o + i).unwrap_or(&Value::Null), - b.get(n + i).unwrap_or(&Value::Null), - ) - .keys_in_both, - ) - }) + (0..max_length).map(closure!(move n, ref a, ref b, move o, |i| { + let diff = match_json( + a.get(o + i).unwrap_or(&Value::Null), + b.get(n + i).unwrap_or(&Value::Null), + sort_arrays, + ) + .keys_in_both; + let position = o+i; + (position, diff)})) }) + // filter out diffs that turn out not to be diffs after all from deep-sorting + .filter(|(_, d)| d != &KeyNode::Nil) .collect(); - let left_only_values: Vec<_> = deleted - .into_iter() - .flat_map(|(o, ol)| (o..o + ol).map(|i| (i, &a[i]))) - .collect(); + 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 right_only_values: Vec<_> = inserted - .into_iter() - .flat_map(|(n, nl)| (n..n + nl).map(|i| (i, &b[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 left_only_nodes = values_to_node(left_only_values); let right_only_nodes = values_to_node(right_only_values); @@ -147,6 +164,72 @@ pub fn match_json(value1: &Value, value2: &Value) -> Mismatch { } } +fn preprocess_array(sort_arrays: bool, a: &Vec) -> Cow> { + if sort_arrays { + let mut owned = a.to_owned(); + owned.sort_by(compare_values); + Cow::Owned(owned) + } else { + Cow::Borrowed(a) + } +} + +fn compare_values(a: &Value, b: &Value) -> 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)) => { + for (a, b) in a.iter().zip(b.iter()) { + let cmp = compare_values(a, b); + 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().zip(keys_b.iter()) { + 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); + 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, + } +} + fn get_map_of_keys(set: Option>) -> KeyNode { if let Some(set) = set { KeyNode::Node( @@ -227,11 +310,46 @@ mod tests { use maplit::hashmap; use serde_json::json; + #[test] + fn test_arrays_sorted_simple() { + let data1 = r#"["a","b","c"]"#; + let data2 = r#"["b","c","a"]"#; + let diff = compare_jsons(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_jsons(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_jsons(data1, data2, true).unwrap(); + assert!(diff.is_empty()); + } + + #[test] + fn test_arrays_deep_sorted_objects_with_diff() { + let data1 = r#"[{"c": ["d","e"] },"b"]"#; + let data2 = r#"["b","c",{"c": ["e", "d"] }]"#; + let diff = compare_jsons(data1, data2, true).unwrap(); + assert!(!diff.is_empty()); + let insertions = diff.right_only_keys.absolute_keys_to_vec(None); + assert_eq!(insertions.len(), 1); + assert_eq!(insertions.first().unwrap().to_string(), r#" [l: 2] - "c""#); + } + #[test] fn test_arrays_simple_diff() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","b","d"]"#; - let diff = compare_jsons(data1, data2).unwrap(); + let diff = compare_jsons(data1, data2, false).unwrap(); assert_eq!(diff.left_only_keys, KeyNode::Nil); assert_eq!(diff.right_only_keys, KeyNode::Nil); let diff = diff.keys_in_both.absolute_keys_to_vec(None); @@ -246,7 +364,7 @@ mod tests { fn test_arrays_more_complex_diff() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","a","b","d"]"#; - let diff = compare_jsons(data1, data2).unwrap(); + let diff = compare_jsons(data1, data2, false).unwrap(); let changes_diff = diff.keys_in_both.absolute_keys_to_vec(None); assert_eq!(diff.left_only_keys, KeyNode::Nil); @@ -265,7 +383,7 @@ mod tests { fn test_arrays_extra_left() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","b"]"#; - let diff = compare_jsons(data1, data2).unwrap(); + let diff = compare_jsons(data1, data2, false).unwrap(); let diffs = diff.left_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); @@ -278,7 +396,7 @@ mod tests { fn test_arrays_extra_right() { let data1 = r#"["a","b"]"#; let data2 = r#"["a","b","c"]"#; - let diff = compare_jsons(data1, data2).unwrap(); + let diff = compare_jsons(data1, data2, false).unwrap(); let diffs = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); @@ -291,7 +409,7 @@ mod tests { fn long_insertion_modification() { let data1 = r#"["a","b","a"]"#; let data2 = r#"["a","c","c","c","a"]"#; - let diff = compare_jsons(data1, data2).unwrap(); + let diff = compare_jsons(data1, data2, false).unwrap(); let diffs = diff.keys_in_both.absolute_keys_to_vec(None); // 1. is b!=c, second is a!=c, third is missing in data1 but c in data2 assert_eq!(diffs.len(), 3); @@ -307,7 +425,7 @@ mod tests { fn test_arrays_object_extra() { let data1 = r#"["a","b"]"#; let data2 = r#"["a","b", {"c": {"d": "e"} }]"#; - let diff = compare_jsons(data1, data2).unwrap(); + let diff = compare_jsons(data1, data2, false).unwrap(); let diffs = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); @@ -390,7 +508,7 @@ mod tests { }); let expected = Mismatch::new(expected_left, expected_right, expected_uneq); - let mismatch = compare_jsons(data1, data2).unwrap(); + let mismatch = compare_jsons(data1, data2, false).unwrap(); assert_eq!(mismatch, expected, "Diff was incorrect."); } @@ -426,7 +544,7 @@ mod tests { }"#; assert_eq!( - compare_jsons(data1, data2).unwrap(), + compare_jsons(data1, data2, false).unwrap(), Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil) ); } @@ -437,7 +555,7 @@ mod tests { let data2 = r#"{}"#; assert_eq!( - compare_jsons(data1, data2).unwrap(), + compare_jsons(data1, data2, false).unwrap(), Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil) ); } @@ -446,7 +564,7 @@ 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) { + match compare_jsons(invalid_json1, valid_json2, false) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { matches!(err, Error::JSON(_)); @@ -458,7 +576,7 @@ mod tests { fn parse_err_source_two() { let valid_json1 = r#"{"a":"b"}"#; let invalid_json2 = r#"{invalid: json}"#; - match compare_jsons(valid_json1, invalid_json2) { + match compare_jsons(valid_json1, invalid_json2, false) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { matches!(err, Error::JSON(_)); From a16874d412f54b18a94a71caf2c8a04293cd071d Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 16 Feb 2024 00:10:48 +0100 Subject: [PATCH 27/52] Fix inner diffs in deep sorting. Remove extra white-space. --- Cargo.toml | 3 +- src/ds/key_node.rs | 6 +-- src/enums.rs | 2 +- src/process.rs | 114 ++++++++++++++++++++++++++++----------------- 4 files changed, 75 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index df03e6f..fb0e98c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,5 +28,4 @@ vg_errortools = "0.1" serde_json = { version = "1.0", features = ["preserve_order"] } maplit = "1.0" clap = {version = "4.4", features = ["derive"]} -diffs = "0.5" -closure = "0.3.0" \ No newline at end of file +diffs = "0.5" \ No newline at end of file diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index dd07987..9b5751c 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -38,7 +38,7 @@ impl KeyNode { let max_display_length = max_display_length.unwrap_or(4000); let val_key = |key: Option| { key.map(|mut s| { - s.push_str(" ->"); + s.push_str("->"); s }) .unwrap_or_default() @@ -58,7 +58,7 @@ impl KeyNode { for (key, value) in map { value.absolute_keys( keys, - Some(format!("{} {}", val_key(key_from_root.clone()), key)), + Some(format!("{}{}", val_key(key_from_root.clone()), key)), Some(max_display_length), ) } @@ -67,7 +67,7 @@ impl KeyNode { for (idx, value) in vec { value.absolute_keys( keys, - Some(format!("[l: {}] {}", idx, val_key(key_from_root.clone()))), + Some(format!("[l: {}]{}", idx, val_key(key_from_root.clone()))), Some(max_display_length), ) } diff --git a/src/enums.rs b/src/enums.rs index da15cc7..c9d261e 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -68,7 +68,7 @@ impl Display for ValueType { key, value_right, } => { - write!(f, "{key} {{ {value_left} != {value_right} }}") + write!(f, "{key}{{{value_left}!={value_right}}}") } } } diff --git a/src/process.rs b/src/process.rs index 64f35b2..2d54243 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,4 +1,3 @@ -use closure::closure; use diffs::{myers, Diff, Replace}; use std::borrow::Cow; use std::collections::HashMap; @@ -22,7 +21,7 @@ fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { } else { KeyNode::Node( vec.into_iter() - .map(|(id, val)| (format!("[l: {id}] - {}", val), KeyNode::Nil)) + .map(|(id, val)| (format!("[l: {id}]-{}", val), KeyNode::Nil)) .collect(), ) } @@ -111,24 +110,6 @@ pub fn match_json(value1: &Value, value2: &Value, sort_arrays: bool) -> Mismatch ) .unwrap(); - let mismatch: Vec<_> = replaced - .into_iter() - .flat_map(|(o, ol, n, nl)| { - let max_length = ol.max(nl); - (0..max_length).map(closure!(move n, ref a, ref b, move o, |i| { - let diff = match_json( - a.get(o + i).unwrap_or(&Value::Null), - b.get(n + i).unwrap_or(&Value::Null), - sort_arrays, - ) - .keys_in_both; - let position = o+i; - (position, diff)})) - }) - // filter out diffs that turn out not to be diffs after all from deep-sorting - .filter(|(_, d)| d != &KeyNode::Nil) - .collect(); - fn extract_one_sided_values( v: Vec<(usize, usize)>, vals: &[Value], @@ -141,14 +122,32 @@ pub fn match_json(value1: &Value, value2: &Value, sort_arrays: bool) -> Mismatch 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 left_only_nodes = values_to_node(left_only_values); - let right_only_nodes = values_to_node(right_only_values); - let mismatch = if mismatch.is_empty() { - KeyNode::Nil - } else { - KeyNode::Array(mismatch) - }; - Mismatch::new(left_only_nodes, right_only_nodes, mismatch) + 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 = KeyNode::Nil; + + 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); + let position = o + i; + let Mismatch { + left_only_keys: l, + right_only_keys: r, + keys_in_both: u, + } = cdiff; + left_only_nodes = + insert_child_key_map(left_only_nodes, l, &format!("[l: {position}]")); + right_only_nodes = + insert_child_key_map(right_only_nodes, r, &format!("[l: {position}]")); + diff = insert_child_key_map(diff, u, &format!("[l: {position}]")); + } + } + + Mismatch::new(left_only_nodes, right_only_nodes, diff) } (a, b) => { if a == b { @@ -335,14 +334,44 @@ mod tests { } #[test] - fn test_arrays_deep_sorted_objects_with_diff() { + 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_jsons(data1, data2, true).unwrap(); assert!(!diff.is_empty()); let insertions = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(insertions.len(), 1); - assert_eq!(insertions.first().unwrap().to_string(), r#" [l: 2] - "c""#); + assert_eq!(insertions.first().unwrap().to_string(), r#"[l: 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_jsons(data1, data2, true).unwrap(); + assert!(!diff.is_empty()); + let deletions = diff.left_only_keys.absolute_keys_to_vec(None); + + assert_eq!(deletions.len(), 1); + assert_eq!( + deletions.first().unwrap().to_string(), + r#"[l: 0]->c->[l: 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 diff = compare_jsons(data1, data2, true).unwrap(); + assert!(!diff.is_empty()); + let diffs = diff.keys_in_both.absolute_keys_to_vec(None); + + assert_eq!(diffs.len(), 1); + assert_eq!( + diffs.first().unwrap().to_string(), + r#"[l: 0]->c->[l: 1]->{"f"!="e"}"# + ); } #[test] @@ -354,10 +383,7 @@ mod tests { assert_eq!(diff.right_only_keys, KeyNode::Nil); let diff = diff.keys_in_both.absolute_keys_to_vec(None); assert_eq!(diff.len(), 1); - assert_eq!( - diff.first().unwrap().to_string(), - r#"[l: 2] -> { "c" != "d" }"# - ); + assert_eq!(diff.first().unwrap().to_string(), r#"[l: 2]->{"c"!="d"}"#); } #[test] @@ -372,11 +398,11 @@ mod tests { assert_eq!(changes_diff.len(), 1); assert_eq!( changes_diff.first().unwrap().to_string(), - r#"[l: 2] -> { "c" != "d" }"# + r#"[l: 2]->{"c"!="d"}"# ); let insertions = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(insertions.len(), 1); - assert_eq!(insertions.first().unwrap().to_string(), r#" [l: 0] - "a""#); + assert_eq!(insertions.first().unwrap().to_string(), r#"[l: 0]-"a""#); } #[test] @@ -387,7 +413,7 @@ mod tests { let diffs = diff.left_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); - assert_eq!(diffs.first().unwrap().to_string(), r#" [l: 2] - "c""#); + assert_eq!(diffs.first().unwrap().to_string(), r#"[l: 2]-"c""#); assert_eq!(diff.keys_in_both, KeyNode::Nil); assert_eq!(diff.right_only_keys, KeyNode::Nil); } @@ -400,7 +426,7 @@ mod tests { let diffs = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); - assert_eq!(diffs.first().unwrap().to_string(), r#" [l: 2] - "c""#); + assert_eq!(diffs.first().unwrap().to_string(), r#"[l: 2]-"c""#); assert_eq!(diff.keys_in_both, KeyNode::Nil); assert_eq!(diff.left_only_keys, KeyNode::Nil); } @@ -411,12 +437,12 @@ mod tests { let data2 = r#"["a","c","c","c","a"]"#; let diff = compare_jsons(data1, data2, false).unwrap(); let diffs = diff.keys_in_both.absolute_keys_to_vec(None); - // 1. is b!=c, second is a!=c, third is missing in data1 but c in data2 + assert_eq!(diffs.len(), 3); - assert_eq!( - diffs.last().unwrap().to_string(), - r#"[l: 3] -> { null != "c" }"# - ); + let diffs: Vec<_> = diffs.into_iter().map(|d| d.to_string()).collect(); + assert!(diffs.contains(&r#"[l: 3]->{null!="c"}"#.to_string())); + assert!(diffs.contains(&r#"[l: 1]->{"b"!="c"}"#.to_string())); + assert!(diffs.contains(&r#"[l: 2]->{"a"!="c"}"#.to_string())); assert_eq!(diff.right_only_keys, KeyNode::Nil); assert_eq!(diff.left_only_keys, KeyNode::Nil); } @@ -431,7 +457,7 @@ mod tests { assert_eq!(diffs.len(), 1); assert_eq!( diffs.first().unwrap().to_string(), - r#" [l: 2] - {"c":{"d":"e"}}"# + r#"[l: 2]-{"c":{"d":"e"}}"# ); assert_eq!(diff.keys_in_both, KeyNode::Nil); assert_eq!(diff.left_only_keys, KeyNode::Nil); From e6429b943bad8c629fd4454ce96ac68a6f038402 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sat, 17 Feb 2024 22:55:54 +0100 Subject: [PATCH 28/52] Remove unused Array variant. --- src/ds/key_node.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 9b5751c..4c1d69f 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; pub enum KeyNode { Nil, Value(Value, Value), - Array(Vec<(usize, KeyNode)>), Node(HashMap), } @@ -63,15 +62,6 @@ impl KeyNode { ) } } - KeyNode::Array(vec) => { - for (idx, value) in vec { - value.absolute_keys( - keys, - Some(format!("[l: {}]{}", idx, val_key(key_from_root.clone()))), - Some(max_display_length), - ) - } - } } } } From 475e4ceda5537e37ddb99d224325bef99695d574 Mon Sep 17 00:00:00 2001 From: Tsvetelin Pantev Date: Thu, 14 Mar 2024 16:18:47 +0100 Subject: [PATCH 29/52] sort nested arrays --- src/process.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/process.rs b/src/process.rs index 2d54243..bdc2086 100644 --- a/src/process.rs +++ b/src/process.rs @@ -191,6 +191,8 @@ fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering { } (Value::String(a), Value::String(b)) => a.cmp(b), (Value::Array(a), Value::Array(b)) => { + let a = preprocess_array(true, a); + let b = preprocess_array(true, b); for (a, b) in a.iter().zip(b.iter()) { let cmp = compare_values(a, b); if cmp != std::cmp::Ordering::Equal { @@ -333,6 +335,14 @@ mod tests { 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_jsons(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"]"#; From 192e1a9bad3912adc7e4bc2b4c0350da739b875e Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sun, 17 Mar 2024 22:22:50 +0100 Subject: [PATCH 30/52] Remove obsolete comment. Bump version --- Cargo.toml | 2 +- src/process.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb0e98c..2454333 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff_ng" -version = "0.4.0" +version = "0.4.1" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" diff --git a/src/process.rs b/src/process.rs index bdc2086..1e9141c 100644 --- a/src/process.rs +++ b/src/process.rs @@ -85,7 +85,6 @@ pub fn match_json(value1: &Value, value2: &Value, sort_arrays: bool) -> Mismatch } Mismatch::new(left_only_keys, right_only_keys, unequal_keys) } - // this clearly needs to be improved! myers algorithm or whatever? (Value::Array(a), Value::Array(b)) => { let a = preprocess_array(sort_arrays, a); let b = preprocess_array(sort_arrays, b); From ecbf31409167f2c1d569c9a97dde5b24921ce110 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 3 May 2024 21:43:35 +0200 Subject: [PATCH 31/52] Add first shot at ignoring the diff of keys. No sorting yet. --- Cargo.toml | 3 +- src/main.rs | 4 +- src/process.rs | 131 ++++++++++++++++++++++++++++--------------------- 3 files changed, 80 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2454333..669c5d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,4 +28,5 @@ vg_errortools = "0.1" serde_json = { version = "1.0", features = ["preserve_order"] } maplit = "1.0" clap = {version = "4.4", features = ["derive"]} -diffs = "0.5" \ No newline at end of file +diffs = "0.5" +regex = "1.10" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 36a1efb..e4b39aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use clap::Parser; use clap::Subcommand; -use json_diff::enums::Error; use json_diff::{ds::mismatch::Mismatch, process::compare_jsons}; +use json_diff::enums::Error; #[derive(Subcommand, Clone)] /// Input selection @@ -40,7 +40,7 @@ fn main() -> Result<(), Error> { } }; - let mismatch = compare_jsons(&json_1, &json_2, args.sort_arrays)?; + let mismatch = compare_jsons(&json_1, &json_2, args.sort_arrays, &[])?; let comparison_result = check_diffs(mismatch)?; if !comparison_result { diff --git a/src/process.rs b/src/process.rs index 1e9141c..1456576 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,19 +1,25 @@ -use diffs::{myers, Diff, Replace}; use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; -use crate::enums::Error; +use diffs::{Diff, myers, Replace}; +use regex::Regex; use serde_json::Map; use serde_json::Value; use crate::ds::key_node::KeyNode; use crate::ds::mismatch::Mismatch; +use crate::enums::Error; -pub fn compare_jsons(a: &str, b: &str, sort_arrays: bool) -> Result { +pub fn compare_jsons( + 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)?; - Ok(match_json(&value1, &value2, sort_arrays)) + Ok(match_json(&value1, &value2, sort_arrays, ignore_keys)) } fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { if vec.is_empty() { @@ -61,28 +67,37 @@ impl<'a> Diff for ListDiffHandler<'a> { } } -pub fn match_json(value1: &Value, value2: &Value, sort_arrays: bool) -> Mismatch { +pub fn match_json( + value1: &Value, + value2: &Value, + sort_arrays: bool, + ignore_keys: &[Regex], +) -> Mismatch { match (value1, value2) { (Value::Object(a), Value::Object(b)) => { - let diff = intersect_maps(a, b); + 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 = KeyNode::Nil; - 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(), sort_arrays); - 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); - } + 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(), + 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); } + Mismatch::new(left_only_keys, right_only_keys, unequal_keys) } (Value::Array(a), Value::Array(b)) => { @@ -131,7 +146,7 @@ pub fn match_json(value1: &Value, value2: &Value, sort_arrays: bool) -> Mismatch 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); + let cdiff = match_json(inner_a, inner_b, sort_arrays, ignore_keys); let position = o + i; let Mismatch { left_only_keys: l, @@ -230,8 +245,8 @@ fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering { } } -fn get_map_of_keys(set: Option>) -> KeyNode { - if let Some(set) = set { +fn get_map_of_keys(set: HashSet) -> KeyNode { + if !set.is_empty() { KeyNode::Node( set.iter() .map(|key| (String::from(key), KeyNode::Nil)) @@ -259,16 +274,16 @@ fn insert_child_key_map(parent: KeyNode, child: KeyNode, key: &String) -> KeyNod } struct MapDifference { - left_only: Option>, - right_only: Option>, - intersection: Option>, + left_only: HashSet, + right_only: HashSet, + intersection: HashSet, } impl MapDifference { pub fn new( - left_only: Option>, - right_only: Option>, - intersection: Option>, + left_only: HashSet, + right_only: HashSet, + intersection: HashSet, ) -> Self { Self { right_only, @@ -278,43 +293,49 @@ impl MapDifference { } } -fn intersect_maps(a: &Map, b: &Map) -> MapDifference { +fn intersect_maps( + a: &Map, + b: &Map, + 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.is_empty() { None } else { Some(left) }; - let right = if right.is_empty() { None } else { Some(right) }; - let intersection = if intersection.is_empty() { - None - } else { - Some(intersection) - }; + MapDifference::new(left, right, intersection) } #[cfg(test)] mod tests { - use super::*; use maplit::hashmap; use serde_json::json; + use super::*; + #[test] fn test_arrays_sorted_simple() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["b","c","a"]"#; - let diff = compare_jsons(data1, data2, true).unwrap(); + let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -322,7 +343,7 @@ mod tests { fn test_arrays_sorted_objects() { let data1 = r#"[{"c": {"d": "e"} },"b","c"]"#; let data2 = r#"["b","c",{"c": {"d": "e"} }]"#; - let diff = compare_jsons(data1, data2, true).unwrap(); + let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -330,7 +351,7 @@ mod tests { 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_jsons(data1, data2, true).unwrap(); + let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -338,7 +359,7 @@ mod tests { 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_jsons(data1, data2, true).unwrap(); + let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -346,7 +367,7 @@ mod tests { 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_jsons(data1, data2, true).unwrap(); + let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); let insertions = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(insertions.len(), 1); @@ -357,7 +378,7 @@ mod tests { 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_jsons(data1, data2, true).unwrap(); + let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); let deletions = diff.left_only_keys.absolute_keys_to_vec(None); @@ -372,7 +393,7 @@ mod tests { 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 diff = compare_jsons(data1, data2, true).unwrap(); + let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); let diffs = diff.keys_in_both.absolute_keys_to_vec(None); @@ -387,7 +408,7 @@ mod tests { fn test_arrays_simple_diff() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","b","d"]"#; - let diff = compare_jsons(data1, data2, false).unwrap(); + let diff = compare_jsons(data1, data2, false, &[]).unwrap(); assert_eq!(diff.left_only_keys, KeyNode::Nil); assert_eq!(diff.right_only_keys, KeyNode::Nil); let diff = diff.keys_in_both.absolute_keys_to_vec(None); @@ -399,7 +420,7 @@ mod tests { fn test_arrays_more_complex_diff() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","a","b","d"]"#; - let diff = compare_jsons(data1, data2, false).unwrap(); + let diff = compare_jsons(data1, data2, false, &[]).unwrap(); let changes_diff = diff.keys_in_both.absolute_keys_to_vec(None); assert_eq!(diff.left_only_keys, KeyNode::Nil); @@ -418,7 +439,7 @@ mod tests { fn test_arrays_extra_left() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","b"]"#; - let diff = compare_jsons(data1, data2, false).unwrap(); + let diff = compare_jsons(data1, data2, false, &[]).unwrap(); let diffs = diff.left_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); @@ -431,7 +452,7 @@ mod tests { fn test_arrays_extra_right() { let data1 = r#"["a","b"]"#; let data2 = r#"["a","b","c"]"#; - let diff = compare_jsons(data1, data2, false).unwrap(); + let diff = compare_jsons(data1, data2, false, &[]).unwrap(); let diffs = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); @@ -444,7 +465,7 @@ mod tests { fn long_insertion_modification() { let data1 = r#"["a","b","a"]"#; let data2 = r#"["a","c","c","c","a"]"#; - let diff = compare_jsons(data1, data2, false).unwrap(); + let diff = compare_jsons(data1, data2, false, &[]).unwrap(); let diffs = diff.keys_in_both.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 3); @@ -460,7 +481,7 @@ mod tests { fn test_arrays_object_extra() { let data1 = r#"["a","b"]"#; let data2 = r#"["a","b", {"c": {"d": "e"} }]"#; - let diff = compare_jsons(data1, data2, false).unwrap(); + let diff = compare_jsons(data1, data2, false, &[]).unwrap(); let diffs = diff.right_only_keys.absolute_keys_to_vec(None); assert_eq!(diffs.len(), 1); @@ -543,7 +564,7 @@ mod tests { }); let expected = Mismatch::new(expected_left, expected_right, expected_uneq); - let mismatch = compare_jsons(data1, data2, false).unwrap(); + let mismatch = compare_jsons(data1, data2, false, &[]).unwrap(); assert_eq!(mismatch, expected, "Diff was incorrect."); } @@ -579,7 +600,7 @@ mod tests { }"#; assert_eq!( - compare_jsons(data1, data2, false).unwrap(), + compare_jsons(data1, data2, false, &[]).unwrap(), Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil) ); } @@ -590,7 +611,7 @@ mod tests { let data2 = r#"{}"#; assert_eq!( - compare_jsons(data1, data2, false).unwrap(), + compare_jsons(data1, data2, false, &[]).unwrap(), Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil) ); } @@ -599,7 +620,7 @@ 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, false) { + match compare_jsons(invalid_json1, valid_json2, false, &[]) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { matches!(err, Error::JSON(_)); @@ -611,7 +632,7 @@ mod tests { fn parse_err_source_two() { let valid_json1 = r#"{"a":"b"}"#; let invalid_json2 = r#"{invalid: json}"#; - match compare_jsons(valid_json1, invalid_json2, false) { + match compare_jsons(valid_json1, invalid_json2, false, &[]) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { matches!(err, Error::JSON(_)); From cd42fd7432b3d9076c46828d3ccd5a7838ecbde2 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 3 May 2024 22:26:42 +0200 Subject: [PATCH 32/52] Adding first unit test for sorting with ignores --- src/process.rs | 77 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/src/process.rs b/src/process.rs index 1456576..76f3319 100644 --- a/src/process.rs +++ b/src/process.rs @@ -101,8 +101,8 @@ pub fn match_json( Mismatch::new(left_only_keys, right_only_keys, unequal_keys) } (Value::Array(a), Value::Array(b)) => { - let a = preprocess_array(sort_arrays, a); - let b = preprocess_array(sort_arrays, b); + 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(); @@ -177,17 +177,21 @@ pub fn match_json( } } -fn preprocess_array(sort_arrays: bool, a: &Vec) -> Cow> { - if sort_arrays { +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(compare_values); + 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) -> std::cmp::Ordering { +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, @@ -205,10 +209,10 @@ fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering { } (Value::String(a), Value::String(b)) => a.cmp(b), (Value::Array(a), Value::Array(b)) => { - let a = preprocess_array(true, a); - let b = preprocess_array(true, 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); + let cmp = compare_values(a, b, ignore_keys); if cmp != std::cmp::Ordering::Equal { return cmp; } @@ -220,14 +224,22 @@ fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering { let mut keys_b: Vec<_> = b.keys().collect(); keys_a.sort(); keys_b.sort(); - for (key_a, key_b) in keys_a.iter().zip(keys_b.iter()) { + 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); + let cmp = compare_values(value_a, value_b, ignore_keys); if cmp != std::cmp::Ordering::Equal { return cmp; } @@ -331,6 +343,49 @@ mod tests { 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_jsons(data1, data2, true, &[ignore]).unwrap(); + assert!(diff.is_empty()); + } + #[test] fn test_arrays_sorted_simple() { let data1 = r#"["a","b","c"]"#; From 57660763ae995ade758f2b0552b08639253d2e36 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Fri, 3 May 2024 22:57:20 +0200 Subject: [PATCH 33/52] Bump crate version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 669c5d7..5c3d9e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff_ng" -version = "0.4.1" +version = "0.5.0" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" @@ -27,6 +27,6 @@ thiserror = "1.0" vg_errortools = "0.1" serde_json = { version = "1.0", features = ["preserve_order"] } maplit = "1.0" -clap = {version = "4.4", features = ["derive"]} +clap = { version = "4.5", features = ["derive"] } diffs = "0.5" regex = "1.10" \ No newline at end of file From 7073fd23587bea9cf12238cb00d4e2f3dd2167e0 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 23 May 2024 00:08:42 +0200 Subject: [PATCH 34/52] - Clean up truncation - Write better return type - Improve array return type support - Improve visual representation of diffs (more jq-like) --- Cargo.toml | 4 +-- src/ds/key_node.rs | 81 ++++++++++++++++++++-------------------------- src/ds/mismatch.rs | 15 ++++----- src/enums.rs | 58 ++++++++++++++++----------------- src/process.rs | 75 +++++++++++++++++++++++++----------------- 5 files changed, 116 insertions(+), 117 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5c3d9e6..d953f26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "json_diff_ng" -version = "0.5.0" +version = "0.6.0-RC1" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" -description = "A small diff tool utility for comparing jsons. Forked from ksceriath and improved for usage as a library and with good support for array diffs." +description = "A small diff tool utility for comparing jsons. Forked from ksceriath and improved for usage as a library and with proper support for array diffs." readme = "README.md" homepage = "https://github.com/ChrisRega/json-diff" repository = "https://github.com/ChrisRega/json-diff" diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 4c1d69f..41bd766 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -1,65 +1,54 @@ -use crate::enums::ValueType; -use serde_json::Value; use std::collections::HashMap; +use serde_json::Value; + +use crate::enums::{DiffEntry, PathElement}; + #[derive(Debug, PartialEq)] pub enum KeyNode { Nil, Value(Value, Value), Node(HashMap), -} - -fn truncate(s: &str, max_chars: usize) -> String { - match s.char_indices().nth(max_chars) { - None => String::from(s), - Some((idx, _)) => { - let shorter = &s[..idx]; - let snip = "//SNIP//"; - let new_s = format!("{}{}", shorter, snip); - new_s - } - } + Array(Vec<(usize, KeyNode)>), } impl KeyNode { - pub fn absolute_keys_to_vec(&self, max_display_length: Option) -> Vec { - let mut vec = Vec::new(); - self.absolute_keys(&mut vec, None, max_display_length); - vec + pub fn get_diffs(&self) -> Vec { + let mut buf = Vec::new(); + self.follow_path(&mut buf, &[]); + buf } - pub fn absolute_keys( - &self, - keys: &mut Vec, - key_from_root: Option, - max_display_length: Option, - ) { - let max_display_length = max_display_length.unwrap_or(4000); - let val_key = |key: Option| { - key.map(|mut s| { - s.push_str("->"); - s - }) - .unwrap_or_default() - }; + pub fn follow_path(&self, diffs: &mut Vec, offset: &[PathElement]) { match self { KeyNode::Nil => { - if let Some(key) = key_from_root { - keys.push(ValueType::new_key(key)) + 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, + }); + } + } + KeyNode::Value(l, r) => diffs.push(DiffEntry { + path: offset.to_vec(), + values: Some((l.to_string(), r.to_string())), + }), + KeyNode::Node(o) => { + for (k, v) in o { + let mut new_offset = offset.to_vec(); + new_offset.push(PathElement::Object(k.clone())); + v.follow_path(diffs, &new_offset); } } - KeyNode::Value(a, b) => keys.push(ValueType::new_value( - val_key(key_from_root), - truncate(a.to_string().as_str(), max_display_length), - truncate(b.to_string().as_str(), max_display_length), - )), - KeyNode::Node(map) => { - for (key, value) in map { - value.absolute_keys( - keys, - Some(format!("{}{}", val_key(key_from_root.clone()), key)), - Some(max_display_length), - ) + KeyNode::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); } } } diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index e3f3376..92f2ff4 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -1,5 +1,5 @@ use crate::ds::key_node::KeyNode; -use crate::enums::{DiffType, ValueType}; +use crate::enums::{DiffEntry, DiffType}; #[derive(Debug, PartialEq)] pub struct Mismatch { @@ -31,24 +31,20 @@ impl Mismatch { && self.right_only_keys == KeyNode::Nil } - pub fn all_diffs(&self) -> Vec<(DiffType, ValueType)> { - self.all_diffs_trunc(None) - } - - pub fn all_diffs_trunc(&self, truncation_length: Option) -> Vec<(DiffType, ValueType)> { + pub fn all_diffs(&self) -> Vec<(DiffType, DiffEntry)> { let both = self .keys_in_both - .absolute_keys_to_vec(truncation_length) + .get_diffs() .into_iter() .map(|k| (DiffType::Mismatch, k)); let left = self .left_only_keys - .absolute_keys_to_vec(truncation_length) + .get_diffs() .into_iter() .map(|k| (DiffType::LeftExtra, k)); let right = self .right_only_keys - .absolute_keys_to_vec(truncation_length) + .get_diffs() .into_iter() .map(|k| (DiffType::RightExtra, k)); @@ -59,6 +55,7 @@ impl Mismatch { #[cfg(test)] mod test { use super::*; + #[test] fn empty_diffs() { let empty = Mismatch::empty(); diff --git a/src/enums.rs b/src/enums.rs index c9d261e..6c7f51f 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,4 +1,5 @@ use std::fmt::{Display, Formatter}; + use thiserror::Error; use vg_errortools::FatIOError; @@ -30,45 +31,42 @@ impl Display for DiffType { } } -pub enum ValueType { - Key(String), - Value { - key: String, - value_left: String, - value_right: String, - }, +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PathElement { + Object(String), + ArrayEntry(usize), } -impl ValueType { - pub fn new_value(key: String, value_left: String, value_right: String) -> Self { - Self::Value { - value_right, - value_left, - key, - } - } - pub fn new_key(key: String) -> Self { - Self::Key(key) - } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct DiffEntry { + pub path: Vec, + pub values: Option<(String, String)>, +} - pub fn get_key(&self) -> &str { - match self { - ValueType::Value { key, .. } => key.as_str(), - ValueType::Key(key) => key.as_str(), +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 ValueType { +impl Display for PathElement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - ValueType::Key(key) => write!(f, "{key}"), - ValueType::Value { - value_left, - key, - value_right, - } => { - write!(f, "{key}{{{value_left}!={value_right}}}") + PathElement::Object(o) => { + write!(f, "{o}") + } + PathElement::ArrayEntry(l) => { + write!(f, "[{l}]") } } } diff --git a/src/process.rs b/src/process.rs index 76f3319..a6663b4 100644 --- a/src/process.rs +++ b/src/process.rs @@ -25,9 +25,9 @@ fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { if vec.is_empty() { KeyNode::Nil } else { - KeyNode::Node( + KeyNode::Array( vec.into_iter() - .map(|(id, val)| (format!("[l: {id}]-{}", val), KeyNode::Nil)) + .map(|(l, v)| (l, KeyNode::Value(v.clone(), v.clone()))) .collect(), ) } @@ -153,11 +153,9 @@ pub fn match_json( right_only_keys: r, keys_in_both: u, } = cdiff; - left_only_nodes = - insert_child_key_map(left_only_nodes, l, &format!("[l: {position}]")); - right_only_nodes = - insert_child_key_map(right_only_nodes, r, &format!("[l: {position}]")); - diff = insert_child_key_map(diff, u, &format!("[l: {position}]")); + 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); } } @@ -269,13 +267,27 @@ fn get_map_of_keys(set: HashSet) -> KeyNode { } } +fn insert_child_key_diff(parent: KeyNode, child: KeyNode, line: usize) -> KeyNode { + if child == KeyNode::Nil { + return parent; + } + if let KeyNode::Array(mut array) = parent { + array.push((line, child)); + KeyNode::Array(array) + } else if let KeyNode::Nil = parent { + KeyNode::Array(vec![(line, child)]) + } else { + parent // TODO Trying to insert child node in a Value variant : Should not happen => Throw an error instead. + } +} + fn insert_child_key_map(parent: KeyNode, child: KeyNode, key: &String) -> KeyNode { if child == KeyNode::Nil { return parent; } if let KeyNode::Node(mut map) = parent { map.insert(String::from(key), child); - KeyNode::Node(map) // This is weird! I just wanted to return back `parent` here + KeyNode::Node(map) } else if let KeyNode::Nil = parent { let mut map = HashMap::new(); map.insert(String::from(key), child); @@ -424,9 +436,9 @@ mod tests { let data2 = r#"["b","c",{"c": ["e", "d"] }]"#; let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); - let insertions = diff.right_only_keys.absolute_keys_to_vec(None); + let insertions = diff.right_only_keys.get_diffs(); assert_eq!(insertions.len(), 1); - assert_eq!(insertions.first().unwrap().to_string(), r#"[l: 2]-"c""#); + assert_eq!(insertions.first().unwrap().to_string(), r#".[2].("c")"#); } #[test] @@ -435,12 +447,12 @@ mod tests { let data2 = r#"["b",{"c": ["e","d"] },"a"]"#; let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); - let deletions = diff.left_only_keys.absolute_keys_to_vec(None); + let deletions = diff.left_only_keys.get_diffs(); assert_eq!(deletions.len(), 1); assert_eq!( deletions.first().unwrap().to_string(), - r#"[l: 0]->c->[l: 2]-"f""# + r#".[0].c.[2].("f")"# ); } @@ -450,12 +462,12 @@ mod tests { let data2 = r#"["b",{"c": ["e","d"] },"a"]"#; let diff = compare_jsons(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); - let diffs = diff.keys_in_both.absolute_keys_to_vec(None); + let diffs = diff.keys_in_both.get_diffs(); assert_eq!(diffs.len(), 1); assert_eq!( diffs.first().unwrap().to_string(), - r#"[l: 0]->c->[l: 1]->{"f"!="e"}"# + r#".[0].c.[1].("f" != "e")"# ); } @@ -466,9 +478,9 @@ mod tests { let diff = compare_jsons(data1, data2, false, &[]).unwrap(); assert_eq!(diff.left_only_keys, KeyNode::Nil); assert_eq!(diff.right_only_keys, KeyNode::Nil); - let diff = diff.keys_in_both.absolute_keys_to_vec(None); + let diff = diff.keys_in_both.get_diffs(); assert_eq!(diff.len(), 1); - assert_eq!(diff.first().unwrap().to_string(), r#"[l: 2]->{"c"!="d"}"#); + assert_eq!(diff.first().unwrap().to_string(), r#".[2].("c" != "d")"#); } #[test] @@ -477,17 +489,17 @@ mod tests { let data2 = r#"["a","a","b","d"]"#; let diff = compare_jsons(data1, data2, false, &[]).unwrap(); - let changes_diff = diff.keys_in_both.absolute_keys_to_vec(None); + let changes_diff = diff.keys_in_both.get_diffs(); assert_eq!(diff.left_only_keys, KeyNode::Nil); assert_eq!(changes_diff.len(), 1); assert_eq!( changes_diff.first().unwrap().to_string(), - r#"[l: 2]->{"c"!="d"}"# + r#".[2].("c" != "d")"# ); - let insertions = diff.right_only_keys.absolute_keys_to_vec(None); + let insertions = diff.right_only_keys.get_diffs(); assert_eq!(insertions.len(), 1); - assert_eq!(insertions.first().unwrap().to_string(), r#"[l: 0]-"a""#); + assert_eq!(insertions.first().unwrap().to_string(), r#".[0].("a")"#); } #[test] @@ -496,9 +508,9 @@ mod tests { let data2 = r#"["a","b"]"#; let diff = compare_jsons(data1, data2, false, &[]).unwrap(); - let diffs = diff.left_only_keys.absolute_keys_to_vec(None); + let diffs = diff.left_only_keys.get_diffs(); assert_eq!(diffs.len(), 1); - assert_eq!(diffs.first().unwrap().to_string(), r#"[l: 2]-"c""#); + assert_eq!(diffs.first().unwrap().to_string(), r#".[2].("c")"#); assert_eq!(diff.keys_in_both, KeyNode::Nil); assert_eq!(diff.right_only_keys, KeyNode::Nil); } @@ -509,9 +521,9 @@ mod tests { let data2 = r#"["a","b","c"]"#; let diff = compare_jsons(data1, data2, false, &[]).unwrap(); - let diffs = diff.right_only_keys.absolute_keys_to_vec(None); + let diffs = diff.right_only_keys.get_diffs(); assert_eq!(diffs.len(), 1); - assert_eq!(diffs.first().unwrap().to_string(), r#"[l: 2]-"c""#); + assert_eq!(diffs.first().unwrap().to_string(), r#".[2].("c")"#); assert_eq!(diff.keys_in_both, KeyNode::Nil); assert_eq!(diff.left_only_keys, KeyNode::Nil); } @@ -521,13 +533,16 @@ mod tests { let data1 = r#"["a","b","a"]"#; let data2 = r#"["a","c","c","c","a"]"#; let diff = compare_jsons(data1, data2, false, &[]).unwrap(); - let diffs = diff.keys_in_both.absolute_keys_to_vec(None); + let diffs = diff.keys_in_both.get_diffs(); assert_eq!(diffs.len(), 3); let diffs: Vec<_> = diffs.into_iter().map(|d| d.to_string()).collect(); - assert!(diffs.contains(&r#"[l: 3]->{null!="c"}"#.to_string())); - assert!(diffs.contains(&r#"[l: 1]->{"b"!="c"}"#.to_string())); - assert!(diffs.contains(&r#"[l: 2]->{"a"!="c"}"#.to_string())); + for diff in &diffs { + eprintln!("{diff}"); + } + 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_keys, KeyNode::Nil); assert_eq!(diff.left_only_keys, KeyNode::Nil); } @@ -538,11 +553,11 @@ mod tests { let data2 = r#"["a","b", {"c": {"d": "e"} }]"#; let diff = compare_jsons(data1, data2, false, &[]).unwrap(); - let diffs = diff.right_only_keys.absolute_keys_to_vec(None); + let diffs = diff.right_only_keys.get_diffs(); assert_eq!(diffs.len(), 1); assert_eq!( diffs.first().unwrap().to_string(), - r#"[l: 2]-{"c":{"d":"e"}}"# + r#".[2].({"c":{"d":"e"}})"# ); assert_eq!(diff.keys_in_both, KeyNode::Nil); assert_eq!(diff.left_only_keys, KeyNode::Nil); From f04ba930129d922dd7509071e00cea803069938f Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sat, 25 May 2024 22:58:24 +0200 Subject: [PATCH 35/52] Add error message to structural problems, Refactoring comparison code into more readable functions. --- .vscode/launch.json | 64 +++++++++++++ src/enums.rs | 8 ++ src/process.rs | 227 +++++++++++++++++++++++--------------------- 3 files changed, 192 insertions(+), 107 deletions(-) create mode 100644 .vscode/launch.json 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/src/enums.rs b/src/enums.rs index 6c7f51f..c423078 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -5,12 +5,20 @@ 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), } +impl From for Error { + fn from(value: String) -> Self { + Self::Misc(value) + } +} + #[derive(Debug)] pub enum DiffType { RootMismatch, diff --git a/src/process.rs b/src/process.rs index a6663b4..c5ef18e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; -use diffs::{Diff, myers, Replace}; +use diffs::{myers, Diff, Replace}; use regex::Regex; use serde_json::Map; use serde_json::Value; @@ -19,7 +19,7 @@ pub fn compare_jsons( ) -> Result { let value1 = serde_json::from_str(a)?; let value2 = serde_json::from_str(b)?; - Ok(match_json(&value1, &value2, sort_arrays, ignore_keys)) + match_json(&value1, &value2, sort_arrays, ignore_keys) } fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { if vec.is_empty() { @@ -72,107 +72,120 @@ pub fn match_json( value2: &Value, sort_arrays: bool, ignore_keys: &[Regex], -) -> Mismatch { +) -> Result { match (value1, value2) { - (Value::Object(a), Value::Object(b)) => { - 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 = KeyNode::Nil; - - 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(), - 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); - } + (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), + } +} - Mismatch::new(left_only_keys, right_only_keys, unequal_keys) - } - (Value::Array(a), Value::Array(b)) => { - 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::>() - } +fn process_values(a: &Value, b: &Value) -> Result { + if a == b { + Ok(Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil)) + } else { + Ok(Mismatch::new( + KeyNode::Nil, + KeyNode::Nil, + KeyNode::Value(a.clone(), b.clone()), + )) + } +} - 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 = KeyNode::Nil; - - 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_keys: l, - right_only_keys: r, - keys_in_both: 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); - } - } +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 = KeyNode::Nil; + + 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(), + 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)) +} - Mismatch::new(left_only_nodes, right_only_nodes, diff) - } - (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()), - ) - } +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 = KeyNode::Nil; + + 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_keys: l, + right_only_keys: r, + keys_in_both: 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 preprocess_array<'a>( @@ -267,33 +280,33 @@ fn get_map_of_keys(set: HashSet) -> KeyNode { } } -fn insert_child_key_diff(parent: KeyNode, child: KeyNode, line: usize) -> KeyNode { +fn insert_child_key_diff(parent: KeyNode, child: KeyNode, line: usize) -> Result { if child == KeyNode::Nil { - return parent; + return Ok(parent); } if let KeyNode::Array(mut array) = parent { array.push((line, child)); - KeyNode::Array(array) + Ok(KeyNode::Array(array)) } else if let KeyNode::Nil = parent { - KeyNode::Array(vec![(line, child)]) + Ok(KeyNode::Array(vec![(line, child)])) } 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 array - somehow json structure seems broken").into()) } } -fn insert_child_key_map(parent: KeyNode, child: KeyNode, key: &String) -> KeyNode { +fn insert_child_key_map(parent: KeyNode, child: KeyNode, key: &String) -> Result { if child == KeyNode::Nil { - return parent; + return Ok(parent); } if let KeyNode::Node(mut map) = parent { map.insert(String::from(key), child); - KeyNode::Node(map) + Ok(KeyNode::Node(map)) } else if let KeyNode::Nil = parent { let mut map = HashMap::new(); map.insert(String::from(key), child); - KeyNode::Node(map) + Ok(KeyNode::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()) } } From 8f0becf346fbeb910d5103c5b72e2ecd3ddc42ce Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sat, 25 May 2024 23:07:04 +0200 Subject: [PATCH 36/52] Starting with documentation --- Cargo.toml | 4 ++-- src/ds/mismatch.rs | 2 ++ src/process.rs | 17 +++++++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d953f26..184fc94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json_diff_ng" -version = "0.6.0-RC1" +version = "0.6.0-RC2" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" @@ -29,4 +29,4 @@ serde_json = { version = "1.0", features = ["preserve_order"] } maplit = "1.0" clap = { version = "4.5", features = ["derive"] } diffs = "0.5" -regex = "1.10" \ No newline at end of file +regex = "1.10" diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 92f2ff4..973a151 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -1,6 +1,8 @@ use crate::ds::key_node::KeyNode; use crate::enums::{DiffEntry, DiffType}; +/// 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_keys: KeyNode, diff --git a/src/process.rs b/src/process.rs index c5ef18e..dc22aa4 100644 --- a/src/process.rs +++ b/src/process.rs @@ -11,6 +11,8 @@ use crate::ds::key_node::KeyNode; use crate::ds::mismatch::Mismatch; use crate::enums::Error; +/// 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_values`] after deserializing the string slices into [`serde_json::Value`] pub fn compare_jsons( a: &str, b: &str, @@ -19,8 +21,19 @@ pub fn compare_jsons( ) -> Result { let value1 = serde_json::from_str(a)?; let value2 = serde_json::from_str(b)?; - match_json(&value1, &value2, sort_arrays, ignore_keys) + compare_values(&value1, &value2, sort_arrays, ignore_keys) } + +/// Compares two [`serde_json::Value`] items with each other, returns an error or a [`Mismatch`] structure holding all differences. +pub fn compare_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)>) -> KeyNode { if vec.is_empty() { KeyNode::Nil @@ -67,7 +80,7 @@ impl<'a> Diff for ListDiffHandler<'a> { } } -pub fn match_json( +fn match_json( value1: &Value, value2: &Value, sort_arrays: bool, From 6e8e4512b69c6c5f6dabb1017bebf8017d779e0d Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sun, 26 May 2024 22:27:38 +0200 Subject: [PATCH 37/52] - Add more docs - Add serde_json::Value parsing for PathElement - Refactor variable names --- src/ds/key_node.rs | 18 ++-- src/ds/mismatch.rs | 34 ++++---- src/enums.rs | 20 +++++ src/lib.rs | 30 +++++++ src/main.rs | 4 +- src/process.rs | 212 ++++++++++++++++++++++++--------------------- 6 files changed, 192 insertions(+), 126 deletions(-) diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 41bd766..0c28dc1 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -5,14 +5,14 @@ use serde_json::Value; use crate::enums::{DiffEntry, PathElement}; #[derive(Debug, PartialEq)] -pub enum KeyNode { - Nil, +pub enum TreeNode { + Null, Value(Value, Value), - Node(HashMap), - Array(Vec<(usize, KeyNode)>), + Node(HashMap), + Array(Vec<(usize, TreeNode)>), } -impl KeyNode { +impl TreeNode { pub fn get_diffs(&self) -> Vec { let mut buf = Vec::new(); self.follow_path(&mut buf, &[]); @@ -21,7 +21,7 @@ impl KeyNode { pub fn follow_path(&self, diffs: &mut Vec, offset: &[PathElement]) { match self { - KeyNode::Nil => { + TreeNode::Null => { let is_map_child = offset .last() .map(|o| matches!(o, PathElement::Object(_))) @@ -33,18 +33,18 @@ impl KeyNode { }); } } - KeyNode::Value(l, r) => diffs.push(DiffEntry { + TreeNode::Value(l, r) => diffs.push(DiffEntry { path: offset.to_vec(), values: Some((l.to_string(), r.to_string())), }), - KeyNode::Node(o) => { + TreeNode::Node(o) => { for (k, v) in o { let mut new_offset = offset.to_vec(); new_offset.push(PathElement::Object(k.clone())); v.follow_path(diffs, &new_offset); } } - KeyNode::Array(v) => { + TreeNode::Array(v) => { for (l, k) in v { let mut new_offset = offset.to_vec(); new_offset.push(PathElement::ArrayEntry(*l)); diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 973a151..69c3d0a 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -1,51 +1,51 @@ -use crate::ds::key_node::KeyNode; +use crate::ds::key_node::TreeNode; use crate::enums::{DiffEntry, DiffType}; /// 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_keys: KeyNode, - pub right_only_keys: KeyNode, - pub keys_in_both: KeyNode, + pub left_only: TreeNode, + pub right_only: TreeNode, + pub unequal_values: TreeNode, } impl Mismatch { - pub fn new(l: KeyNode, r: KeyNode, u: KeyNode) -> Mismatch { + pub fn new(l: TreeNode, r: TreeNode, u: TreeNode) -> Mismatch { Mismatch { - left_only_keys: l, - right_only_keys: r, - keys_in_both: u, + left_only: l, + right_only: r, + unequal_values: u, } } pub fn empty() -> Self { Mismatch { - left_only_keys: KeyNode::Nil, - keys_in_both: KeyNode::Nil, - right_only_keys: KeyNode::Nil, + left_only: TreeNode::Null, + unequal_values: TreeNode::Null, + right_only: TreeNode::Null, } } pub fn is_empty(&self) -> bool { - self.left_only_keys == KeyNode::Nil - && self.keys_in_both == KeyNode::Nil - && self.right_only_keys == KeyNode::Nil + self.left_only == TreeNode::Null + && self.unequal_values == TreeNode::Null + && self.right_only == TreeNode::Null } pub fn all_diffs(&self) -> Vec<(DiffType, DiffEntry)> { let both = self - .keys_in_both + .unequal_values .get_diffs() .into_iter() .map(|k| (DiffType::Mismatch, k)); let left = self - .left_only_keys + .left_only .get_diffs() .into_iter() .map(|k| (DiffType::LeftExtra, k)); let right = self - .right_only_keys + .right_only .get_diffs() .into_iter() .map(|k| (DiffType::RightExtra, k)); diff --git a/src/enums.rs b/src/enums.rs index c423078..fbc69d9 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -45,6 +45,26 @@ pub enum PathElement { ArrayEntry(usize), } +impl PathElement { + pub fn resolve<'a>(&self, v: &'a serde_json::Value) -> Option<&'a serde_json::Value> { + match self { + PathElement::Object(o) => v.get(o), + PathElement::ArrayEntry(i) => v.get(*i), + } + } + + pub fn resolve_mut<'a>( + &self, + v: &'a mut serde_json::Value, + ) -> Option<&'a mut serde_json::Value> { + match self { + PathElement::Object(o) => v.get_mut(o), + PathElement::ArrayEntry(i) => v.get_mut(*i), + } + } +} + +/// Represents a single difference in a JSON file #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DiffEntry { pub path: Vec, diff --git a/src/lib.rs b/src/lib.rs index 626d033..b3fa7ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,33 @@ +//! # 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`] +//! ## Examples: +//! ### Compare string slices: +//! ```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")"# +//! ); +//! ``` +//! +//! + pub mod ds; + pub mod enums; pub mod process; +pub use ds::key_node::TreeNode; +pub use enums::DiffEntry; +pub use enums::DiffType; +pub use enums::Error; +pub use process::compare_serde_values; +pub use process::compare_strs; +pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs index e4b39aa..db41d07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use clap::Parser; use clap::Subcommand; -use json_diff::{ds::mismatch::Mismatch, process::compare_jsons}; use json_diff::enums::Error; +use json_diff::{ds::mismatch::Mismatch, process::compare_strs}; #[derive(Subcommand, Clone)] /// Input selection @@ -40,7 +40,7 @@ fn main() -> Result<(), Error> { } }; - let mismatch = compare_jsons(&json_1, &json_2, args.sort_arrays, &[])?; + let mismatch = compare_strs(&json_1, &json_2, args.sort_arrays, &[])?; let comparison_result = check_diffs(mismatch)?; if !comparison_result { diff --git a/src/process.rs b/src/process.rs index dc22aa4..0569c55 100644 --- a/src/process.rs +++ b/src/process.rs @@ -7,13 +7,15 @@ use regex::Regex; use serde_json::Map; use serde_json::Value; -use crate::ds::key_node::KeyNode; +use crate::ds::key_node::TreeNode; use crate::ds::mismatch::Mismatch; use crate::enums::Error; /// 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_values`] after deserializing the string slices into [`serde_json::Value`] -pub fn compare_jsons( +/// Internally this calls into [`compare_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, @@ -21,11 +23,13 @@ pub fn compare_jsons( ) -> Result { let value1 = serde_json::from_str(a)?; let value2 = serde_json::from_str(b)?; - compare_values(&value1, &value2, sort_arrays, ignore_keys) + compare_serde_values(&value1, &value2, sort_arrays, ignore_keys) } /// Compares two [`serde_json::Value`] items with each other, returns an error or a [`Mismatch`] structure holding all differences. -pub fn compare_values( +/// 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, @@ -34,13 +38,13 @@ pub fn compare_values( match_json(a, b, sort_arrays, ignore_keys) } -fn values_to_node(vec: Vec<(usize, &Value)>) -> KeyNode { +fn values_to_node(vec: Vec<(usize, &Value)>) -> TreeNode { if vec.is_empty() { - KeyNode::Nil + TreeNode::Null } else { - KeyNode::Array( + TreeNode::Array( vec.into_iter() - .map(|(l, v)| (l, KeyNode::Value(v.clone(), v.clone()))) + .map(|(l, v)| (l, TreeNode::Value(v.clone(), v.clone()))) .collect(), ) } @@ -95,12 +99,16 @@ fn match_json( fn process_values(a: &Value, b: &Value) -> Result { if a == b { - Ok(Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil)) + Ok(Mismatch::new( + TreeNode::Null, + TreeNode::Null, + TreeNode::Null, + )) } else { Ok(Mismatch::new( - KeyNode::Nil, - KeyNode::Nil, - KeyNode::Value(a.clone(), b.clone()), + TreeNode::Null, + TreeNode::Null, + TreeNode::Value(a.clone(), b.clone()), )) } } @@ -116,13 +124,13 @@ fn process_objects( let mut right_only_keys = get_map_of_keys(diff.right_only); let intersection_keys = diff.intersection; - let mut unequal_keys = KeyNode::Nil; + let mut unequal_keys = TreeNode::Null; for key in intersection_keys { let Mismatch { - left_only_keys: l, - right_only_keys: r, - keys_in_both: u, + left_only: l, + right_only: r, + unequal_values: u, } = match_json( a.get(&key).unwrap(), b.get(&key).unwrap(), @@ -177,7 +185,7 @@ fn process_arrays( 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 = KeyNode::Nil; + let mut diff = TreeNode::Null; for (o, ol, n, nl) in replaced { let max_length = ol.max(nl); @@ -188,9 +196,9 @@ fn process_arrays( let cdiff = match_json(inner_a, inner_b, sort_arrays, ignore_keys)?; let position = o + i; let Mismatch { - left_only_keys: l, - right_only_keys: r, - keys_in_both: u, + 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)?; @@ -281,43 +289,51 @@ fn compare_values(a: &Value, b: &Value, ignore_keys: &[Regex]) -> std::cmp::Orde } } -fn get_map_of_keys(set: HashSet) -> KeyNode { +fn get_map_of_keys(set: HashSet) -> TreeNode { if !set.is_empty() { - KeyNode::Node( + TreeNode::Node( set.iter() - .map(|key| (String::from(key), KeyNode::Nil)) + .map(|key| (String::from(key), TreeNode::Null)) .collect(), ) } else { - KeyNode::Nil + TreeNode::Null } } -fn insert_child_key_diff(parent: KeyNode, child: KeyNode, line: usize) -> Result { - if child == KeyNode::Nil { +fn insert_child_key_diff( + parent: TreeNode, + child: TreeNode, + line: usize, +) -> Result { + if child == TreeNode::Null { return Ok(parent); } - if let KeyNode::Array(mut array) = parent { + if let TreeNode::Array(mut array) = parent { array.push((line, child)); - Ok(KeyNode::Array(array)) - } else if let KeyNode::Nil = parent { - Ok(KeyNode::Array(vec![(line, child)])) + Ok(TreeNode::Array(array)) + } else if let TreeNode::Null = parent { + Ok(TreeNode::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) -> Result { - if child == KeyNode::Nil { +fn insert_child_key_map( + parent: TreeNode, + child: TreeNode, + key: &String, +) -> Result { + if child == TreeNode::Null { return Ok(parent); } - if let KeyNode::Node(mut map) = parent { + if let TreeNode::Node(mut map) = parent { map.insert(String::from(key), child); - Ok(KeyNode::Node(map)) - } else if let KeyNode::Nil = parent { + Ok(TreeNode::Node(map)) + } else if let TreeNode::Null = parent { let mut map = HashMap::new(); map.insert(String::from(key), child); - Ok(KeyNode::Node(map)) + Ok(TreeNode::Node(map)) } else { Err(format!("Tried to insert child: {child:?} into parent {parent:?} - structure incoherent, expected a parent object - somehow json structure seems broken").into()) } @@ -420,7 +436,7 @@ mod tests { 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_jsons(data1, data2, true, &[ignore]).unwrap(); + let diff = compare_strs(data1, data2, true, &[ignore]).unwrap(); assert!(diff.is_empty()); } @@ -428,7 +444,7 @@ mod tests { fn test_arrays_sorted_simple() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["b","c","a"]"#; - let diff = compare_jsons(data1, data2, true, &[]).unwrap(); + let diff = compare_strs(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -436,7 +452,7 @@ mod tests { fn test_arrays_sorted_objects() { let data1 = r#"[{"c": {"d": "e"} },"b","c"]"#; let data2 = r#"["b","c",{"c": {"d": "e"} }]"#; - let diff = compare_jsons(data1, data2, true, &[]).unwrap(); + let diff = compare_strs(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -444,7 +460,7 @@ mod tests { 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_jsons(data1, data2, true, &[]).unwrap(); + let diff = compare_strs(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -452,7 +468,7 @@ mod tests { 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_jsons(data1, data2, true, &[]).unwrap(); + let diff = compare_strs(data1, data2, true, &[]).unwrap(); assert!(diff.is_empty()); } @@ -460,9 +476,9 @@ mod tests { 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_jsons(data1, data2, true, &[]).unwrap(); + let diff = compare_strs(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); - let insertions = diff.right_only_keys.get_diffs(); + let insertions = diff.right_only.get_diffs(); assert_eq!(insertions.len(), 1); assert_eq!(insertions.first().unwrap().to_string(), r#".[2].("c")"#); } @@ -471,9 +487,9 @@ mod tests { 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_jsons(data1, data2, true, &[]).unwrap(); + let diff = compare_strs(data1, data2, true, &[]).unwrap(); assert!(!diff.is_empty()); - let deletions = diff.left_only_keys.get_diffs(); + let deletions = diff.left_only.get_diffs(); assert_eq!(deletions.len(), 1); assert_eq!( @@ -486,9 +502,9 @@ mod tests { 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 diff = compare_jsons(data1, data2, true, &[]).unwrap(); - assert!(!diff.is_empty()); - let diffs = diff.keys_in_both.get_diffs(); + 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!( @@ -501,10 +517,10 @@ mod tests { fn test_arrays_simple_diff() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","b","d"]"#; - let diff = compare_jsons(data1, data2, false, &[]).unwrap(); - assert_eq!(diff.left_only_keys, KeyNode::Nil); - assert_eq!(diff.right_only_keys, KeyNode::Nil); - let diff = diff.keys_in_both.get_diffs(); + let diff = compare_strs(data1, data2, false, &[]).unwrap(); + assert_eq!(diff.left_only, TreeNode::Null); + assert_eq!(diff.right_only, TreeNode::Null); + let diff = diff.unequal_values.get_diffs(); assert_eq!(diff.len(), 1); assert_eq!(diff.first().unwrap().to_string(), r#".[2].("c" != "d")"#); } @@ -513,17 +529,17 @@ mod tests { fn test_arrays_more_complex_diff() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","a","b","d"]"#; - let diff = compare_jsons(data1, data2, false, &[]).unwrap(); + let diff = compare_strs(data1, data2, false, &[]).unwrap(); - let changes_diff = diff.keys_in_both.get_diffs(); - assert_eq!(diff.left_only_keys, KeyNode::Nil); + let changes_diff = diff.unequal_values.get_diffs(); + assert_eq!(diff.left_only, TreeNode::Null); assert_eq!(changes_diff.len(), 1); assert_eq!( changes_diff.first().unwrap().to_string(), r#".[2].("c" != "d")"# ); - let insertions = diff.right_only_keys.get_diffs(); + let insertions = diff.right_only.get_diffs(); assert_eq!(insertions.len(), 1); assert_eq!(insertions.first().unwrap().to_string(), r#".[0].("a")"#); } @@ -532,34 +548,34 @@ mod tests { fn test_arrays_extra_left() { let data1 = r#"["a","b","c"]"#; let data2 = r#"["a","b"]"#; - let diff = compare_jsons(data1, data2, false, &[]).unwrap(); + let diff = compare_strs(data1, data2, false, &[]).unwrap(); - let diffs = diff.left_only_keys.get_diffs(); + 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.keys_in_both, KeyNode::Nil); - assert_eq!(diff.right_only_keys, KeyNode::Nil); + assert_eq!(diff.unequal_values, TreeNode::Null); + assert_eq!(diff.right_only, TreeNode::Null); } #[test] fn test_arrays_extra_right() { let data1 = r#"["a","b"]"#; let data2 = r#"["a","b","c"]"#; - let diff = compare_jsons(data1, data2, false, &[]).unwrap(); + let diff = compare_strs(data1, data2, false, &[]).unwrap(); - let diffs = diff.right_only_keys.get_diffs(); + 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.keys_in_both, KeyNode::Nil); - assert_eq!(diff.left_only_keys, KeyNode::Nil); + assert_eq!(diff.unequal_values, TreeNode::Null); + assert_eq!(diff.left_only, TreeNode::Null); } #[test] fn long_insertion_modification() { let data1 = r#"["a","b","a"]"#; let data2 = r#"["a","c","c","c","a"]"#; - let diff = compare_jsons(data1, data2, false, &[]).unwrap(); - let diffs = diff.keys_in_both.get_diffs(); + 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(); @@ -569,24 +585,24 @@ mod tests { 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_keys, KeyNode::Nil); - assert_eq!(diff.left_only_keys, KeyNode::Nil); + assert_eq!(diff.right_only, TreeNode::Null); + assert_eq!(diff.left_only, TreeNode::Null); } #[test] fn test_arrays_object_extra() { let data1 = r#"["a","b"]"#; let data2 = r#"["a","b", {"c": {"d": "e"} }]"#; - let diff = compare_jsons(data1, data2, false, &[]).unwrap(); + let diff = compare_strs(data1, data2, false, &[]).unwrap(); - let diffs = diff.right_only_keys.get_diffs(); + 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.keys_in_both, KeyNode::Nil); - assert_eq!(diff.left_only_keys, KeyNode::Nil); + assert_eq!(diff.unequal_values, TreeNode::Null); + assert_eq!(diff.left_only, TreeNode::Null); } #[test] @@ -620,24 +636,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 = TreeNode::Node(hashmap! { + "b".to_string() => TreeNode::Node(hashmap! { + "c".to_string() => TreeNode::Node(hashmap! { + "f".to_string() => TreeNode::Null, + "h".to_string() => TreeNode::Node( hashmap! { + "j".to_string() => TreeNode::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 = TreeNode::Node(hashmap! { + "b".to_string() => TreeNode::Node(hashmap! { + "c".to_string() => TreeNode::Node(hashmap! { + "g".to_string() => TreeNode::Null, + "h".to_string() => TreeNode::Node(hashmap! { + "k".to_string() => TreeNode::Null, } ) } @@ -645,12 +661,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 = TreeNode::Node(hashmap! { + "b".to_string() => TreeNode::Node(hashmap! { + "c".to_string() => TreeNode::Node(hashmap! { + "e".to_string() => TreeNode::Value(json!(5), json!(6)), + "h".to_string() => TreeNode::Node(hashmap! { + "i".to_string() => TreeNode::Value(json!(true), json!(false)), } ) } @@ -660,7 +676,7 @@ mod tests { }); let expected = Mismatch::new(expected_left, expected_right, expected_uneq); - let mismatch = compare_jsons(data1, data2, false, &[]).unwrap(); + let mismatch = compare_strs(data1, data2, false, &[]).unwrap(); assert_eq!(mismatch, expected, "Diff was incorrect."); } @@ -696,8 +712,8 @@ mod tests { }"#; assert_eq!( - compare_jsons(data1, data2, false, &[]).unwrap(), - Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil) + compare_strs(data1, data2, false, &[]).unwrap(), + Mismatch::new(TreeNode::Null, TreeNode::Null, TreeNode::Null) ); } @@ -707,8 +723,8 @@ mod tests { let data2 = r#"{}"#; assert_eq!( - compare_jsons(data1, data2, false, &[]).unwrap(), - Mismatch::new(KeyNode::Nil, KeyNode::Nil, KeyNode::Nil) + compare_strs(data1, data2, false, &[]).unwrap(), + Mismatch::new(TreeNode::Null, TreeNode::Null, TreeNode::Null) ); } @@ -716,7 +732,7 @@ 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, false, &[]) { + match compare_strs(invalid_json1, valid_json2, false, &[]) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { matches!(err, Error::JSON(_)); @@ -728,7 +744,7 @@ mod tests { fn parse_err_source_two() { let valid_json1 = r#"{"a":"b"}"#; let invalid_json2 = r#"{invalid: json}"#; - match compare_jsons(valid_json1, invalid_json2, false, &[]) { + match compare_strs(valid_json1, invalid_json2, false, &[]) { Ok(_) => panic!("This shouldn't be an Ok"), Err(err) => { matches!(err, Error::JSON(_)); From a5f39dd9592dc2ad95d24b506dfe7ca63571d9a9 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sun, 26 May 2024 23:52:29 +0200 Subject: [PATCH 38/52] - Make the diff enums drop useless allocations - Change the API to hand throught serde_jsons Values - Add better README.md - Bump version - Add better docs --- Cargo.toml | 8 +-- README.md | 41 ++++++++++++--- src/ds/key_node.rs | 28 +++++----- src/ds/mismatch.rs | 22 ++++---- src/enums.rs | 26 ++++----- src/lib.rs | 12 +++-- src/process.rs | 128 ++++++++++++++++++++++----------------------- 7 files changed, 152 insertions(+), 113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 184fc94..c478c32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "json_diff_ng" -version = "0.6.0-RC2" +version = "0.6.0-RC3" authors = ["ksceriath", "ChrisRega"] edition = "2021" license = "Unlicense" -description = "A small diff tool utility for comparing jsons. Forked from ksceriath and improved for usage as a library and with proper support for array diffs." +description = "A JSON diff library and CLI." readme = "README.md" homepage = "https://github.com/ChrisRega/json-diff" repository = "https://github.com/ChrisRega/json-diff" @@ -12,12 +12,12 @@ keywords = ["cli", "diff", "json"] categories = ["command-line-utilities"] [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" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 3b895d5..cfa8c39 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,38 @@ -# json-diff +# json-diff-ng +[![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") +[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) + +## Contributors: + + + Contributors + + +## Library +json_diff_ng can be used to get diffs of json-serializable structures in rust. + +### Usage example +```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")"# +); +``` + +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. @@ -17,9 +50,5 @@ file : read input from json files direct : read input from command line ### Installation +`$ cargo install json_diff_ng` -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` diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs index 0c28dc1..62180ee 100644 --- a/src/ds/key_node.rs +++ b/src/ds/key_node.rs @@ -5,23 +5,27 @@ use serde_json::Value; use crate::enums::{DiffEntry, PathElement}; #[derive(Debug, PartialEq)] -pub enum TreeNode { +pub enum DiffTreeNode { Null, Value(Value, Value), - Node(HashMap), - Array(Vec<(usize, TreeNode)>), + Node(HashMap), + Array(Vec<(usize, DiffTreeNode)>), } -impl TreeNode { - pub fn get_diffs(&self) -> Vec { +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(&self, diffs: &mut Vec, offset: &[PathElement]) { + pub fn follow_path<'b>( + &'a self, + diffs: &mut Vec>, + offset: &'b [PathElement<'a>], + ) { match self { - TreeNode::Null => { + DiffTreeNode::Null => { let is_map_child = offset .last() .map(|o| matches!(o, PathElement::Object(_))) @@ -33,18 +37,18 @@ impl TreeNode { }); } } - TreeNode::Value(l, r) => diffs.push(DiffEntry { + DiffTreeNode::Value(l, r) => diffs.push(DiffEntry { path: offset.to_vec(), - values: Some((l.to_string(), r.to_string())), + values: Some((l, r)), }), - TreeNode::Node(o) => { + DiffTreeNode::Node(o) => { for (k, v) in o { let mut new_offset = offset.to_vec(); - new_offset.push(PathElement::Object(k.clone())); + new_offset.push(PathElement::Object(k)); v.follow_path(diffs, &new_offset); } } - TreeNode::Array(v) => { + DiffTreeNode::Array(v) => { for (l, k) in v { let mut new_offset = offset.to_vec(); new_offset.push(PathElement::ArrayEntry(*l)); diff --git a/src/ds/mismatch.rs b/src/ds/mismatch.rs index 69c3d0a..ade6ff2 100644 --- a/src/ds/mismatch.rs +++ b/src/ds/mismatch.rs @@ -1,17 +1,17 @@ -use crate::ds::key_node::TreeNode; +use crate::ds::key_node::DiffTreeNode; use crate::enums::{DiffEntry, DiffType}; /// 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: TreeNode, - pub right_only: TreeNode, - pub unequal_values: TreeNode, + pub left_only: DiffTreeNode, + pub right_only: DiffTreeNode, + pub unequal_values: DiffTreeNode, } impl Mismatch { - pub fn new(l: TreeNode, r: TreeNode, u: TreeNode) -> Mismatch { + pub fn new(l: DiffTreeNode, r: DiffTreeNode, u: DiffTreeNode) -> Mismatch { Mismatch { left_only: l, right_only: r, @@ -21,16 +21,16 @@ impl Mismatch { pub fn empty() -> Self { Mismatch { - left_only: TreeNode::Null, - unequal_values: TreeNode::Null, - right_only: TreeNode::Null, + left_only: DiffTreeNode::Null, + unequal_values: DiffTreeNode::Null, + right_only: DiffTreeNode::Null, } } pub fn is_empty(&self) -> bool { - self.left_only == TreeNode::Null - && self.unequal_values == TreeNode::Null - && self.right_only == TreeNode::Null + self.left_only == DiffTreeNode::Null + && self.unequal_values == DiffTreeNode::Null + && self.right_only == DiffTreeNode::Null } pub fn all_diffs(&self) -> Vec<(DiffType, DiffEntry)> { diff --git a/src/enums.rs b/src/enums.rs index fbc69d9..5d5235d 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -40,23 +40,23 @@ impl Display for DiffType { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum PathElement { - Object(String), +pub enum PathElement<'a> { + Object(&'a str), ArrayEntry(usize), } -impl PathElement { - pub fn resolve<'a>(&self, v: &'a serde_json::Value) -> Option<&'a serde_json::Value> { +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<'a>( + pub fn resolve_mut<'b>( &self, - v: &'a mut serde_json::Value, - ) -> Option<&'a mut serde_json::Value> { + 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), @@ -64,14 +64,14 @@ impl PathElement { } } -/// Represents a single difference in a JSON file +/// A view on a single end-node of the [`DiffKeyNode`] tree. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct DiffEntry { - pub path: Vec, - pub values: Option<(String, String)>, +pub struct DiffEntry<'a> { + pub path: Vec>, + pub values: Option<(&'a serde_json::Value, &'a serde_json::Value)>, } -impl Display for DiffEntry { +impl Display for DiffEntry<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for element in &self.path { write!(f, ".{element}")?; @@ -87,7 +87,7 @@ impl Display for DiffEntry { } } -impl Display for PathElement { +impl Display for PathElement<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { PathElement::Object(o) => { diff --git a/src/lib.rs b/src/lib.rs index b3fa7ee..f8e78e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,7 @@ //! # 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`] -//! ## Examples: -//! ### Compare string slices: +//! ## Example: //! ```rust //! use json_diff::compare_strs; //! let data1 = r#"["a",{"c": ["d","f"] },"b"]"#; @@ -17,6 +16,12 @@ //! 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`]. //! //! @@ -24,7 +29,8 @@ pub mod ds; pub mod enums; pub mod process; -pub use ds::key_node::TreeNode; +pub use ds::key_node::DiffTreeNode; +pub use ds::mismatch::Mismatch; pub use enums::DiffEntry; pub use enums::DiffType; pub use enums::Error; diff --git a/src/process.rs b/src/process.rs index 0569c55..0cab40d 100644 --- a/src/process.rs +++ b/src/process.rs @@ -7,7 +7,7 @@ use regex::Regex; use serde_json::Map; use serde_json::Value; -use crate::ds::key_node::TreeNode; +use crate::ds::key_node::DiffTreeNode; use crate::ds::mismatch::Mismatch; use crate::enums::Error; @@ -38,13 +38,13 @@ pub fn compare_serde_values( match_json(a, b, sort_arrays, ignore_keys) } -fn values_to_node(vec: Vec<(usize, &Value)>) -> TreeNode { +fn values_to_node(vec: Vec<(usize, &Value)>) -> DiffTreeNode { if vec.is_empty() { - TreeNode::Null + DiffTreeNode::Null } else { - TreeNode::Array( + DiffTreeNode::Array( vec.into_iter() - .map(|(l, v)| (l, TreeNode::Value(v.clone(), v.clone()))) + .map(|(l, v)| (l, DiffTreeNode::Value(v.clone(), v.clone()))) .collect(), ) } @@ -100,15 +100,15 @@ fn match_json( fn process_values(a: &Value, b: &Value) -> Result { if a == b { Ok(Mismatch::new( - TreeNode::Null, - TreeNode::Null, - TreeNode::Null, + DiffTreeNode::Null, + DiffTreeNode::Null, + DiffTreeNode::Null, )) } else { Ok(Mismatch::new( - TreeNode::Null, - TreeNode::Null, - TreeNode::Value(a.clone(), b.clone()), + DiffTreeNode::Null, + DiffTreeNode::Null, + DiffTreeNode::Value(a.clone(), b.clone()), )) } } @@ -124,7 +124,7 @@ fn process_objects( let mut right_only_keys = get_map_of_keys(diff.right_only); let intersection_keys = diff.intersection; - let mut unequal_keys = TreeNode::Null; + let mut unequal_keys = DiffTreeNode::Null; for key in intersection_keys { let Mismatch { @@ -185,7 +185,7 @@ fn process_arrays( 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 = TreeNode::Null; + let mut diff = DiffTreeNode::Null; for (o, ol, n, nl) in replaced { let max_length = ol.max(nl); @@ -289,51 +289,51 @@ fn compare_values(a: &Value, b: &Value, ignore_keys: &[Regex]) -> std::cmp::Orde } } -fn get_map_of_keys(set: HashSet) -> TreeNode { +fn get_map_of_keys(set: HashSet) -> DiffTreeNode { if !set.is_empty() { - TreeNode::Node( + DiffTreeNode::Node( set.iter() - .map(|key| (String::from(key), TreeNode::Null)) + .map(|key| (String::from(key), DiffTreeNode::Null)) .collect(), ) } else { - TreeNode::Null + DiffTreeNode::Null } } fn insert_child_key_diff( - parent: TreeNode, - child: TreeNode, + parent: DiffTreeNode, + child: DiffTreeNode, line: usize, -) -> Result { - if child == TreeNode::Null { +) -> Result { + if child == DiffTreeNode::Null { return Ok(parent); } - if let TreeNode::Array(mut array) = parent { + if let DiffTreeNode::Array(mut array) = parent { array.push((line, child)); - Ok(TreeNode::Array(array)) - } else if let TreeNode::Null = parent { - Ok(TreeNode::Array(vec![(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: TreeNode, - child: TreeNode, + parent: DiffTreeNode, + child: DiffTreeNode, key: &String, -) -> Result { - if child == TreeNode::Null { +) -> Result { + if child == DiffTreeNode::Null { return Ok(parent); } - if let TreeNode::Node(mut map) = parent { + if let DiffTreeNode::Node(mut map) = parent { map.insert(String::from(key), child); - Ok(TreeNode::Node(map)) - } else if let TreeNode::Null = parent { + Ok(DiffTreeNode::Node(map)) + } else if let DiffTreeNode::Null = parent { let mut map = HashMap::new(); map.insert(String::from(key), child); - Ok(TreeNode::Node(map)) + Ok(DiffTreeNode::Node(map)) } else { Err(format!("Tried to insert child: {child:?} into parent {parent:?} - structure incoherent, expected a parent object - somehow json structure seems broken").into()) } @@ -518,8 +518,8 @@ mod tests { 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, TreeNode::Null); - assert_eq!(diff.right_only, TreeNode::Null); + 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")"#); @@ -532,7 +532,7 @@ mod tests { let diff = compare_strs(data1, data2, false, &[]).unwrap(); let changes_diff = diff.unequal_values.get_diffs(); - assert_eq!(diff.left_only, TreeNode::Null); + assert_eq!(diff.left_only, DiffTreeNode::Null); assert_eq!(changes_diff.len(), 1); assert_eq!( @@ -553,8 +553,8 @@ mod tests { 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, TreeNode::Null); - assert_eq!(diff.right_only, TreeNode::Null); + assert_eq!(diff.unequal_values, DiffTreeNode::Null); + assert_eq!(diff.right_only, DiffTreeNode::Null); } #[test] @@ -566,8 +566,8 @@ mod tests { 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, TreeNode::Null); - assert_eq!(diff.left_only, TreeNode::Null); + assert_eq!(diff.unequal_values, DiffTreeNode::Null); + assert_eq!(diff.left_only, DiffTreeNode::Null); } #[test] @@ -585,8 +585,8 @@ mod tests { 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, TreeNode::Null); - assert_eq!(diff.left_only, TreeNode::Null); + assert_eq!(diff.right_only, DiffTreeNode::Null); + assert_eq!(diff.left_only, DiffTreeNode::Null); } #[test] @@ -601,8 +601,8 @@ mod tests { diffs.first().unwrap().to_string(), r#".[2].({"c":{"d":"e"}})"# ); - assert_eq!(diff.unequal_values, TreeNode::Null); - assert_eq!(diff.left_only, TreeNode::Null); + assert_eq!(diff.unequal_values, DiffTreeNode::Null); + assert_eq!(diff.left_only, DiffTreeNode::Null); } #[test] @@ -636,24 +636,24 @@ mod tests { } }"#; - let expected_left = TreeNode::Node(hashmap! { - "b".to_string() => TreeNode::Node(hashmap! { - "c".to_string() => TreeNode::Node(hashmap! { - "f".to_string() => TreeNode::Null, - "h".to_string() => TreeNode::Node( hashmap! { - "j".to_string() => TreeNode::Null, + 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 = TreeNode::Node(hashmap! { - "b".to_string() => TreeNode::Node(hashmap! { - "c".to_string() => TreeNode::Node(hashmap! { - "g".to_string() => TreeNode::Null, - "h".to_string() => TreeNode::Node(hashmap! { - "k".to_string() => TreeNode::Null, + 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, } ) } @@ -661,12 +661,12 @@ mod tests { } ) }); - let expected_uneq = TreeNode::Node(hashmap! { - "b".to_string() => TreeNode::Node(hashmap! { - "c".to_string() => TreeNode::Node(hashmap! { - "e".to_string() => TreeNode::Value(json!(5), json!(6)), - "h".to_string() => TreeNode::Node(hashmap! { - "i".to_string() => TreeNode::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)), } ) } @@ -713,7 +713,7 @@ mod tests { assert_eq!( compare_strs(data1, data2, false, &[]).unwrap(), - Mismatch::new(TreeNode::Null, TreeNode::Null, TreeNode::Null) + Mismatch::new(DiffTreeNode::Null, DiffTreeNode::Null, DiffTreeNode::Null) ); } @@ -724,7 +724,7 @@ mod tests { assert_eq!( compare_strs(data1, data2, false, &[]).unwrap(), - Mismatch::new(TreeNode::Null, TreeNode::Null, TreeNode::Null) + Mismatch::new(DiffTreeNode::Null, DiffTreeNode::Null, DiffTreeNode::Null) ); } From a205441f1627a0abf8796e7e360a6adf47af1d2c Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Sun, 26 May 2024 23:53:24 +0200 Subject: [PATCH 39/52] Fix unit tests --- src/lib.rs | 2 +- src/main.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f8e78e1..ee90f3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ //! 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::compare_strs; +//! 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(); diff --git a/src/main.rs b/src/main.rs index db41d07..cde916f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use clap::Parser; use clap::Subcommand; -use json_diff::enums::Error; -use json_diff::{ds::mismatch::Mismatch, process::compare_strs}; +use json_diff_ng::enums::Error; +use json_diff_ng::{ds::mismatch::Mismatch, process::compare_strs}; #[derive(Subcommand, Clone)] /// Input selection From 3e153bb56476963bf43c0ea915427202b95c5f00 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Mon, 27 May 2024 00:00:13 +0200 Subject: [PATCH 40/52] - Refactor old ds dir away - Rename files to be more consistent --- src/ds/key_node.rs | 60 ---------------------------- src/ds/mod.rs | 2 - src/enums.rs | 59 +++++++++++++++++++++++++++ src/lib.rs | 8 ++-- src/main.rs | 7 ++-- src/{ds => }/mismatch.rs | 2 +- src/process.rs | 86 ++-------------------------------------- src/sort.rs | 82 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 152 insertions(+), 154 deletions(-) delete mode 100644 src/ds/key_node.rs delete mode 100644 src/ds/mod.rs rename src/{ds => }/mismatch.rs (97%) create mode 100644 src/sort.rs diff --git a/src/ds/key_node.rs b/src/ds/key_node.rs deleted file mode 100644 index 62180ee..0000000 --- a/src/ds/key_node.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::HashMap; - -use serde_json::Value; - -use crate::enums::{DiffEntry, PathElement}; - -#[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); - } - } - } - } -} 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 index 5d5235d..31420ca 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -19,6 +19,65 @@ impl From for Error { } } +use std::collections::HashMap; + +use serde_json::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, diff --git a/src/lib.rs b/src/lib.rs index ee90f3e..84db57d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,15 +25,15 @@ //! //! -pub mod ds; - pub mod enums; +pub mod mismatch; pub mod process; -pub use ds::key_node::DiffTreeNode; -pub use ds::mismatch::Mismatch; +pub mod sort; pub use enums::DiffEntry; +pub use enums::DiffTreeNode; pub use enums::DiffType; pub use enums::Error; +pub use mismatch::Mismatch; pub use process::compare_serde_values; pub use process::compare_strs; pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs index cde916f..d5b4d14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ use clap::Parser; use clap::Subcommand; -use json_diff_ng::enums::Error; -use json_diff_ng::{ds::mismatch::Mismatch, process::compare_strs}; +use json_diff_ng::{compare_strs, Mismatch, Result}; #[derive(Subcommand, Clone)] /// Input selection @@ -29,7 +28,7 @@ struct Args { truncation_length: usize, } -fn main() -> Result<(), Error> { +fn main() -> Result<()> { let args = Args::parse(); let (json_1, json_2) = match args.cmd { Mode::Direct { json_2, json_1 } => (json_1, json_2), @@ -49,7 +48,7 @@ fn main() -> Result<(), Error> { Ok(()) } -pub fn check_diffs(result: Mismatch) -> Result { +pub fn check_diffs(result: Mismatch) -> Result { let mismatches = result.all_diffs(); let is_good = mismatches.is_empty(); for (d_type, key) in mismatches { diff --git a/src/ds/mismatch.rs b/src/mismatch.rs similarity index 97% rename from src/ds/mismatch.rs rename to src/mismatch.rs index ade6ff2..8d1da21 100644 --- a/src/ds/mismatch.rs +++ b/src/mismatch.rs @@ -1,5 +1,5 @@ -use crate::ds::key_node::DiffTreeNode; 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. diff --git a/src/process.rs b/src/process.rs index 0cab40d..44529c2 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; @@ -7,9 +6,10 @@ use regex::Regex; use serde_json::Map; use serde_json::Value; -use crate::ds::key_node::DiffTreeNode; -use crate::ds::mismatch::Mismatch; use crate::enums::Error; +use crate::sort::preprocess_array; +use crate::DiffTreeNode; +use crate::Mismatch; /// 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_values`] after deserializing the string slices into [`serde_json::Value`]. @@ -209,86 +209,6 @@ fn process_arrays( Ok(Mismatch::new(left_only_nodes, right_only_nodes, diff)) } -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, - } -} - fn get_map_of_keys(set: HashSet) -> DiffTreeNode { if !set.is_empty() { DiffTreeNode::Node( diff --git a/src/sort.rs b/src/sort.rs new file mode 100644 index 0000000..f4c4b51 --- /dev/null +++ b/src/sort.rs @@ -0,0 +1,82 @@ +use regex::Regex; +use serde_json::Value; +use std::borrow::Cow; + +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, + } +} From 7fb67bf2d6722c4cfb516908bcfe797a78915fe2 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Mon, 27 May 2024 00:06:38 +0200 Subject: [PATCH 41/52] - Minor cleanups --- src/process.rs | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/process.rs b/src/process.rs index 44529c2..9c0fc00 100644 --- a/src/process.rs +++ b/src/process.rs @@ -6,10 +6,10 @@ use regex::Regex; use serde_json::Map; use serde_json::Value; -use crate::enums::Error; use crate::sort::preprocess_array; use crate::DiffTreeNode; use crate::Mismatch; +use crate::Result; /// 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_values`] after deserializing the string slices into [`serde_json::Value`]. @@ -20,7 +20,7 @@ pub fn compare_strs( b: &str, sort_arrays: bool, ignore_keys: &[Regex], -) -> Result { +) -> Result { let value1 = serde_json::from_str(a)?; let value2 = serde_json::from_str(b)?; compare_serde_values(&value1, &value2, sort_arrays, ignore_keys) @@ -34,7 +34,7 @@ pub fn compare_serde_values( b: &Value, sort_arrays: bool, ignore_keys: &[Regex], -) -> Result { +) -> Result { match_json(a, b, sort_arrays, ignore_keys) } @@ -70,15 +70,21 @@ impl<'a> ListDiffHandler<'a> { } impl<'a> Diff for ListDiffHandler<'a> { type Error = (); - fn delete(&mut self, old: usize, len: usize, _new: usize) -> Result<(), ()> { + 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) -> Result<(), ()> { + 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) -> Result<(), ()> { + 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(()) } @@ -89,7 +95,7 @@ fn match_json( value2: &Value, sort_arrays: bool, ignore_keys: &[Regex], -) -> Result { +) -> 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), @@ -97,7 +103,7 @@ fn match_json( } } -fn process_values(a: &Value, b: &Value) -> Result { +fn process_values(a: &Value, b: &Value) -> Result { if a == b { Ok(Mismatch::new( DiffTreeNode::Null, @@ -118,7 +124,7 @@ fn process_objects( b: &Map, ignore_keys: &[Regex], sort_arrays: bool, -) -> Result { +) -> 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); @@ -150,7 +156,7 @@ fn process_arrays( a: &Vec, ignore_keys: &[Regex], b: &Vec, -) -> Result { +) -> Result { let a = preprocess_array(sort_arrays, a, ignore_keys); let b = preprocess_array(sort_arrays, b, ignore_keys); @@ -225,7 +231,7 @@ fn insert_child_key_diff( parent: DiffTreeNode, child: DiffTreeNode, line: usize, -) -> Result { +) -> Result { if child == DiffTreeNode::Null { return Ok(parent); } @@ -243,7 +249,7 @@ fn insert_child_key_map( parent: DiffTreeNode, child: DiffTreeNode, key: &String, -) -> Result { +) -> Result { if child == DiffTreeNode::Null { return Ok(parent); } @@ -644,7 +650,7 @@ mod tests { assert_eq!( compare_strs(data1, data2, false, &[]).unwrap(), - Mismatch::new(DiffTreeNode::Null, DiffTreeNode::Null, DiffTreeNode::Null) + Mismatch::empty() ); } @@ -652,23 +658,15 @@ mod tests { fn parse_err_source_one() { let invalid_json1 = r#"{invalid: json}"#; let valid_json2 = r#"{"a":"b"}"#; - match compare_strs(invalid_json1, valid_json2, false, &[]) { - Ok(_) => panic!("This shouldn't be an Ok"), - Err(err) => { - matches!(err, Error::JSON(_)); - } - }; + 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_strs(valid_json1, invalid_json2, false, &[]) { - Ok(_) => panic!("This shouldn't be an Ok"), - Err(err) => { - matches!(err, Error::JSON(_)); - } - }; + compare_strs(valid_json1, invalid_json2, false, &[]) + .expect_err("Parsing invalid JSON didn't throw an err"); } } From 6f2a322bd43370d6f2e1667c57fd378a14020b39 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Mon, 27 May 2024 00:09:35 +0200 Subject: [PATCH 42/52] - More minor cleanups --- src/process.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/process.rs b/src/process.rs index 9c0fc00..5d0d689 100644 --- a/src/process.rs +++ b/src/process.rs @@ -105,11 +105,7 @@ fn match_json( fn process_values(a: &Value, b: &Value) -> Result { if a == b { - Ok(Mismatch::new( - DiffTreeNode::Null, - DiffTreeNode::Null, - DiffTreeNode::Null, - )) + Ok(Mismatch::empty()) } else { Ok(Mismatch::new( DiffTreeNode::Null, From e4f6ec9a6d38a26cf5f22eb26d8a99521a0a6832 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Mon, 27 May 2024 00:11:27 +0200 Subject: [PATCH 43/52] - Fix documentation --- src/enums.rs | 2 +- src/lib.rs | 1 + src/process.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/enums.rs b/src/enums.rs index 31420ca..24ad804 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -123,7 +123,7 @@ impl<'a> PathElement<'a> { } } -/// A view on a single end-node of the [`DiffKeyNode`] tree. +/// A view on a single end-node of the [`DiffTreeNode`] tree. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DiffEntry<'a> { pub path: Vec>, diff --git a/src/lib.rs b/src/lib.rs index 84db57d..86f1490 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ 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; diff --git a/src/process.rs b/src/process.rs index 5d0d689..5b43ca4 100644 --- a/src/process.rs +++ b/src/process.rs @@ -12,7 +12,7 @@ use crate::Mismatch; use crate::Result; /// 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_values`] after deserializing the string slices into [`serde_json::Value`]. +/// 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( From 2fa9d0f1ac2e1d56274a612dacd7107e315c0b20 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 21:34:49 +0200 Subject: [PATCH 44/52] - Add even better docs - Add more doctests - Fix existing tests --- src/enums.rs | 42 ++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 44 ++++++++++++++++++++++++++++++++++++++++---- src/process.rs | 9 +++------ src/sort.rs | 23 ++++++++++++++++++++++- 4 files changed, 103 insertions(+), 15 deletions(-) diff --git a/src/enums.rs b/src/enums.rs index 24ad804..00728fe 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; use std::fmt::{Display, Formatter}; +use serde_json::Value; use thiserror::Error; use vg_errortools::FatIOError; @@ -19,10 +21,6 @@ impl From for Error { } } -use std::collections::HashMap; - -use serde_json::Value; - #[derive(Debug, PartialEq)] pub enum DiffTreeNode { Null, @@ -130,6 +128,16 @@ pub struct DiffEntry<'a> { 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 { @@ -158,3 +166,29 @@ impl Display for PathElement<'_> { } } } + +#[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 86f1490..f181439 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,12 +23,42 @@ //! 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 mod enums; -pub mod mismatch; -pub mod process; -pub mod sort; pub use enums::DiffEntry; pub use enums::DiffTreeNode; pub use enums::DiffType; @@ -37,4 +67,10 @@ 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/process.rs b/src/process.rs index 5b43ca4..ee56f97 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,15 +1,15 @@ use std::collections::HashMap; use std::collections::HashSet; -use diffs::{myers, Diff, Replace}; +use diffs::{Diff, myers, Replace}; use regex::Regex; use serde_json::Map; use serde_json::Value; -use crate::sort::preprocess_array; 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`]. @@ -194,7 +194,6 @@ fn process_arrays( 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 { @@ -501,9 +500,7 @@ mod tests { assert_eq!(diffs.len(), 3); let diffs: Vec<_> = diffs.into_iter().map(|d| d.to_string()).collect(); - for diff in &diffs { - eprintln!("{diff}"); - } + 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())); diff --git a/src/sort.rs b/src/sort.rs index f4c4b51..2edf8e9 100644 --- a/src/sort.rs +++ b/src/sort.rs @@ -1,6 +1,27 @@ +use std::borrow::Cow; + use regex::Regex; use serde_json::Value; -use std::borrow::Cow; + +/// 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, From 661e966994cbec9b8d9618724e5f9ed1e2fefa95 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 21:36:46 +0200 Subject: [PATCH 45/52] - Add coverage --- .github/workflows/coverage.yml | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..d5b8b7d --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,43 @@ +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: "image-compare" + 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 + cargo +nightly test --verbose + 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" From 1450958dc2c2a157bba4f3960a07d712ef73d248 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 21:40:08 +0200 Subject: [PATCH 46/52] - Fix README.md - Add coverage to README.md --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cfa8c39..03f239a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![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") -[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) +[![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) ## Contributors: @@ -12,31 +13,34 @@ ## Library + json_diff_ng can be used to get diffs of json-serializable structures in rust. ### Usage example + ```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(); +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")"# + diffs.first().unwrap().to_string(), + r#".[0].c.[1].("f" != "e")"# ); ``` See [docs.rs](https://docs.rs/json_diff_ng) for more details. - ## CLI -json-diff is a command line utility to compare two jsons. + +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. +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: @@ -50,5 +54,6 @@ file : read input from json files direct : read input from command line ### Installation + `$ cargo install json_diff_ng` From 45b513bfb3dadffa2c10b0e891a3773f01716a98 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 21:42:32 +0200 Subject: [PATCH 47/52] - Fix target in coverage.yml --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d5b8b7d..7e6810c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -28,7 +28,7 @@ jobs: - name: Run grcov env: - PROJECT_NAME: "image-compare" + PROJECT_NAME: "json-diff" RUSTFLAGS: "-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" CARGO_INCREMENTAL: 0 run: | From d62528dc8ad066be9750605fd2d0883504ad3c1b Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 21:44:54 +0200 Subject: [PATCH 48/52] - Add cached build for grcov --- .github/workflows/coverage.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7e6810c..742fa19 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,8 +19,9 @@ jobs: - name: Install lcov run: sudo apt-get install lcov - - name: install grcov - run: cargo install grcov + - uses: taiki-e/cache-cargo-install-action@v2 + with: + tool: grcov - uses: actions/checkout@v2 with: From b8bb8dd9ee9dc8932d56c2358912bb0d43f2c74c Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 21:49:53 +0200 Subject: [PATCH 49/52] - Go to non-cached grcov build again --- .github/workflows/coverage.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 742fa19..7e6810c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,9 +19,8 @@ jobs: - name: Install lcov run: sudo apt-get install lcov - - uses: taiki-e/cache-cargo-install-action@v2 - with: - tool: grcov + - name: install grcov + run: cargo install grcov - uses: actions/checkout@v2 with: From 2b24ba68aa8a208c518b17bf030a4a7721aaa07e Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 21:50:45 +0200 Subject: [PATCH 50/52] - Add doctest coverage also --- .github/workflows/coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7e6810c..bac04f2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -29,6 +29,7 @@ jobs: - 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: | From aaf53b5fedeff3fb2f666af0067edfa444529163 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 22:22:04 +0200 Subject: [PATCH 51/52] - Add feature for CLI - Make it a default feature - Add regex option to CLI --- Cargo.toml | 21 ++++++++++++++------- src/enums.rs | 2 ++ src/main.rs | 24 ++++++++++++++++++------ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c478c32..56b6355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "json_diff_ng" -version = "0.6.0-RC3" -authors = ["ksceriath", "ChrisRega"] +version = "0.6.0" +authors = ["ChrisRega", "ksceriath"] edition = "2021" license = "Unlicense" -description = "A JSON diff library and CLI." +description = "A JSON diff library, featuring deep-sorting and key exclusion by regex. CLI is included." readme = "README.md" homepage = "https://github.com/ChrisRega/json-diff" repository = "https://github.com/ChrisRega/json-diff" -keywords = ["cli", "diff", "json"] -categories = ["command-line-utilities"] +categories = ["command-line-utilities", "development-tools"] +keywords = ["json-structural-diff", "json-diff", "diff", "json", "cli"] [lib] name = "json_diff_ng" @@ -19,6 +19,11 @@ crate-type = ["lib"] [[bin]] 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 @@ -26,7 +31,9 @@ path = "src/main.rs" thiserror = "1.0" vg_errortools = "0.1" serde_json = { version = "1.0", features = ["preserve_order"] } -maplit = "1.0" -clap = { version = "4.5", features = ["derive"] } diffs = "0.5" regex = "1.10" +clap = { version = "4.5", features = ["derive"], optional = true } + +[dev-dependencies] +maplit = "1.0" diff --git a/src/enums.rs b/src/enums.rs index 00728fe..2e95d9e 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -13,6 +13,8 @@ pub enum Error { 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 { diff --git a/src/main.rs b/src/main.rs index d5b4d14..31b0497 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,13 +23,14 @@ struct Args { /// deep-sort arrays before comparing sort_arrays: bool, - #[clap(short, long, default_value_t = 20)] - /// truncate keys with more chars then this parameter - truncation_length: usize, + #[clap(short, long)] + /// Exclude a given list of keys by regex. + exclude_keys: Option>, } 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 } => { @@ -38,9 +39,20 @@ fn main() -> Result<()> { (d1, d2) } }; - - let mismatch = compare_strs(&json_1, &json_2, args.sort_arrays, &[])?; - + 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); From 2d2b0a4f485928be45e264898e830da2f7f3cef8 Mon Sep 17 00:00:00 2001 From: Christopher Regali Date: Thu, 30 May 2024 22:31:12 +0200 Subject: [PATCH 52/52] - Don't run tests on CLI --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bac04f2..e367da5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,8 +33,8 @@ jobs: 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 - cargo +nightly test --verbose + 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