icydb_core/visitor/
validate.rs

1use crate::{
2    Error, ThisError,
3    traits::Visitable,
4    visitor::{Event, PathSegment, Visitor, VisitorError, perform_visit},
5};
6use icydb_error::ErrorTree;
7use std::fmt::Write;
8
9///
10/// ValidateError
11///
12
13#[derive(Debug, ThisError)]
14pub enum ValidateError {
15    #[error("validation failed: {0}")]
16    ValidationFailed(ErrorTree),
17}
18
19impl From<ValidateError> for Error {
20    fn from(err: ValidateError) -> Self {
21        VisitorError::from(err).into()
22    }
23}
24
25// validate
26/// Validate a visitable tree, collecting errors into an `ErrorTree`.
27pub fn validate(node: &dyn Visitable) -> Result<(), ValidateError> {
28    let mut visitor = ValidateVisitor::new();
29    perform_visit(&mut visitor, node, PathSegment::Empty);
30
31    visitor
32        .errors
33        .result()
34        .map_err(ValidateError::ValidationFailed)?;
35
36    Ok(())
37}
38
39///
40/// ValidateVisitor
41///
42
43#[derive(Debug, Default)]
44pub struct ValidateVisitor {
45    pub errors: ErrorTree,
46    pub path: Vec<PathSegment>,
47}
48
49impl ValidateVisitor {
50    #[must_use]
51    /// Create a validator with empty state.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    #[inline]
57    fn current_route(&self) -> String {
58        let mut out = String::new();
59        let mut first = true;
60
61        for seg in &self.path {
62            match seg {
63                PathSegment::Field(s) if !s.is_empty() => {
64                    if !first {
65                        out.push('.');
66                    }
67                    out.push_str(s);
68                    first = false;
69                }
70                PathSegment::Index(i) => {
71                    // indices shown as [0], [1], ...
72                    let _ = write!(out, "[{i}]");
73                    first = false;
74                }
75                _ => {}
76            }
77        }
78
79        out
80    }
81}
82
83impl Visitor for ValidateVisitor {
84    #[inline]
85    fn visit(&mut self, node: &dyn Visitable, event: Event) {
86        match event {
87            Event::Enter => {
88                let mut errs = ErrorTree::new();
89
90                // combine all validation types
91                // better to do it here and not in the trait
92                if let Err(e) = node.validate_self() {
93                    errs.merge(e);
94                }
95                if let Err(e) = node.validate_children() {
96                    errs.merge(e);
97                }
98                if let Err(e) = node.validate_custom() {
99                    errs.merge(e);
100                }
101
102                // check for errs
103                if !errs.is_empty() {
104                    if self.path.is_empty() {
105                        // At the current level, merge directly.
106                        self.errors.merge(errs);
107                    } else {
108                        // Add to a child entry under the computed route.
109                        let route = self.current_route();
110                        self.errors.children.entry(route).or_default().merge(errs);
111                    }
112                }
113            }
114            Event::Exit => {}
115        }
116    }
117
118    #[inline]
119    fn push(&mut self, seg: PathSegment) {
120        if !matches!(seg, PathSegment::Empty) {
121            self.path.push(seg);
122        }
123    }
124
125    #[inline]
126    fn pop(&mut self) {
127        self.path.pop();
128    }
129}
130
131///
132/// TESTS
133///
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::{
139        traits::{SanitizeAuto, SanitizeCustom, ValidateAuto, ValidateCustom, Visitable},
140        visitor::{perform_visit, validate},
141    };
142
143    const ERR_MSG: &str = "leaf error";
144
145    // A simple leaf type that can emit an error based on a flag.
146    #[derive(Clone, Debug, Default)]
147    struct Leaf(bool);
148
149    impl SanitizeAuto for Leaf {}
150    impl SanitizeCustom for Leaf {}
151    impl Visitable for Leaf {}
152    impl ValidateAuto for Leaf {}
153    impl ValidateCustom for Leaf {
154        fn validate_custom(&self) -> Result<(), ErrorTree> {
155            if self.0 {
156                Err(ErrorTree::from(ERR_MSG))
157            } else {
158                Ok(())
159            }
160        }
161    }
162
163    // Helper: get flattened errors from validate()
164    fn flatten_errs(node: &dyn Visitable) -> Vec<(String, String)> {
165        match validate(node) {
166            Ok(()) => Vec::new(),
167            Err(crate::visitor::ValidateError::ValidationFailed(tree)) => tree.flatten_ref(),
168        }
169    }
170
171    // Root-level error should attach to the root (empty route)
172    #[test]
173    fn root_level_error_is_at_root() {
174        let leaf = Leaf(true);
175        let flat = flatten_errs(&leaf);
176
177        assert!(flat.iter().any(|(k, v)| k.is_empty() && v == ERR_MSG));
178    }
179
180    // Container with a Vec field should index items: "field.1"
181    #[test]
182    fn record_field_vec_item_path_is_indexed() {
183        #[derive(Debug, Default)]
184        struct Container {
185            nums: Vec<Leaf>,
186        }
187
188        impl SanitizeAuto for Container {}
189        impl SanitizeCustom for Container {}
190        impl ValidateAuto for Container {}
191        impl ValidateCustom for Container {}
192        impl Visitable for Container {
193            fn drive(&self, visitor: &mut dyn Visitor) {
194                // Record field key then Vec indices
195                perform_visit(visitor, &self.nums, "nums");
196            }
197        }
198
199        let node = Container {
200            nums: vec![Leaf(false), Leaf(true), Leaf(false)],
201        };
202
203        let flat = flatten_errs(&node);
204        assert!(flat.iter().any(|(k, v)| k == "nums[1]" && v == ERR_MSG));
205    }
206
207    // Nested record, tuple-like, and map-like structures should produce dotted keys.
208    #[test]
209    fn nested_record_tuple_map_paths_are_dotted() {
210        // Inner record with a single leaf field
211        #[derive(Debug, Default)]
212        struct Inner {
213            leaf: Leaf,
214        }
215
216        impl SanitizeAuto for Inner {}
217        impl SanitizeCustom for Inner {}
218        impl ValidateAuto for Inner {}
219        impl ValidateCustom for Inner {}
220        impl Visitable for Inner {
221            fn drive(&self, visitor: &mut dyn Visitor) {
222                perform_visit(visitor, &self.leaf, "leaf");
223            }
224        }
225
226        // Tuple-like struct with two leaves; use indices "0", "1"
227        #[derive(Debug, Default)]
228        struct Tup2(Leaf, Leaf);
229
230        impl SanitizeAuto for Tup2 {}
231        impl SanitizeCustom for Tup2 {}
232        impl ValidateAuto for Tup2 {}
233        impl ValidateCustom for Tup2 {}
234        impl Visitable for Tup2 {
235            fn drive(&self, visitor: &mut dyn Visitor) {
236                perform_visit(visitor, &self.0, 0);
237                perform_visit(visitor, &self.1, 1);
238            }
239        }
240
241        // Simple map-like wrapper iterating key/value pairs
242        #[derive(Debug, Default)]
243        struct MyMap(Vec<(String, Leaf)>);
244
245        impl SanitizeAuto for MyMap {}
246        impl SanitizeCustom for MyMap {}
247        impl ValidateAuto for MyMap {}
248        impl ValidateCustom for MyMap {}
249        impl Visitable for MyMap {
250            fn drive(&self, visitor: &mut dyn Visitor) {
251                for (_k, v) in &self.0 {
252                    // Align with macro-generated map visitor: push "value"
253                    perform_visit(visitor, v, "value");
254                }
255            }
256        }
257
258        #[derive(Debug, Default)]
259        struct Outer {
260            rec: Inner,
261            tup: Tup2,
262            map: MyMap,
263        }
264
265        impl SanitizeAuto for Outer {}
266        impl SanitizeCustom for Outer {}
267        impl ValidateAuto for Outer {}
268        impl ValidateCustom for Outer {}
269        impl Visitable for Outer {
270            fn drive(&self, visitor: &mut dyn Visitor) {
271                perform_visit(visitor, &self.rec, "rec");
272                perform_visit(visitor, &self.tup, "tup");
273                perform_visit(visitor, &self.map, "map");
274            }
275        }
276
277        let node = Outer {
278            rec: Inner { leaf: Leaf(true) },
279            tup: Tup2(Leaf(false), Leaf(true)),
280            map: MyMap(vec![("k".to_string(), Leaf(true))]),
281        };
282
283        let flat = flatten_errs(&node);
284
285        // Expect errors at specific dotted paths
286        assert!(flat.iter().any(|(k, v)| k == "rec.leaf" && v == ERR_MSG));
287        assert!(flat.iter().any(|(k, v)| k == "tup[1]" && v == ERR_MSG));
288        assert!(flat.iter().any(|(k, v)| k == "map.value" && v == ERR_MSG));
289    }
290}