use std::collections::HashMap;
use facet::{Def, Facet};
use facet_reflect::Peek;
use crate::{FacetInspect, FacetPath};
#[derive(Debug)]
pub struct ModificationPeek<'mem, 'facet> {
pub old_value: Peek<'mem, 'facet>,
pub new_value: Peek<'mem, 'facet>,
}
#[derive(Debug, Facet, PartialEq)]
#[repr(C)]
pub enum Modification {
U32 { before: u32, after: u32 },
String { before: String, after: String },
}
impl<'a> From<ModificationPeek<'a, 'a>> for Modification {
fn from(peek: ModificationPeek<'a, 'a>) -> Self {
let old_value = peek.old_value;
let new_value = peek.new_value;
match old_value.shape().type_identifier {
"u32" => {
let before = *old_value.get::<u32>().expect("old_value should be u32");
let after = *new_value.get::<u32>().expect("new_value should be u32");
Modification::U32 { before, after }
}
"String" => {
let before = old_value
.get::<String>()
.expect("old_value should be String")
.clone();
let after = new_value
.get::<String>()
.expect("new_value should be String")
.clone();
Modification::String { before, after }
}
_ => {
panic!(
"Unsupported type for modification: {}",
old_value.shape().type_identifier
);
}
}
}
}
#[derive(Debug, Facet)]
pub struct Diff {
pub changes: HashMap<FacetPath, Modification>,
}
impl Diff {
pub fn compare<'a, T: Facet<'a>>(facet1: &'a T, facet2: &'a T) -> Self {
let mut changes = HashMap::new();
for ((path, peek1), (_, peek2)) in facet1.inspect().zip(facet2.inspect()) {
if peek1 == peek2 {
continue; }
if peek1.shape() == peek2.shape() && !matches!(peek1.shape().def, Def::Scalar) {
continue;
}
let modif = ModificationPeek {
old_value: peek1,
new_value: peek2,
};
changes.insert(path, Modification::from(modif));
}
Diff { changes }
}
}
#[derive(Debug)]
pub struct ShapeDiff<'a> {
pub changes: HashMap<FacetPath, DiffType<'a>>,
}
#[derive(Debug)]
pub enum DiffType<'a> {
Added(Peek<'a, 'a>),
Removed(Peek<'a, 'a>),
Modified(ModificationPeek<'a, 'a>),
}
impl<'a> ShapeDiff<'a> {
pub fn compare<A: Facet<'a>, B: Facet<'a>>(facet1: &'a A, facet2: &'a B) -> Self {
let facet1_elements = facet1.inspect().collect::<HashMap<_, _>>();
let facet2_elements = facet2.inspect().collect::<HashMap<_, _>>();
let mut changes = HashMap::new();
for path in facet1_elements.keys().chain(facet2_elements.keys()) {
let peek1 = facet1_elements.get(path);
let peek2 = facet2_elements.get(path);
match (peek1, peek2) {
(Some(p1), Some(p2)) if p1 == p2 => continue, (Some(p1), Some(p2))
if p1.shape() == p2.shape() && !matches!(p1.shape().def, Def::Scalar) =>
{
continue;
}
(Some(p1), None) => {
changes.insert(path.clone(), DiffType::Removed(*p1));
}
(None, Some(p2)) => {
changes.insert(path.clone(), DiffType::Added(*p2));
}
(Some(p1), Some(p2)) => {
changes.insert(
path.clone(),
DiffType::Modified(ModificationPeek {
old_value: *p1,
new_value: *p2,
}),
);
}
(None, None) => unreachable!(), }
}
ShapeDiff { changes }
}
}
pub trait FacetDiff<'facet>: Facet<'facet> + Sized {
fn diff(&'facet self, other: &'facet Self) -> Diff {
Diff::compare(self, other)
}
fn shape_diff<T: Facet<'facet>>(&'facet self, other: &'facet T) -> ShapeDiff<'facet> {
ShapeDiff::compare(self, other)
}
}
impl<'facet, T: Facet<'facet>> FacetDiff<'facet> for T {}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
#[derive(Facet, Clone)]
struct TestFacet {
field1: u32,
field2: String,
}
#[derive(Facet)]
struct NestedFacet {
nested_field: TestFacet,
}
#[derive(Facet)]
struct AnotherNestedFacet {
nested_field: TestFacet,
another_field: u32,
}
#[test]
fn test_facet_diff() {
let sub_facet1 = TestFacet {
field1: 42,
field2: "Hello".to_string(),
};
let facet1 = NestedFacet {
nested_field: sub_facet1.clone(),
};
let facet2 = NestedFacet {
nested_field: TestFacet {
field1: 43,
field2: "World".to_string(),
},
};
let diff = facet1.diff(&facet2);
assert_eq!(
diff.changes.get(&"$.nested_field.field1".into()).unwrap(),
&Modification::U32 {
before: 42,
after: 43
}
);
}
#[test]
fn test_shape_diff() {
let sub_facet1 = TestFacet {
field1: 42,
field2: "Hello".to_string(),
};
let facet1 = NestedFacet {
nested_field: sub_facet1.clone(),
};
let facet2 = AnotherNestedFacet {
nested_field: TestFacet {
field1: 43,
field2: "World".to_string(),
},
another_field: 100,
};
let shape_diff = facet1.shape_diff(&facet2);
dbg!(shape_diff);
}
}