diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..bac04f2 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,44 @@ +name: coverage instrument based + +on: [ push, pull_request ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install latest nightly + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: rustfmt, clippy, llvm-tools-preview + + - name: Install lcov + run: sudo apt-get install lcov + + - name: install grcov + run: cargo install grcov + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Run grcov + env: + PROJECT_NAME: "json-diff" + RUSTDOCFLAGS: "-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" + RUSTFLAGS: "-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" + CARGO_INCREMENTAL: 0 + run: | + cargo +nightly build --verbose + 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" 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` diff --git a/src/enums.rs b/src/enums.rs index 31420ca..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, @@ -123,13 +121,23 @@ 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>, 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 84db57d..f181439 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,17 +23,54 @@ //! 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; pub use enums::Error; +pub use enums::PathElement; pub use mismatch::Mismatch; pub use process::compare_serde_values; pub use process::compare_strs; + +pub mod enums; +pub mod mismatch; +pub mod process; +pub mod sort; + pub type Result = std::result::Result; diff --git a/src/process.rs b/src/process.rs index 44529c2..ee56f97 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,18 +1,18 @@ 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::enums::Error; -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_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( @@ -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,13 +103,9 @@ 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, - DiffTreeNode::Null, - DiffTreeNode::Null, - )) + Ok(Mismatch::empty()) } else { Ok(Mismatch::new( DiffTreeNode::Null, @@ -118,7 +120,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 +152,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); @@ -192,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 { @@ -225,7 +226,7 @@ fn insert_child_key_diff( parent: DiffTreeNode, child: DiffTreeNode, line: usize, -) -> Result { +) -> Result { if child == DiffTreeNode::Null { return Ok(parent); } @@ -243,7 +244,7 @@ fn insert_child_key_map( parent: DiffTreeNode, child: DiffTreeNode, key: &String, -) -> Result { +) -> Result { if child == DiffTreeNode::Null { return Ok(parent); } @@ -499,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())); @@ -644,7 +643,7 @@ mod tests { assert_eq!( compare_strs(data1, data2, false, &[]).unwrap(), - Mismatch::new(DiffTreeNode::Null, DiffTreeNode::Null, DiffTreeNode::Null) + Mismatch::empty() ); } @@ -652,23 +651,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"); } } 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,