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