use crate::{Change, DiffConfig, Path};
use serde_value::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct Diff {
changes: BTreeMap<Path, Change>,
}
impl Diff {
pub fn new() -> Self {
Diff {
changes: BTreeMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn len(&self) -> usize {
self.changes.len()
}
pub fn changes(&self) -> &BTreeMap<Path, Change> {
&self.changes
}
pub fn get(&self, path: &str) -> Option<&Change> {
self.changes
.iter()
.find(|(p, _)| p.as_str() == path)
.map(|(_, c)| c)
}
pub fn paths(&self) -> impl Iterator<Item = &Path> {
self.changes.keys()
}
pub fn insert(&mut self, path: Path, change: Change) {
self.changes.insert(path, change);
}
pub fn merge(&mut self, other: Diff) {
self.changes.extend(other.changes);
}
}
impl Default for Diff {
fn default() -> Self {
Self::new()
}
}
pub fn diff_values(old: &Value, new: &Value, path: Path, config: &DiffConfig) -> Diff {
let mut diff = Diff::new();
if config.should_ignore(&path) {
return diff;
}
if config.exceeds_depth(&path) {
diff.insert(
path,
Change::Elided {
reason: "max depth exceeded".into(),
count: 0,
},
);
return diff;
}
if let Some(comparator) = config.get_comparator(&path) {
if comparator(old, new) {
return diff;
}
}
macro_rules! match_identical {
($($variant:ident),+ $(,)?) => {
match (old, new) {
$((Value::$variant(a), Value::$variant(b)) if a == b => return diff,)+
(Value::Unit, Value::Unit) => return diff,
_ => {}
}
};
}
match_identical!(Bool, U8, U16, U32, U64, I8, I16, I32, I64, String, Char);
match (old, new) {
(Value::F32(a), Value::F32(b)) => {
if let Some(change) = diff_float(*a, *b, config.float_tolerance_for(&path), old, new) {
diff.insert(path, change);
}
}
(Value::F64(a), Value::F64(b)) => {
if let Some(change) = diff_float(*a, *b, config.float_tolerance_for(&path), old, new) {
diff.insert(path, change);
}
}
(Value::Bytes(a), Value::Bytes(b)) => {
if let Some(change) = diff_bytes(a, b, old, new) {
diff.insert(path, change);
}
}
(Value::Map(old_map), Value::Map(new_map)) => {
diff.merge(diff_maps(old_map, new_map, path, config));
}
(Value::Seq(old_seq), Value::Seq(new_seq)) => {
let algorithm = config.get_sequence_algorithm(&path);
diff.merge(crate::sequence_diff::diff_sequences_with_algorithm(
old_seq, new_seq, path, config, algorithm,
));
}
(Value::Option(old_opt), Value::Option(new_opt)) => {
match (old_opt.as_deref(), new_opt.as_deref()) {
(Some(old_val), Some(new_val)) => {
diff.merge(diff_values(old_val, new_val, path, config));
}
(Some(_), None) => {
diff.insert(path, Change::Removed(old.clone()));
}
(None, Some(_)) => {
diff.insert(path, Change::Added(new.clone()));
}
(None, None) => {}
}
}
(Value::Newtype(old_inner), Value::Newtype(new_inner)) => {
diff.merge(diff_values(old_inner, new_inner, path, config));
}
_ => {
diff.insert(
path,
Change::Modified {
from: old.clone(),
to: new.clone(),
},
);
}
}
diff
}
fn hex_preview(bytes: &[u8], len: usize) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(len * 2);
for byte in bytes.iter().take(len) {
write!(&mut s, "{:02x}", byte).unwrap();
}
s
}
fn diff_maps(
old: &BTreeMap<Value, Value>,
new: &BTreeMap<Value, Value>,
path: Path,
config: &DiffConfig,
) -> Diff {
use std::cmp::Ordering;
let mut diff = Diff::new();
let mut old_iter = old.iter().peekable();
let mut new_iter = new.iter().peekable();
loop {
match (old_iter.peek(), new_iter.peek()) {
(Some((old_key, _)), Some((new_key, _))) => match old_key.cmp(new_key) {
Ordering::Less => {
let (key, val) = old_iter.next().unwrap();
let key_str = value_to_key(key);
let field_path = path.field(&key_str);
diff.insert(field_path, Change::Removed(val.clone()));
}
Ordering::Greater => {
let (key, val) = new_iter.next().unwrap();
let key_str = value_to_key(key);
let field_path = path.field(&key_str);
diff.insert(field_path, Change::Added(val.clone()));
}
Ordering::Equal => {
let (key, old_val) = old_iter.next().unwrap();
let (_, new_val) = new_iter.next().unwrap();
let key_str = value_to_key(key);
let field_path = path.field(&key_str);
diff.merge(diff_values(old_val, new_val, field_path, config));
}
},
(Some(_), None) => {
let (key, val) = old_iter.next().unwrap();
let key_str = value_to_key(key);
let field_path = path.field(&key_str);
diff.insert(field_path, Change::Removed(val.clone()));
}
(None, Some(_)) => {
let (key, val) = new_iter.next().unwrap();
let key_str = value_to_key(key);
let field_path = path.field(&key_str);
diff.insert(field_path, Change::Added(val.clone()));
}
(None, None) => break,
}
}
diff
}
fn diff_float<T>(a: T, b: T, tolerance: Option<f64>, old: &Value, new: &Value) -> Option<Change>
where
T: Into<f64> + Copy,
{
let a_f64 = a.into();
let b_f64 = b.into();
let equal = match (a_f64.is_nan(), b_f64.is_nan()) {
(true, true) => true,
(true, false) | (false, true) => false,
_ if a_f64 == b_f64 => true,
_ => tolerance.is_some_and(|tol| (a_f64 - b_f64).abs() <= tol),
};
if equal {
None
} else {
Some(Change::Modified {
from: old.clone(),
to: new.clone(),
})
}
}
fn diff_bytes(a: &[u8], b: &[u8], old: &Value, new: &Value) -> Option<Change> {
if a == b {
return None;
}
let from = if a.len() > 64 {
Value::String(format!("{}... ({} bytes)", hex_preview(a, 32), a.len()))
} else {
old.clone()
};
let to = if b.len() > 64 {
Value::String(format!("{}... ({} bytes)", hex_preview(b, 32), b.len()))
} else {
new.clone()
};
Some(Change::Modified { from, to })
}
fn value_to_key(value: &Value) -> String {
macro_rules! to_string {
($($variant:ident),+ $(,)?) => {
match value {
$(Value::$variant(v) => v.to_string(),)+
Value::String(s) => s.clone(),
_ => format!("{:?}", value),
}
};
}
to_string!(Bool, U8, U16, U32, U64, I8, I16, I32, I64, F32, F64, Char)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_diff() {
let diff = Diff::new();
assert!(diff.is_empty());
assert_eq!(diff.len(), 0);
}
#[test]
fn test_insert() {
let mut diff = Diff::new();
diff.insert(Path::root().field("x"), Change::Added(Value::I64(42)));
assert!(!diff.is_empty());
assert_eq!(diff.len(), 1);
}
#[test]
fn test_merge() {
let mut d1 = Diff::new();
d1.insert(Path::root().field("a"), Change::Added(Value::I64(1)));
let mut d2 = Diff::new();
d2.insert(Path::root().field("b"), Change::Added(Value::I64(2)));
d1.merge(d2);
assert_eq!(d1.len(), 2);
}
#[test]
fn test_get() {
let mut diff = Diff::new();
diff.insert(Path::root().field("x"), Change::Added(Value::I64(42)));
assert!(diff.get("x").is_some());
assert!(diff.get("y").is_none());
}
#[test]
fn test_hex_preview() {
let bytes = vec![0x12, 0x34, 0x56, 0x78];
assert_eq!(hex_preview(&bytes, 4), "12345678");
assert_eq!(hex_preview(&bytes, 2), "1234");
}
}