icydb_core/visitor/
validate.rs1use 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#[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
25pub 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#[derive(Debug, Default)]
44pub struct ValidateVisitor {
45 pub errors: ErrorTree,
46 pub path: Vec<PathSegment>,
47}
48
49impl ValidateVisitor {
50 #[must_use]
51 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 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 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 if !errs.is_empty() {
104 if self.path.is_empty() {
105 self.errors.merge(errs);
107 } else {
108 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#[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 #[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 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 #[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 #[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 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 #[test]
209 fn nested_record_tuple_map_paths_are_dotted() {
210 #[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 #[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 #[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 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 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}