facet_inspect/
inspect.rs

1use facet::{Def, Facet, PointerType, Type, UserType};
2use facet_reflect::{HasFields, Peek};
3
4// TODO: Discuss if we should use a indexes as path to reduce the size of the path, this would make it more efficient to
5// send through the network, but less readable.
6/// A structure representing the path to a sub-object in a [`Facet`] object.
7#[derive(Facet, Debug, Clone, PartialEq, Eq, Hash)]
8pub struct FacetPath {
9    pub segments: Vec<String>,
10}
11
12impl FacetPath {
13    pub fn root() -> Self {
14        FacetPath {
15            segments: vec!["$".to_string()],
16        }
17    }
18
19    pub fn push(&mut self, path: &FacetPath) {
20        self.segments.extend(path.segments.iter().cloned());
21    }
22
23    pub fn join(&self, path: &FacetPath) -> Self {
24        let mut new_path = self.clone();
25        new_path.push(path);
26        new_path
27    }
28}
29
30impl From<&str> for FacetPath {
31    fn from(path: &str) -> Self {
32        FacetPath {
33            segments: path.split('.').map(|s| s.to_string()).collect(),
34        }
35    }
36}
37
38/// A structure allowing to iterate over the shape of a [`Facet`] object.
39/// This iterator will yield the [`Peek`]s of the [`Facet`] sub-objects composing the inspected object.
40/// Each [`Peek`] will be associated to the path leading to it, allowing to reconstruct the structure of the object.
41#[derive(Clone)]
42pub struct FacetIterator<'mem, 'facet> {
43    stack: Vec<(FacetPath, Peek<'mem, 'facet>)>,
44}
45
46impl<'mem, 'facet> Iterator for FacetIterator<'mem, 'facet> {
47    type Item = (FacetPath, Peek<'mem, 'facet>);
48
49    fn next(&mut self) -> Option<Self::Item> {
50        let Some((path, peek)) = self.stack.pop() else {
51            return None; // If the stack is empty, we are done
52        };
53
54        let def = peek.shape().def;
55        let ty = peek.shape().ty;
56
57        match (def, ty) {
58            (Def::Scalar, _) => {} // Scalars do not have sub-objects, so we skip them
59            (Def::Array(_), _) | (Def::List(_), _) | (Def::Slice(_), _) => {
60                self.push_list_items_to_stack(
61                    &path,
62                    peek.into_list_like().expect("Expected a list").iter(),
63                );
64            }
65            (Def::Map(_), _) => {
66                self.push_map_items_to_stack(
67                    &path,
68                    peek.into_map().expect("Expected a map").iter(),
69                );
70            }
71            (_, Type::User(UserType::Struct(_))) => {
72                self.push_fields_to_stack(&path, peek.into_struct().expect("Expected a struct"));
73            }
74            (_, Type::User(UserType::Enum(_))) => {
75                self.push_fields_to_stack(&path, peek.into_enum().expect("Expected an enum"));
76            }
77            (_, Type::Sequence(_)) => {
78                // Sequences are treated like lists, so we push their items to the stack
79                self.push_list_items_to_stack(
80                    &path,
81                    peek.into_list_like().expect("Expected a sequence").iter(),
82                );
83            }
84            (_, Type::Pointer(PointerType::Reference(r))) => {
85                let target = (r.target)();
86                if let Type::Sequence(_) = target.ty {
87                    self.push_list_items_to_stack(
88                        &path,
89                        peek.into_list_like().expect("Expected a sequence").iter(),
90                    );
91                }
92            }
93            (_, _) => {
94                // TODO: discuss behavior here as I don't think runtime crash is the best option
95                todo!(
96                    "this type is not yet supported for inspection\ndef:{:?}\nty:{:?}",
97                    def,
98                    ty
99                );
100            }
101        }
102
103        Some((path, peek))
104    }
105}
106
107impl<'mem, 'facet> FacetIterator<'mem, 'facet> {
108    fn push_fields_to_stack(
109        &mut self,
110        parent_path: &FacetPath,
111        object: impl HasFields<'mem, 'facet>,
112    ) {
113        // TODO: discuss if the performance trade-off of having the fields in reverse order is worth it
114        // We reverse the fields to maintain the order of fields as they are defined in the struct
115        for (field, peek) in object.fields().rev() {
116            let new_path = parent_path.join(&field.name.into());
117            self.stack.push((new_path, peek));
118        }
119    }
120
121    fn push_list_items_to_stack(
122        &mut self,
123        parent_path: &FacetPath,
124        list: impl Iterator<Item = Peek<'mem, 'facet>>,
125    ) {
126        for (index, item) in list.enumerate() {
127            let new_path = parent_path.join(&FacetPath::from(index.to_string().as_str()));
128            self.stack.push((new_path, item));
129        }
130    }
131
132    fn push_map_items_to_stack(
133        &mut self,
134        parent_path: &FacetPath,
135        map: impl Iterator<Item = (Peek<'mem, 'facet>, Peek<'mem, 'facet>)>,
136    ) {
137        for (key, value_peek) in map {
138            let new_path = parent_path.join(&FacetPath::from(format!("{key}").as_str()));
139            self.stack.push((new_path, value_peek));
140        }
141    }
142}
143
144pub trait FacetInspect<'a>: Facet<'a> {
145    /// Returns an iterator over the shape of the [`Facet`] object.
146    ///
147    /// The iterator will yield tuples containing the path to the sub-object and its corresponding [`Peek`].
148    fn inspect(&'a self) -> FacetIterator<'a, 'a> {
149        FacetIterator {
150            stack: vec![(FacetPath::root(), Peek::new(self))], // Start with the root path and a Peek of self
151        }
152    }
153
154    /// Returns a [`Peek`] for the sub-object at the specified path.
155    ///
156    /// If the path does not lead to a valid sub-object, `None` is returned.
157    fn get(&'a self, path: &FacetPath) -> Option<Peek<'a, 'a>> {
158        self.inspect()
159            .find(|(p, _)| p == path)
160            .map(|(_, peek)| peek)
161    }
162}
163
164impl<'a, T: Facet<'a>> FacetInspect<'a> for T {}
165
166#[cfg(test)]
167mod tests {
168    use super::FacetInspect;
169    use super::*;
170    use std::collections::HashMap;
171
172    #[derive(Facet)]
173    struct TestFacet {
174        field1: u32,
175        field2: String,
176    }
177
178    #[derive(Facet)]
179    struct NestedFacet {
180        nested_field: TestFacet,
181    }
182
183    #[derive(Facet, Debug, PartialEq)]
184    #[repr(u8)]
185    enum MyEnum {
186        Unit,
187        Tuple(u32, String),
188        Struct { x: u32, y: String },
189    }
190
191    #[test]
192    fn test_facet_iterator_struct() {
193        let facet = NestedFacet {
194            nested_field: TestFacet {
195                field1: 42,
196                field2: "Hello".to_string(),
197            },
198        };
199
200        let mut iter = facet.inspect();
201
202        let iterator_contains_field1 = iter.any(|(path, peek)| {
203            path == FacetPath::from("$.nested_field.field1")
204                && matches!(
205                    peek.partial_eq(&Peek::new(&facet.nested_field.field1)),
206                    Some(true)
207                )
208        });
209
210        let iterator_contains_field2 = iter.any(|(path, peek)| {
211            path == FacetPath::from("$.nested_field.field2")
212                && matches!(
213                    peek.partial_eq(&Peek::new(&facet.nested_field.field2)),
214                    Some(true)
215                )
216        });
217
218        assert!(iterator_contains_field1);
219        assert!(iterator_contains_field2);
220    }
221
222    #[test]
223    fn test_get_peek_by_path_struct() {
224        let facet = TestFacet {
225            field1: 42,
226            field2: "Hello".to_string(),
227        };
228
229        let peek1 = facet.get(&FacetPath::from("$.field1")).unwrap();
230        let peek2 = facet.get(&FacetPath::from("$.field2")).unwrap();
231
232        assert_eq!(peek1.partial_eq(&Peek::new(&facet.field1)), Some(true));
233        assert_eq!(peek2.partial_eq(&Peek::new(&facet.field2)), Some(true));
234    }
235
236    #[test]
237    fn test_facet_iterator_enum_unit() {
238        let facet = MyEnum::Unit;
239        let mut iter = facet.inspect();
240        // Should contain only the root path
241        assert!(iter.any(|(p, _)| p == FacetPath::from("$")));
242    }
243
244    #[test]
245    fn test_facet_iterator_enum_tuple() {
246        let facet = MyEnum::Tuple(42, "hello".to_string());
247        let mut iter = facet.inspect();
248        // Should contain root, and tuple fields
249        assert!(iter.any(|(p, peek)| p == FacetPath::from("$.0")
250            && peek.partial_eq(&Peek::new(&42)).unwrap_or(false)));
251        assert!(iter.any(|(p, peek)| {
252            p == FacetPath::from("$.1")
253                && peek
254                    .partial_eq(&Peek::new(&"hello".to_string()))
255                    .unwrap_or(false)
256        }));
257    }
258
259    #[test]
260    fn test_facet_iterator_enum_struct() {
261        let facet = MyEnum::Struct {
262            x: 7,
263            y: "abc".to_string(),
264        };
265        let mut iter = facet.inspect();
266
267        assert!(iter.any(|(p, peek)| p == FacetPath::from("$.x")
268            && peek.partial_eq(&Peek::new(&7)).unwrap_or(false)));
269        assert!(iter.any(|(p, peek)| {
270            p == FacetPath::from("$.y")
271                && peek
272                    .partial_eq(&Peek::new(&"abc".to_string()))
273                    .unwrap_or(false)
274        }));
275    }
276
277    #[test]
278    fn test_get_peek_by_path_enum_variants() {
279        // Struct variant
280        let facet = MyEnum::Struct {
281            x: 99,
282            y: "zzz".to_string(),
283        };
284        let peek_x = facet.get(&FacetPath::from("$.x")).unwrap();
285        let peek_y = facet.get(&FacetPath::from("$.y")).unwrap();
286        assert_eq!(peek_x.partial_eq(&Peek::new(&99)), Some(true));
287        assert_eq!(
288            peek_y.partial_eq(&Peek::new(&"zzz".to_string())),
289            Some(true)
290        );
291
292        // Tuple variant
293        let facet = MyEnum::Tuple(123, "tupleval".to_string());
294        let peek_0 = facet.get(&FacetPath::from("$.0")).unwrap();
295        let peek_1 = facet.get(&FacetPath::from("$.1")).unwrap();
296        assert_eq!(peek_0.partial_eq(&Peek::new(&123)), Some(true));
297        assert_eq!(
298            peek_1.partial_eq(&Peek::new(&"tupleval".to_string())),
299            Some(true)
300        );
301
302        // Unit variant (should only have root path)
303        let facet = MyEnum::Unit;
304        let peek_root = facet.get(&FacetPath::from("$")).unwrap();
305        // For unit, just check that we get a Peek and it matches itself
306        assert!(peek_root.partial_eq(&Peek::new(&facet)).unwrap_or(false));
307    }
308
309    #[test]
310    fn test_facet_iterator_array() {
311        let arr = [10, 20, 30];
312        let iter = arr.inspect();
313
314        let items: Vec<_> = iter.clone().collect();
315        dbg!(items);
316
317        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.0")
318            && peek.partial_eq(&Peek::new(&10)).unwrap_or(false)));
319        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.1")
320            && peek.partial_eq(&Peek::new(&20)).unwrap_or(false)));
321        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.2")
322            && peek.partial_eq(&Peek::new(&30)).unwrap_or(false)));
323    }
324
325    #[test]
326    fn test_facet_iterator_vec() {
327        let vec = vec!["a".to_string(), "b".to_string()];
328        let iter = vec.inspect();
329        assert!(iter.clone().any(|(p, peek)| {
330            p == FacetPath::from("$.0")
331                && peek
332                    .partial_eq(&Peek::new(&"a".to_string()))
333                    .unwrap_or(false)
334        }));
335        assert!(iter.clone().any(|(p, peek)| {
336            p == FacetPath::from("$.1")
337                && peek
338                    .partial_eq(&Peek::new(&"b".to_string()))
339                    .unwrap_or(false)
340        }));
341    }
342
343    #[test]
344    fn test_facet_iterator_slice() {
345        let slice: &[u32] = &[5, 6, 7];
346        let iter = slice.inspect();
347
348        dbg!(iter.clone().collect::<Vec<_>>());
349
350        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.0")
351            && peek.partial_eq(&Peek::new(&5)).unwrap_or(false)));
352        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.1")
353            && peek.partial_eq(&Peek::new(&6)).unwrap_or(false)));
354        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.2")
355            && peek.partial_eq(&Peek::new(&7)).unwrap_or(false)));
356    }
357
358    #[test]
359    fn test_facet_iterator_map() {
360        let mut map = HashMap::new();
361        map.insert("foo".to_string(), 123);
362        map.insert("bar".to_string(), 456);
363        let iter = map.inspect();
364        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.foo")
365            && peek.partial_eq(&Peek::new(&123)).unwrap_or(false)));
366        assert!(iter.clone().any(|(p, peek)| p == FacetPath::from("$.bar")
367            && peek.partial_eq(&Peek::new(&456)).unwrap_or(false)));
368    }
369
370    #[test]
371    fn test_get_peek_by_path_array_vec_slice_map() {
372        let arr = [1, 2, 3];
373        assert_eq!(
374            arr.get(&FacetPath::from("$.0"))
375                .unwrap()
376                .partial_eq(&Peek::new(&1)),
377            Some(true)
378        );
379        assert_eq!(
380            arr.get(&FacetPath::from("$.2"))
381                .unwrap()
382                .partial_eq(&Peek::new(&3)),
383            Some(true)
384        );
385
386        let vec = vec![9, 8, 7];
387        assert_eq!(
388            vec.get(&FacetPath::from("$.1"))
389                .unwrap()
390                .partial_eq(&Peek::new(&8)),
391            Some(true)
392        );
393
394        let slice: &[u32] = &[4, 5];
395        assert_eq!(
396            FacetInspect::get(&slice, &FacetPath::from("$.0"))
397                .unwrap()
398                .partial_eq(&Peek::new(&4)),
399            Some(true)
400        );
401
402        let mut map = HashMap::new();
403        map.insert("k1".to_string(), 11);
404        map.insert("k2".to_string(), 22);
405        assert_eq!(
406            FacetInspect::get(&map, &FacetPath::from("$.k1"))
407                .unwrap()
408                .partial_eq(&Peek::new(&11)),
409            Some(true)
410        );
411        assert_eq!(
412            FacetInspect::get(&map, &FacetPath::from("$.k2"))
413                .unwrap()
414                .partial_eq(&Peek::new(&22)),
415            Some(true)
416        );
417    }
418}