Skip to main content

icydb_model/visitor/
traits.rs

1//! Module: visitor::traits
2//! Responsibility: visitable-node traits and default container traversal wiring.
3//! Does not own: concrete normalize/validate visitor implementations.
4//! Boundary: structural traversal contract implemented by domain types.
5
6use crate::visitor::{
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 normalize/validate hooks.
20/// - `drive` / `drive_mut` describe *structure only*.
21/// - No validation or normalization logic lives here.
22pub trait Visitable: Normalize + 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 normalize 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> 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
74macro_rules! impl_primitive_visitable {
75    ($($ty:ty),* $(,)?) => {
76        $(impl Visitable for $ty {})*
77    };
78}
79
80impl_primitive_visitable!(
81    i8,
82    i16,
83    i32,
84    i64,
85    i128,
86    u8,
87    u16,
88    u32,
89    u64,
90    u128,
91    f32,
92    f64,
93    bool,
94    String,
95    crate::schema::Account,
96    crate::schema::Blob,
97    crate::schema::Date,
98    crate::schema::Decimal,
99    crate::schema::Duration,
100    crate::schema::Float32,
101    crate::schema::Float64,
102    crate::schema::IntBig,
103    crate::schema::NatBig,
104    crate::schema::Principal,
105    crate::schema::Subaccount,
106    crate::schema::Timestamp,
107    crate::schema::Ulid,
108    crate::schema::Unit,
109);
110
111//
112// ============================================================================
113// Normalize
114// ============================================================================
115//
116
117/// Marker trait: a type supports normalization.
118pub trait Normalize: NormalizeAuto + NormalizeCustom {}
119
120impl<T> Normalize for T where T: NormalizeAuto + NormalizeCustom {}
121
122//
123// -------------------- NormalizeAuto --------------------
124//
125
126/// Schema-defined normalization for this node only.
127///
128/// Rules:
129/// - May mutate only `self`
130/// - Must NOT recurse
131/// - Must NOT fail-fast
132/// - Must report issues via `VisitorContext`
133pub trait NormalizeAuto {
134    fn normalize_self(&mut self, _ctx: &mut dyn VisitorContext) {}
135}
136
137impl<T: NormalizeAuto> NormalizeAuto for Option<T> {}
138
139impl<T: NormalizeAuto> NormalizeAuto for Vec<T> {}
140
141impl<T: NormalizeAuto + ?Sized> NormalizeAuto for Box<T> {
142    fn normalize_self(&mut self, ctx: &mut dyn VisitorContext) {
143        (**self).normalize_self(ctx);
144    }
145}
146
147impl_primitive!(NormalizeAuto);
148
149//
150// -------------------- NormalizeCustom --------------------
151//
152
153/// User-defined normalization hooks.
154///
155/// Same rules as `NormalizeAuto`.
156pub trait NormalizeCustom {
157    fn normalize_custom(&mut self, _ctx: &mut dyn VisitorContext) {}
158}
159
160impl<T: NormalizeCustom> NormalizeCustom for Option<T> {}
161
162impl<T: NormalizeCustom> NormalizeCustom for Vec<T> {}
163
164impl<T: NormalizeCustom + ?Sized> NormalizeCustom for Box<T> {
165    fn normalize_custom(&mut self, ctx: &mut dyn VisitorContext) {
166        (**self).normalize_custom(ctx);
167    }
168}
169
170impl_primitive!(NormalizeCustom);
171
172//
173// ============================================================================
174// Validate
175// ============================================================================
176//
177
178/// Marker trait: a type supports validation.
179pub trait Validate: ValidateAuto + ValidateCustom {}
180
181impl<T> Validate for T where T: ValidateAuto + ValidateCustom {}
182
183//
184// -------------------- ValidateAuto --------------------
185//
186
187/// Schema-defined validation for this node only.
188///
189/// Rules:
190/// - Must NOT recurse
191/// - Must NOT aggregate
192/// - Must NOT return errors
193/// - Must report issues via `VisitorContext`
194pub trait ValidateAuto {
195    fn validate_self(&self, _ctx: &mut dyn VisitorContext) {}
196}
197
198impl<T: ValidateAuto> ValidateAuto for Option<T> {}
199
200impl<T: ValidateAuto> ValidateAuto for Vec<T> {}
201
202impl<T: ValidateAuto + ?Sized> ValidateAuto for Box<T> {
203    fn validate_self(&self, ctx: &mut dyn VisitorContext) {
204        (**self).validate_self(ctx);
205    }
206}
207
208impl_primitive!(ValidateAuto);
209
210//
211// -------------------- ValidateCustom --------------------
212//
213
214/// User-defined validation hooks.
215///
216/// Same rules as `ValidateAuto`.
217pub trait ValidateCustom {
218    fn validate_custom(&self, _ctx: &mut dyn VisitorContext) {}
219}
220
221impl<T: ValidateCustom> ValidateCustom for Option<T> {}
222
223impl<T: ValidateCustom> ValidateCustom for Vec<T> {}
224
225impl<T: ValidateCustom + ?Sized> ValidateCustom for Box<T> {
226    fn validate_custom(&self, ctx: &mut dyn VisitorContext) {
227        (**self).validate_custom(ctx);
228    }
229}
230
231impl_primitive!(ValidateCustom);
232
233/// Transforms a value into a normalized version.
234pub trait Normalizer<T> {
235    fn normalize(&self, value: &mut T) -> Result<(), String>;
236
237    fn normalize_with_context(
238        &self,
239        value: &mut T,
240        ctx: &mut dyn VisitorContext,
241    ) -> Result<(), String> {
242        let _ = ctx;
243
244        self.normalize(value)
245    }
246}
247
248/// Allows a node to validate values.
249pub trait Validator<T: ?Sized> {
250    fn validate(&self, value: &T, ctx: &mut dyn VisitorContext);
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::{
257        normalize::normalize,
258        validate::validate,
259        visitor::{Issue, VisitorError},
260    };
261    use std::cell::Cell;
262
263    const AUTO_NORMALIZE_ISSUE: &str = "automatic normalize";
264    const CUSTOM_NORMALIZE_ISSUE: &str = "custom normalize";
265    const AUTO_VALIDATE_ISSUE: &str = "automatic validate";
266    const CUSTOM_VALIDATE_ISSUE: &str = "custom validate";
267
268    #[derive(Default)]
269    struct HookProbe {
270        auto_normalize: u32,
271        custom_normalize: u32,
272        auto_validate: Cell<u32>,
273        custom_validate: Cell<u32>,
274    }
275
276    impl Visitable for HookProbe {}
277
278    impl NormalizeAuto for HookProbe {
279        fn normalize_self(&mut self, ctx: &mut dyn VisitorContext) {
280            self.auto_normalize += 1;
281            ctx.issue(AUTO_NORMALIZE_ISSUE);
282        }
283    }
284
285    impl NormalizeCustom for HookProbe {
286        fn normalize_custom(&mut self, ctx: &mut dyn VisitorContext) {
287            self.custom_normalize += 1;
288            ctx.issue(CUSTOM_NORMALIZE_ISSUE);
289        }
290    }
291
292    impl ValidateAuto for HookProbe {
293        fn validate_self(&self, ctx: &mut dyn VisitorContext) {
294            self.auto_validate.set(self.auto_validate.get() + 1);
295            ctx.issue(AUTO_VALIDATE_ISSUE);
296        }
297    }
298
299    impl ValidateCustom for HookProbe {
300        fn validate_custom(&self, ctx: &mut dyn VisitorContext) {
301            self.custom_validate.set(self.custom_validate.get() + 1);
302            ctx.issue(CUSTOM_VALIDATE_ISSUE);
303        }
304    }
305
306    fn assert_issues(error: &VisitorError, path: &str, expected: [&str; 2]) {
307        let issues = error
308            .issues()
309            .get(path)
310            .unwrap_or_else(|| panic!("expected visitor issues at {path}"));
311        let messages = issues.iter().map(Issue::message).collect::<Vec<_>>();
312        assert_eq!(messages, expected);
313    }
314
315    #[test]
316    fn option_vec_normalize_hooks_run_once_at_each_indexed_path() {
317        let mut value = Some(vec![HookProbe::default(), HookProbe::default()]);
318
319        let error = normalize(&mut value).expect_err("probe normalizers should report issues");
320
321        let Some(probes) = value.as_ref() else {
322            panic!("normalize should preserve the populated option");
323        };
324        for probe in probes {
325            assert_eq!(probe.auto_normalize, 1);
326            assert_eq!(probe.custom_normalize, 1);
327        }
328        assert!(error.issues().get("").is_none());
329        assert_issues(
330            &error,
331            "[0]",
332            [AUTO_NORMALIZE_ISSUE, CUSTOM_NORMALIZE_ISSUE],
333        );
334        assert_issues(
335            &error,
336            "[1]",
337            [AUTO_NORMALIZE_ISSUE, CUSTOM_NORMALIZE_ISSUE],
338        );
339    }
340
341    #[test]
342    fn option_vec_validate_hooks_run_once_at_each_indexed_path() {
343        let value = Some(vec![HookProbe::default(), HookProbe::default()]);
344
345        let error = validate(&value).expect_err("probe validators should report issues");
346
347        let Some(probes) = value.as_ref() else {
348            panic!("validate should preserve the populated option");
349        };
350        for probe in probes {
351            assert_eq!(probe.auto_validate.get(), 1);
352            assert_eq!(probe.custom_validate.get(), 1);
353        }
354        assert!(error.issues().get("").is_none());
355        assert_issues(&error, "[0]", [AUTO_VALIDATE_ISSUE, CUSTOM_VALIDATE_ISSUE]);
356        assert_issues(&error, "[1]", [AUTO_VALIDATE_ISSUE, CUSTOM_VALIDATE_ISSUE]);
357    }
358
359    #[test]
360    fn box_transparency_keeps_one_forwarded_hook_call() {
361        let mut normalized = Box::new(HookProbe::default());
362        let _ = normalize(&mut normalized).expect_err("probe normalizers should report issues");
363        assert_eq!(normalized.auto_normalize, 1);
364        assert_eq!(normalized.custom_normalize, 1);
365
366        let validated = Box::new(HookProbe::default());
367        let _ = validate(&validated).expect_err("probe validators should report issues");
368        assert_eq!(validated.auto_validate.get(), 1);
369        assert_eq!(validated.custom_validate.get(), 1);
370    }
371}