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