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 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 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 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 if !errs.is_empty() {
102 if self.path.is_empty() {
103 self.errors.merge(errs);
105 } else {
106 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#[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 #[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 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 #[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 #[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 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 #[test]
205 fn nested_record_tuple_map_paths_are_dotted() {
206 #[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 #[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 #[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 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 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}