Skip to main content

icydb_core/visitor/
traits.rs

1//! Module: visitor::traits
2//! Responsibility: visitable-node traits and default container traversal wiring.
3//! Does not own: concrete sanitize/validate visitor implementations.
4//! Boundary: structural traversal contract implemented by domain types.
5
6use super::{
7    PathSegment, VisitorContext, VisitorCore, VisitorMutCore, perform_visit, perform_visit_mut,
8};
9
10//
11// ============================================================================
12// Visitable
13// ============================================================================
14//
15
16/// A node that participates in visitor-based traversal.
17///
18/// Invariants:
19/// - Traversal is owned by the visitor, not by sanitize/validate hooks.
20/// - `drive` / `drive_mut` describe *structure only*.
21/// - No validation or sanitization logic lives here.
22pub trait Visitable: Sanitize + Validate {
23    fn drive(&self, _: &mut dyn VisitorCore) {}
24    fn drive_mut(&mut self, _: &mut dyn VisitorMutCore) {}
25}
26
27//
28// -------------------- Container forwarding --------------------
29//
30
31// `Option` and `Vec` describe child structure here; their sanitize and
32// validate hooks remain node-local no-ops. `Box` is transparent instead, so
33// its hook forwarding supplies the boxed node's one logical hook call.
34
35impl<T: Visitable> Visitable for Option<T> {
36    fn drive(&self, visitor: &mut dyn VisitorCore) {
37        if let Some(value) = self.as_ref() {
38            perform_visit(visitor, value, PathSegment::Empty);
39        }
40    }
41
42    fn drive_mut(&mut self, visitor: &mut dyn VisitorMutCore) {
43        if let Some(value) = self.as_mut() {
44            perform_visit_mut(visitor, value, PathSegment::Empty);
45        }
46    }
47}
48
49impl<T: Visitable> Visitable for Vec<T> {
50    fn drive(&self, visitor: &mut dyn VisitorCore) {
51        for (i, value) in self.iter().enumerate() {
52            perform_visit(visitor, value, i);
53        }
54    }
55
56    fn drive_mut(&mut self, visitor: &mut dyn VisitorMutCore) {
57        for (i, value) in self.iter_mut().enumerate() {
58            perform_visit_mut(visitor, value, i);
59        }
60    }
61}
62
63impl<T: Visitable + ?Sized> Visitable for Box<T> {
64    fn drive(&self, visitor: &mut dyn VisitorCore) {
65        (**self).drive(visitor);
66    }
67
68    fn drive_mut(&mut self, visitor: &mut dyn VisitorMutCore) {
69        (**self).drive_mut(visitor);
70    }
71}
72
73// Primitive leaf nodes: no structure
74impl_primitive!(Visitable);
75
76//
77// ============================================================================
78// Sanitize
79// ============================================================================
80//
81
82/// Marker trait: a type supports sanitization.
83pub trait Sanitize: SanitizeAuto + SanitizeCustom {}
84
85impl<T> Sanitize for T where T: SanitizeAuto + SanitizeCustom {}
86
87//
88// -------------------- SanitizeAuto --------------------
89//
90
91/// Schema-defined sanitization for this node only.
92///
93/// Rules:
94/// - May mutate only `self`
95/// - Must NOT recurse
96/// - Must NOT fail-fast
97/// - Must report issues via `VisitorContext`
98pub trait SanitizeAuto {
99    fn sanitize_self(&mut self, _ctx: &mut dyn VisitorContext) {}
100}
101
102impl<T: SanitizeAuto> SanitizeAuto for Option<T> {}
103
104impl<T: SanitizeAuto> SanitizeAuto for Vec<T> {}
105
106impl<T: SanitizeAuto + ?Sized> SanitizeAuto for Box<T> {
107    fn sanitize_self(&mut self, ctx: &mut dyn VisitorContext) {
108        (**self).sanitize_self(ctx);
109    }
110}
111
112impl_primitive!(SanitizeAuto);
113
114//
115// -------------------- SanitizeCustom --------------------
116//
117
118/// User-defined sanitization hooks.
119///
120/// Same rules as `SanitizeAuto`.
121pub trait SanitizeCustom {
122    fn sanitize_custom(&mut self, _ctx: &mut dyn VisitorContext) {}
123}
124
125impl<T: SanitizeCustom> SanitizeCustom for Option<T> {}
126
127impl<T: SanitizeCustom> SanitizeCustom for Vec<T> {}
128
129impl<T: SanitizeCustom + ?Sized> SanitizeCustom for Box<T> {
130    fn sanitize_custom(&mut self, ctx: &mut dyn VisitorContext) {
131        (**self).sanitize_custom(ctx);
132    }
133}
134
135impl_primitive!(SanitizeCustom);
136
137//
138// ============================================================================
139// Validate
140// ============================================================================
141//
142
143/// Marker trait: a type supports validation.
144pub trait Validate: ValidateAuto + ValidateCustom {}
145
146impl<T> Validate for T where T: ValidateAuto + ValidateCustom {}
147
148//
149// -------------------- ValidateAuto --------------------
150//
151
152/// Schema-defined validation for this node only.
153///
154/// Rules:
155/// - Must NOT recurse
156/// - Must NOT aggregate
157/// - Must NOT return errors
158/// - Must report issues via `VisitorContext`
159pub trait ValidateAuto {
160    fn validate_self(&self, _ctx: &mut dyn VisitorContext) {}
161}
162
163impl<T: ValidateAuto> ValidateAuto for Option<T> {}
164
165impl<T: ValidateAuto> ValidateAuto for Vec<T> {}
166
167impl<T: ValidateAuto + ?Sized> ValidateAuto for Box<T> {
168    fn validate_self(&self, ctx: &mut dyn VisitorContext) {
169        (**self).validate_self(ctx);
170    }
171}
172
173impl_primitive!(ValidateAuto);
174
175//
176// -------------------- ValidateCustom --------------------
177//
178
179/// User-defined validation hooks.
180///
181/// Same rules as `ValidateAuto`.
182pub trait ValidateCustom {
183    fn validate_custom(&self, _ctx: &mut dyn VisitorContext) {}
184}
185
186impl<T: ValidateCustom> ValidateCustom for Option<T> {}
187
188impl<T: ValidateCustom> ValidateCustom for Vec<T> {}
189
190impl<T: ValidateCustom + ?Sized> ValidateCustom for Box<T> {
191    fn validate_custom(&self, ctx: &mut dyn VisitorContext) {
192        (**self).validate_custom(ctx);
193    }
194}
195
196impl_primitive!(ValidateCustom);
197
198/// Transforms a value into a sanitized version.
199pub trait Sanitizer<T> {
200    fn sanitize(&self, value: &mut T) -> Result<(), String>;
201
202    fn sanitize_with_context(
203        &self,
204        value: &mut T,
205        ctx: &mut dyn VisitorContext,
206    ) -> Result<(), String> {
207        let _ = ctx;
208
209        self.sanitize(value)
210    }
211}
212
213/// Allows a node to validate values.
214pub trait Validator<T: ?Sized> {
215    fn validate(&self, value: &T, ctx: &mut dyn VisitorContext);
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::{
222        sanitize::sanitize,
223        validate::validate,
224        visitor::{Issue, VisitorError},
225    };
226    use std::cell::Cell;
227
228    const AUTO_SANITIZE_ISSUE: &str = "automatic sanitize";
229    const CUSTOM_SANITIZE_ISSUE: &str = "custom sanitize";
230    const AUTO_VALIDATE_ISSUE: &str = "automatic validate";
231    const CUSTOM_VALIDATE_ISSUE: &str = "custom validate";
232
233    #[derive(Default)]
234    struct HookProbe {
235        auto_sanitize: u32,
236        custom_sanitize: u32,
237        auto_validate: Cell<u32>,
238        custom_validate: Cell<u32>,
239    }
240
241    impl Visitable for HookProbe {}
242
243    impl SanitizeAuto for HookProbe {
244        fn sanitize_self(&mut self, ctx: &mut dyn VisitorContext) {
245            self.auto_sanitize += 1;
246            ctx.issue(AUTO_SANITIZE_ISSUE);
247        }
248    }
249
250    impl SanitizeCustom for HookProbe {
251        fn sanitize_custom(&mut self, ctx: &mut dyn VisitorContext) {
252            self.custom_sanitize += 1;
253            ctx.issue(CUSTOM_SANITIZE_ISSUE);
254        }
255    }
256
257    impl ValidateAuto for HookProbe {
258        fn validate_self(&self, ctx: &mut dyn VisitorContext) {
259            self.auto_validate.set(self.auto_validate.get() + 1);
260            ctx.issue(AUTO_VALIDATE_ISSUE);
261        }
262    }
263
264    impl ValidateCustom for HookProbe {
265        fn validate_custom(&self, ctx: &mut dyn VisitorContext) {
266            self.custom_validate.set(self.custom_validate.get() + 1);
267            ctx.issue(CUSTOM_VALIDATE_ISSUE);
268        }
269    }
270
271    fn assert_issues(error: &VisitorError, path: &str, expected: [&str; 2]) {
272        let issues = error
273            .issues()
274            .get(path)
275            .unwrap_or_else(|| panic!("expected visitor issues at {path}"));
276        let messages = issues.iter().map(Issue::message).collect::<Vec<_>>();
277        assert_eq!(messages, expected);
278    }
279
280    #[test]
281    fn option_vec_sanitize_hooks_run_once_at_each_indexed_path() {
282        let mut value = Some(vec![HookProbe::default(), HookProbe::default()]);
283
284        let error = sanitize(&mut value).expect_err("probe sanitizers should report issues");
285
286        let Some(probes) = value.as_ref() else {
287            panic!("sanitize should preserve the populated option");
288        };
289        for probe in probes {
290            assert_eq!(probe.auto_sanitize, 1);
291            assert_eq!(probe.custom_sanitize, 1);
292        }
293        assert!(error.issues().get("").is_none());
294        assert_issues(&error, "[0]", [AUTO_SANITIZE_ISSUE, CUSTOM_SANITIZE_ISSUE]);
295        assert_issues(&error, "[1]", [AUTO_SANITIZE_ISSUE, CUSTOM_SANITIZE_ISSUE]);
296    }
297
298    #[test]
299    fn option_vec_validate_hooks_run_once_at_each_indexed_path() {
300        let value = Some(vec![HookProbe::default(), HookProbe::default()]);
301
302        let error = validate(&value).expect_err("probe validators should report issues");
303
304        let Some(probes) = value.as_ref() else {
305            panic!("validate should preserve the populated option");
306        };
307        for probe in probes {
308            assert_eq!(probe.auto_validate.get(), 1);
309            assert_eq!(probe.custom_validate.get(), 1);
310        }
311        assert!(error.issues().get("").is_none());
312        assert_issues(&error, "[0]", [AUTO_VALIDATE_ISSUE, CUSTOM_VALIDATE_ISSUE]);
313        assert_issues(&error, "[1]", [AUTO_VALIDATE_ISSUE, CUSTOM_VALIDATE_ISSUE]);
314    }
315
316    #[test]
317    fn box_transparency_keeps_one_forwarded_hook_call() {
318        let mut sanitized = Box::new(HookProbe::default());
319        let _ = sanitize(&mut sanitized).expect_err("probe sanitizers should report issues");
320        assert_eq!(sanitized.auto_sanitize, 1);
321        assert_eq!(sanitized.custom_sanitize, 1);
322
323        let validated = Box::new(HookProbe::default());
324        let _ = validate(&validated).expect_err("probe validators should report issues");
325        assert_eq!(validated.auto_validate.get(), 1);
326        assert_eq!(validated.custom_validate.get(), 1);
327    }
328}