facet_inspect/
diff.rs

1use std::collections::HashMap;
2
3use facet::{Def, Facet};
4use facet_reflect::Peek;
5
6use crate::{FacetInspect, FacetPath};
7
8#[derive(Debug)]
9pub struct ModificationPeek<'mem, 'facet> {
10    pub old_value: Peek<'mem, 'facet>,
11    pub new_value: Peek<'mem, 'facet>,
12}
13
14#[derive(Debug, Facet, PartialEq)]
15#[repr(C)]
16pub enum Modification {
17    U32 { before: u32, after: u32 },
18    String { before: String, after: String },
19}
20
21impl<'a> From<ModificationPeek<'a, 'a>> for Modification {
22    fn from(peek: ModificationPeek<'a, 'a>) -> Self {
23        let old_value = peek.old_value;
24        let new_value = peek.new_value;
25
26        match old_value.shape().type_identifier {
27            "u32" => {
28                let before = *old_value.get::<u32>().expect("old_value should be u32");
29                let after = *new_value.get::<u32>().expect("new_value should be u32");
30                Modification::U32 { before, after }
31            }
32            "String" => {
33                let before = old_value
34                    .get::<String>()
35                    .expect("old_value should be String")
36                    .clone();
37                let after = new_value
38                    .get::<String>()
39                    .expect("new_value should be String")
40                    .clone();
41                Modification::String { before, after }
42            }
43            _ => {
44                // Handle other types as needed
45                // For now, we will just panic if the type is not recognized
46                panic!(
47                    "Unsupported type for modification: {}",
48                    old_value.shape().type_identifier
49                );
50            }
51        }
52    }
53}
54
55#[derive(Debug, Facet)]
56pub struct Diff {
57    pub changes: HashMap<FacetPath, Modification>,
58}
59
60impl Diff {
61    pub fn compare<'a, T: Facet<'a>>(facet1: &'a T, facet2: &'a T) -> Self {
62        let mut changes = HashMap::new();
63
64        for ((path, peek1), (_, peek2)) in facet1.inspect().zip(facet2.inspect()) {
65            if peek1 == peek2 {
66                continue; // No change
67            }
68
69            if peek1.shape() == peek2.shape() && !matches!(peek1.shape().def, Def::Scalar) {
70                // There is change, deeper in the shape
71                // TODO: find a way to do the same without using Def::Scalar
72                continue;
73            }
74
75            let modif = ModificationPeek {
76                old_value: peek1,
77                new_value: peek2,
78            };
79
80            changes.insert(path, Modification::from(modif));
81        }
82
83        Diff { changes }
84    }
85}
86
87#[derive(Debug)]
88pub struct ShapeDiff<'a> {
89    pub changes: HashMap<FacetPath, DiffType<'a>>,
90}
91
92#[derive(Debug)]
93pub enum DiffType<'a> {
94    Added(Peek<'a, 'a>),
95    Removed(Peek<'a, 'a>),
96    Modified(ModificationPeek<'a, 'a>),
97}
98
99impl<'a> ShapeDiff<'a> {
100    pub fn compare<A: Facet<'a>, B: Facet<'a>>(facet1: &'a A, facet2: &'a B) -> Self {
101        let facet1_elements = facet1.inspect().collect::<HashMap<_, _>>();
102        let facet2_elements = facet2.inspect().collect::<HashMap<_, _>>();
103
104        let mut changes = HashMap::new();
105
106        for path in facet1_elements.keys().chain(facet2_elements.keys()) {
107            let peek1 = facet1_elements.get(path);
108            let peek2 = facet2_elements.get(path);
109
110            match (peek1, peek2) {
111                (Some(p1), Some(p2)) if p1 == p2 => continue, // No change
112                (Some(p1), Some(p2))
113                    if p1.shape() == p2.shape() && !matches!(p1.shape().def, Def::Scalar) =>
114                {
115                    // There is change, deeper in the shape
116                    // TODO: find a way to do the same without using Def::Scalar
117                    continue;
118                }
119                (Some(p1), None) => {
120                    changes.insert(path.clone(), DiffType::Removed(*p1));
121                }
122                (None, Some(p2)) => {
123                    changes.insert(path.clone(), DiffType::Added(*p2));
124                }
125                (Some(p1), Some(p2)) => {
126                    changes.insert(
127                        path.clone(),
128                        DiffType::Modified(ModificationPeek {
129                            old_value: *p1,
130                            new_value: *p2,
131                        }),
132                    );
133                }
134                (None, None) => unreachable!(), // This case should not happen
135            }
136        }
137
138        ShapeDiff { changes }
139    }
140}
141
142pub trait FacetDiff<'facet>: Facet<'facet> + Sized {
143    fn diff(&'facet self, other: &'facet Self) -> Diff {
144        Diff::compare(self, other)
145    }
146
147    fn shape_diff<T: Facet<'facet>>(&'facet self, other: &'facet T) -> ShapeDiff<'facet> {
148        ShapeDiff::compare(self, other)
149    }
150}
151
152impl<'facet, T: Facet<'facet>> FacetDiff<'facet> for T {}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use facet::Facet;
158
159    #[derive(Facet, Clone)]
160    struct TestFacet {
161        field1: u32,
162        field2: String,
163    }
164
165    #[derive(Facet)]
166    struct NestedFacet {
167        nested_field: TestFacet,
168    }
169
170    #[derive(Facet)]
171    struct AnotherNestedFacet {
172        nested_field: TestFacet,
173        another_field: u32,
174    }
175
176    #[test]
177    fn test_facet_diff() {
178        let sub_facet1 = TestFacet {
179            field1: 42,
180            field2: "Hello".to_string(),
181        };
182
183        let facet1 = NestedFacet {
184            nested_field: sub_facet1.clone(),
185        };
186
187        let facet2 = NestedFacet {
188            nested_field: TestFacet {
189                field1: 43,
190                field2: "World".to_string(),
191            },
192        };
193
194        let diff = facet1.diff(&facet2);
195
196        assert_eq!(
197            diff.changes.get(&"$.nested_field.field1".into()).unwrap(),
198            &Modification::U32 {
199                before: 42,
200                after: 43
201            }
202        );
203    }
204
205    #[test]
206    fn test_shape_diff() {
207        let sub_facet1 = TestFacet {
208            field1: 42,
209            field2: "Hello".to_string(),
210        };
211
212        let facet1 = NestedFacet {
213            nested_field: sub_facet1.clone(),
214        };
215
216        let facet2 = AnotherNestedFacet {
217            nested_field: TestFacet {
218                field1: 43,
219                field2: "World".to_string(),
220            },
221            another_field: 100,
222        };
223
224        let shape_diff = facet1.shape_diff(&facet2);
225        dbg!(shape_diff);
226    }
227}