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
26pub fn validate(node: &dyn Visitable) -> Result<(), ValidateError> {
27    let mut visitor = ValidateVisitor::new();
28    perform_visit(&mut visitor, node, PathSegment::Empty);
29
30    visitor
31        .errors
32        .result()
33        .map_err(ValidateError::ValidationFailed)?;
34
35    Ok(())
36}
37
38///
39/// ValidateVisitor
40///
41
42#[derive(Debug, Default)]
43pub struct ValidateVisitor {
44    pub errors: ErrorTree,
45    pub path: Vec<PathSegment>,
46}
47
48impl ValidateVisitor {
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    #[inline]
55    fn current_route(&self) -> String {
56        let mut out = String::new();
57        let mut first = true;
58
59        for seg in &self.path {
60            match seg {
61                PathSegment::Field(s) if !s.is_empty() => {
62                    if !first {
63                        out.push('.');
64                    }
65                    out.push_str(s);
66                    first = false;
67                }
68                PathSegment::Index(i) => {
69                    // indices shown as [0], [1], ...
70                    let _ = write!(out, "[{i}]");
71                    first = false;
72                }
73                _ => {}
74            }
75        }
76
77        out
78    }
79}
80
81impl Visitor for ValidateVisitor {
82    #[inline]
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    #[inline]
117    fn push(&mut self, seg: PathSegment) {
118        if !matches!(seg, PathSegment::Empty) {
119            self.path.push(seg);
120        }
121    }
122
123    #[inline]
124    fn pop(&mut self) {
125        self.path.pop();
126    }
127}
128
129///
130/// TESTS
131///
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::{
137        traits::{SanitizeAuto, SanitizeCustom, ValidateAuto, ValidateCustom, Visitable},
138        visitor::{perform_visit, validate},
139    };
140
141    const ERR_MSG: &str = "leaf error";
142
143    // A simple leaf type that can emit an error based on a flag.
144    #[derive(Clone, Debug, Default)]
145    struct Leaf(bool);
146
147    impl SanitizeAuto for Leaf {}
148    impl SanitizeCustom for Leaf {}
149    impl Visitable for Leaf {}
150    impl ValidateAuto for Leaf {}
151    impl ValidateCustom for Leaf {
152        fn validate_custom(&self) -> Result<(), ErrorTree> {
153            if self.0 {
154                Err(ErrorTree::from(ERR_MSG))
155            } else {
156                Ok(())
157            }
158        }
159    }
160
161    // Helper: get flattened errors from validate()
162    fn flatten_errs(node: &dyn Visitable) -> Vec<(String, String)> {
163        match validate(node) {
164            Ok(()) => Vec::new(),
165            Err(crate::visitor::ValidateError::ValidationFailed(tree)) => tree.flatten_ref(),
166        }
167    }
168
169    // Root-level error should attach to the root (empty route)
170    #[test]
171    fn root_level_error_is_at_root() {
172        let leaf = Leaf(true);
173        let flat = flatten_errs(&leaf);
174
175        assert!(flat.iter().any(|(k, v)| k.is_empty() && v == ERR_MSG));
176    }
177
178    // Container with a Vec field should index items: "field.1"
179    #[test]
180    fn record_field_vec_item_path_is_indexed() {
181        #[derive(Debug, Default)]
182        struct Container {
183            nums: Vec<Leaf>,
184        }
185
186        impl SanitizeAuto for Container {}
187        impl SanitizeCustom for Container {}
188        impl ValidateAuto for Container {}
189        impl ValidateCustom for Container {}
190        impl Visitable for Container {
191            fn drive(&self, visitor: &mut dyn Visitor) {
192                // Record field key then Vec indices
193                perform_visit(visitor, &self.nums, "nums");
194            }
195        }
196
197        let node = Container {
198            nums: vec![Leaf(false), Leaf(true), Leaf(false)],
199        };
200
201        let flat = flatten_errs(&node);
202        assert!(flat.iter().any(|(k, v)| k == "nums[1]" && v == ERR_MSG));
203    }
204
205    // Nested record, tuple-like, and map-like structures should produce dotted keys.
206    #[test]
207    fn nested_record_tuple_map_paths_are_dotted() {
208        // Inner record with a single leaf field
209        #[derive(Debug, Default)]
210        struct Inner {
211            leaf: Leaf,
212        }
213
214        impl SanitizeAuto for Inner {}
215        impl SanitizeCustom for Inner {}
216        impl ValidateAuto for Inner {}
217        impl ValidateCustom for Inner {}
218        impl Visitable for Inner {
219            fn drive(&self, visitor: &mut dyn Visitor) {
220                perform_visit(visitor, &self.leaf, "leaf");
221            }
222        }
223
224        // Tuple-like struct with two leaves; use indices "0", "1"
225        #[derive(Debug, Default)]
226        struct Tup2(Leaf, Leaf);
227
228        impl SanitizeAuto for Tup2 {}
229        impl SanitizeCustom for Tup2 {}
230        impl ValidateAuto for Tup2 {}
231        impl ValidateCustom for Tup2 {}
232        impl Visitable for Tup2 {
233            fn drive(&self, visitor: &mut dyn Visitor) {
234                perform_visit(visitor, &self.0, 0);
235                perform_visit(visitor, &self.1, 1);
236            }
237        }
238
239        // Simple map-like wrapper iterating key/value pairs
240        #[derive(Debug, Default)]
241        struct MyMap(Vec<(String, Leaf)>);
242
243        impl SanitizeAuto for MyMap {}
244        impl SanitizeCustom for MyMap {}
245        impl ValidateAuto for MyMap {}
246        impl ValidateCustom for MyMap {}
247        impl Visitable for MyMap {
248            fn drive(&self, visitor: &mut dyn Visitor) {
249                for (_k, v) in &self.0 {
250                    // Align with macro-generated map visitor: push "value"
251                    perform_visit(visitor, v, "value");
252                }
253            }
254        }
255
256        #[derive(Debug, Default)]
257        struct Outer {
258            rec: Inner,
259            tup: Tup2,
260            map: MyMap,
261        }
262
263        impl SanitizeAuto for Outer {}
264        impl SanitizeCustom for Outer {}
265        impl ValidateAuto for Outer {}
266        impl ValidateCustom for Outer {}
267        impl Visitable for Outer {
268            fn drive(&self, visitor: &mut dyn Visitor) {
269                perform_visit(visitor, &self.rec, "rec");
270                perform_visit(visitor, &self.tup, "tup");
271                perform_visit(visitor, &self.map, "map");
272            }
273        }
274
275        let node = Outer {
276            rec: Inner { leaf: Leaf(true) },
277            tup: Tup2(Leaf(false), Leaf(true)),
278            map: MyMap(vec![("k".to_string(), Leaf(true))]),
279        };
280
281        let flat = flatten_errs(&node);
282
283        // Expect errors at specific dotted paths
284        assert!(flat.iter().any(|(k, v)| k == "rec.leaf" && v == ERR_MSG));
285        assert!(flat.iter().any(|(k, v)| k == "tup[1]" && v == ERR_MSG));
286        assert!(flat.iter().any(|(k, v)| k == "map.value" && v == ERR_MSG));
287    }
288}