#![doc = include_str!("../README.md")]
use serde::{ser::SerializeMap, Serialize};
#[derive(Debug, Serialize)]
#[serde(tag = "entry_difference", rename_all = "snake_case")]
pub enum EntryDifference {
Missing { value: serde_json::Value },
Extra,
Value { value_diff: Difference },
}
#[derive(Debug)]
pub struct DumbMap<K: Serialize, V: Serialize>(pub Vec<(K, V)>);
impl<K: Serialize, V: Serialize> Serialize for DumbMap<K, V> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(key, value)?;
}
map.end()
}
}
#[derive(Debug, Serialize)]
#[serde(tag = "array_difference", rename_all = "snake_case")]
pub enum ArrayDifference {
PairsOnly {
different_pairs: DumbMap<usize, Difference>,
},
Shorter {
different_pairs: Option<DumbMap<usize, Difference>>,
missing_elements: Vec<serde_json::Value>,
},
Longer {
different_pairs: Option<DumbMap<usize, Difference>>,
extra_length: usize,
},
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Type {
Null,
Array,
Bool,
Object,
String,
Number,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum ScalarDifference {
Bool {
source: bool,
target: bool,
},
String {
source: String,
target: String,
},
Number {
source: serde_json::Number,
target: serde_json::Number,
},
}
#[derive(Debug, Serialize)]
#[serde(tag = "difference_of", rename_all = "snake_case")]
pub enum Difference {
Scalar(ScalarDifference),
Type {
source_type: Type,
target_type: Type,
target_value: serde_json::Value,
},
Array(ArrayDifference),
Object {
different_entries: DumbMap<String, EntryDifference>,
},
}
#[must_use]
pub fn arrays(
source: Vec<serde_json::Value>,
target: Vec<serde_json::Value>,
) -> Option<ArrayDifference> {
let mut source_iter = source.into_iter().enumerate().peekable();
let mut target_iter = target.into_iter().peekable();
let mut different_pairs = vec![];
while let (Some(_), Some(_)) = (source_iter.peek(), target_iter.peek()) {
let (Some((i, source)), Some(target)) = (source_iter.next(), target_iter.next()) else {
unreachable!("checked by peek()");
};
different_pairs.push(values(source, target).map(|diff| (i, diff)));
}
let different_pairs = different_pairs.into_iter().flatten().collect::<Vec<_>>();
let different_pairs = if different_pairs.is_empty() {
None
} else {
Some(DumbMap(different_pairs))
};
let extra_elements = source_iter.map(|(_, source)| source).collect::<Vec<_>>();
let missing_elements = target_iter.collect::<Vec<_>>();
if !extra_elements.is_empty() {
return Some(ArrayDifference::Longer {
different_pairs,
extra_length: extra_elements.len(),
});
}
if !missing_elements.is_empty() {
return Some(ArrayDifference::Shorter {
different_pairs,
missing_elements,
});
}
different_pairs.map(|different_pairs| ArrayDifference::PairsOnly { different_pairs })
}
#[must_use]
pub fn objects(
source: serde_json::Map<String, serde_json::Value>,
mut target: serde_json::Map<String, serde_json::Value>,
) -> Option<DumbMap<String, EntryDifference>> {
let mut value_differences = source
.into_iter()
.filter_map(|(key, source)| {
let Some(target) = target.remove(&key) else {
return Some((key, EntryDifference::Extra));
};
values(source, target).map(|diff| (key, EntryDifference::Value { value_diff: diff }))
})
.collect::<Vec<_>>();
value_differences.extend(target.into_iter().map(|(missing_key, missing_value)| {
(
missing_key,
EntryDifference::Missing {
value: missing_value,
},
)
}));
if value_differences.is_empty() {
None
} else {
Some(DumbMap(value_differences))
}
}
impl From<serde_json::Value> for Type {
fn from(value: serde_json::Value) -> Self {
match value {
serde_json::Value::Null => Type::Null,
serde_json::Value::Bool(_) => Type::Bool,
serde_json::Value::Number(_) => Type::Number,
serde_json::Value::String(_) => Type::String,
serde_json::Value::Array(_) => Type::Array,
serde_json::Value::Object(_) => Type::Object,
}
}
}
#[must_use]
pub fn values(source: serde_json::Value, target: serde_json::Value) -> Option<Difference> {
use serde_json::Value::{Array, Bool, Null, Number, Object, String};
match (source, target) {
(Null, Null) => None,
(Bool(source), Bool(target)) => {
if source == target {
None
} else {
Some(Difference::Scalar(ScalarDifference::Bool {
source,
target,
}))
}
}
(Number(source), Number(target)) => {
if source == target {
None
} else {
Some(Difference::Scalar(ScalarDifference::Number {
source,
target,
}))
}
}
(String(source), String(target)) => {
if source == target {
None
} else {
Some(Difference::Scalar(ScalarDifference::String {
source,
target,
}))
}
}
(Array(source), Array(target)) => arrays(source, target).map(Difference::Array),
(Object(source), Object(target)) => objects(source, target)
.map(|different_entries| Difference::Object { different_entries }),
(source, target) => Some(Difference::Type {
source_type: source.into(),
target_type: target.clone().into(),
target_value: target,
}),
}
}