#![allow(clippy::ptr_arg)]
use serde_json::Value;
use std::fmt::{self, Display, Write};
type JsonObject = serde_json::Map<String, Value>;
#[derive(Debug, PartialEq)]
pub struct Diff<'a> {
pub path: String,
pub existing: &'a Value,
pub desired: &'a Value,
}
impl<'a> Display for Diff<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Diff at path: '{}', existing: {}, desired: {}",
self.path, self.existing, self.desired
)
}
}
pub struct Diffs<'a>(Vec<Diff<'a>>);
impl<'a> Diffs<'a> {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn non_empty(&self) -> bool {
!self.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
#[cfg(feature = "testkit")]
pub fn into_vec(self) -> Vec<Diff<'a>> {
self.0
}
}
impl<'a> Display for Diffs<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_empty() {
f.write_str("<empty>")
} else {
write!(f, "{} differences: ", self.0.len())?;
for (i, diff) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
Display::fmt(diff, f)?;
}
Ok(())
}
}
}
enum Segment<'a> {
Key(&'a str),
Index(usize),
}
pub fn compare_values<'a>(existing: &'a Value, desired: &'a Value) -> Diffs<'a> {
let mut diffs = Vec::new();
let mut path = Vec::with_capacity(8);
compare(&mut diffs, &mut path, existing, desired);
Diffs(diffs)
}
fn compare<'a>(
diffs: &mut Vec<Diff<'a>>,
path: &mut Vec<Segment<'a>>,
superset: &'a Value,
subset: &'a Value,
) {
match (superset, subset) {
(Value::Object(ref super_map), Value::Object(ref sub_map)) => {
compare_objects(diffs, path, super_map, sub_map);
}
(Value::Array(ref super_array), Value::Array(ref sub_array)) => {
compare_arrays(diffs, path, super_array, sub_array);
}
(a, b) if a != b => {
diffs.push(diff(&*path, a, b));
}
_ => {}
}
}
fn compare_objects<'a>(
diffs: &mut Vec<Diff<'a>>,
path: &mut Vec<Segment<'a>>,
existing: &'a JsonObject,
desired: &'a JsonObject,
) {
for (key, desired_val) in desired.iter() {
check_value(diffs, path, existing, key, desired_val);
}
}
fn compare_arrays<'a>(
diffs: &mut Vec<Diff<'a>>,
path: &mut Vec<Segment<'a>>,
existing: &'a Vec<Value>,
desired: &'a Vec<Value>,
) {
if is_associative(existing, desired) {
compare_associative_arrays(diffs, path, existing, desired);
} else {
compare_non_associative_arrays(diffs, path, existing, desired);
}
}
fn compare_non_associative_arrays<'a>(
diffs: &mut Vec<Diff<'a>>,
path: &mut Vec<Segment<'a>>,
existing: &'a Vec<Value>,
desired: &'a Vec<Value>,
) {
for (i, desired_item) in desired.iter().enumerate() {
path.push(Segment::Index(i));
if existing.len() > i {
compare(diffs, path, &existing[i], desired_item);
} else {
diffs.push(diff(&*path, &Value::Null, desired_item));
}
path.pop();
}
}
fn compare_associative_arrays<'a>(
diffs: &mut Vec<Diff<'a>>,
path: &mut Vec<Segment<'a>>,
existing: &'a Vec<Value>,
desired: &'a Vec<Value>,
) {
for (i, desired_val) in desired.iter().enumerate() {
path.push(Segment::Index(i));
let name = desired_val.get("name").unwrap().as_str().unwrap();
let existing_item = existing.iter().find(|e| {
e.get("name")
.and_then(Value::as_str)
.map(|item_name| item_name == name)
.unwrap_or(false)
});
if let Some(existing_match) = existing_item {
compare(diffs, path, existing_match, desired_val);
} else {
diffs.push(diff(&*path, &Value::Null, desired_val));
}
path.pop();
}
}
fn is_associative(_existing: &[Value], desired: &[Value]) -> bool {
desired.iter().all(|v| {
v.as_object()
.map(|o| o.get("name").map(Value::is_string).unwrap_or(false))
.unwrap_or(false)
})
}
fn check_value<'a>(
diffs: &mut Vec<Diff<'a>>,
path: &mut Vec<Segment<'a>>,
existing: &'a JsonObject,
key: &'a str,
value: &'a Value,
) {
path.push(Segment::Key(key));
match existing.get(key) {
Some(super_val) => {
compare(diffs, path, super_val, value);
}
None => {
diffs.push(diff(&*path, &Value::Null, value));
}
}
path.pop();
}
fn diff<'a>(path: &[Segment], existing: &'a Value, desired: &'a Value) -> Diff<'a> {
let mut p = String::with_capacity(8);
for s in path.iter() {
p.push('.');
match s {
Segment::Key(ref k) => p.push_str(k),
Segment::Index(i) => {
write!(p, "{}", i).unwrap();
}
}
}
Diff {
path: p,
existing,
desired,
}
}
#[cfg(test)]
mod test {
use super::*;
use serde_json::{json, Value};
#[test]
fn returs_diffs_from_objects() {
let existing = json! {{
"key1": {
"nested1": "same",
"nested2": "existing2",
},
"key2": "here",
"key3": 7,
}};
let desired = json! {{
"key1": {
"nested1": "same",
"nested2": "desired2",
},
"key3": 8,
"newKey": true,
}};
let diffs = compare_values(&existing, &desired);
let existing2 = Value::String("existing2".to_owned());
let desired2 = Value::String("desired2".to_owned());
let seven = Value::Number(7.into());
let eight = Value::Number(8.into());
let expected = vec![
Diff {
path: ".key1.nested2".to_owned(),
existing: &existing2,
desired: &desired2,
},
Diff {
path: ".key3".to_owned(),
existing: &seven,
desired: &eight,
},
Diff {
path: ".newKey".to_owned(),
existing: &Value::Null,
desired: &Value::Bool(true),
},
];
assert_all_diffs_present(expected, diffs);
}
#[test]
fn returns_diffs_from_nested_arrays() {
let existing = json! {{
"nonAssociative": [
{
"nonAssociative": [
{ "same": "same" },
{ "same": "same" },
{ "different": "e" },
]
}
],
"associative": [
{"name": "name2", "value": "value2"},
{"name": "name3", "value": "value3"},
{"name": "name1", "value": "value1existing"},
]
}};
let desired = json! {{
"nonAssociative": [
{
"nonAssociative": [
{ "same": "same" },
{ "same": "same" },
{ "different": "desired" },
]
}
],
"associative": [
{"name": "name3", "value": "value3"},
{"name": "name1", "value": "value1desired"},
{"name": "name2", "value": "value2"},
]
}};
let e = json!("e");
let desired_val = json!("desired");
let value1existing = json!("value1existing");
let value1desired = json!("value1desired");
let expected = vec![
Diff {
path: ".nonAssociative.0.nonAssociative.2.different".to_owned(),
existing: &e,
desired: &desired_val,
},
Diff {
path: ".associative.1.value".to_owned(),
existing: &value1existing,
desired: &value1desired,
},
];
let actual = compare_values(&existing, &desired);
assert_all_diffs_present(expected, actual);
}
fn assert_all_diffs_present(expected: Vec<Diff>, mut actual: Diffs) {
for expected_diff in expected.iter() {
if !actual.0.contains(expected_diff) {
panic!(
"Expected to find diff: {} in actual diffs: {}",
expected_diff, actual
);
}
}
actual.0.retain(|e| !expected.contains(e));
if !actual.is_empty() {
panic!(
"Expected {} diffs but found {} extra, \nextra_diffs: {}\nexpected: {}",
expected.len(),
actual.len(),
actual,
Diffs(expected)
);
}
}
}