Skip to main content

icydb_model/visitor/
mod.rs

1//! Module: visitor
2//!
3//! Responsibility: generic normalize/validate visitor diagnostics and context.
4//! Does not own: schema-specific validation rules or session error mapping.
5//! Boundary: shared visitor error/context surface for derived normalizers and validators.
6
7#[macro_use]
8mod macros;
9mod traits;
10
11pub(crate) mod context;
12pub(crate) mod normalize;
13pub(crate) mod validate;
14
15use std::{collections::BTreeMap, fmt};
16use thiserror::Error as ThisError;
17
18// re-exports
19pub use context::{
20    ApplicationOperation, CallbackContext, CallbackIdentity, CallbackKind, Issue, PathSegment,
21    ScopedContext, VisitorContext,
22};
23pub use traits::{
24    Normalize, NormalizeAuto, NormalizeCustom, Normalizer, Validate, ValidateAuto, ValidateCustom,
25    Validator, Visitable,
26};
27
28//
29// VisitorError
30// Structured error type for visitor-based normalization and validation.
31//
32
33#[derive(Debug, ThisError)]
34#[error("{operation} failed: {issues}")]
35pub struct VisitorError {
36    operation: ApplicationOperation,
37    issues: VisitorIssues,
38}
39
40impl VisitorError {
41    pub(crate) const fn new(operation: ApplicationOperation, issues: VisitorIssues) -> Self {
42        Self { operation, issues }
43    }
44
45    /// Return whether normalization or validation produced this error.
46    #[must_use]
47    pub const fn operation(&self) -> ApplicationOperation {
48        self.operation
49    }
50
51    #[must_use]
52    pub const fn issues(&self) -> &VisitorIssues {
53        &self.issues
54    }
55}
56
57//
58// VisitorIssues
59// Aggregated visitor diagnostics.
60//
61// NOTE: This is not an error type. It does not represent failure.
62// It is converted into a `VisitorError` at the runtime boundary and
63// may be lifted into an `InternalError` as needed.
64//
65
66#[derive(Clone, Debug, Default, Eq, PartialEq)]
67pub struct VisitorIssues(BTreeMap<String, Vec<Issue>>);
68
69impl VisitorIssues {
70    #[must_use]
71    pub const fn new() -> Self {
72        Self(BTreeMap::new())
73    }
74
75    #[must_use]
76    pub fn is_empty(&self) -> bool {
77        self.0.is_empty()
78    }
79
80    /// Return the number of distinct issue paths.
81    #[must_use]
82    pub fn len(&self) -> usize {
83        self.0.len()
84    }
85
86    #[must_use]
87    pub fn get(&self, path: impl AsRef<str>) -> Option<&[Issue]> {
88        self.0.get(path.as_ref()).map(Vec::as_slice)
89    }
90
91    pub fn push(&mut self, path: String, issue: Issue) {
92        self.0.entry(path).or_default().push(issue);
93    }
94}
95
96impl From<BTreeMap<String, Vec<Issue>>> for VisitorIssues {
97    fn from(map: BTreeMap<String, Vec<Issue>>) -> Self {
98        Self(map)
99    }
100}
101
102impl fmt::Display for VisitorIssues {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        let mut wrote = false;
105
106        for (path, messages) in &self.0 {
107            for message in messages {
108                if wrote {
109                    writeln!(f)?;
110                }
111
112                if path.is_empty() {
113                    write!(f, "{message}")?;
114                } else {
115                    write!(f, "{path}: {message}")?;
116                }
117
118                wrote = true;
119            }
120        }
121
122        if !wrote {
123            write!(f, "no visitor issues")?;
124        }
125
126        Ok(())
127    }
128}
129
130impl std::error::Error for VisitorIssues {}
131
132//
133// Visitor
134// (immutable)
135//
136
137pub(crate) trait Visitor {
138    fn enter(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
139    fn exit(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
140}
141
142// ============================================================================
143// VisitorCore (object-safe traversal)
144// ============================================================================
145
146// Object-safe visitor contract for immutable traversal dispatch.
147pub trait VisitorCore {
148    fn enter(&mut self, node: &dyn Visitable);
149    fn exit(&mut self, node: &dyn Visitable);
150
151    fn push(&mut self, _: PathSegment) {}
152    fn pop(&mut self) {}
153}
154
155//
156// VisitableFieldDescriptor
157//
158// Runtime traversal descriptor for one generated struct field.
159// Generated code uses this to replace repeated per-field `drive` bodies with
160// one shared descriptor loop while preserving typed field access at the
161// boundary.
162//
163
164pub struct VisitableFieldDescriptor<T> {
165    name: &'static str,
166    drive: fn(&T, &mut dyn VisitorCore),
167    drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
168}
169
170impl<T> VisitableFieldDescriptor<T> {
171    /// Construct one traversal descriptor for one generated field.
172    #[must_use]
173    pub const fn new(
174        name: &'static str,
175        drive: fn(&T, &mut dyn VisitorCore),
176        drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
177    ) -> Self {
178        Self {
179            name,
180            drive,
181            drive_mut,
182        }
183    }
184
185    /// Return the field name carried by this descriptor.
186    #[must_use]
187    pub const fn name(&self) -> &'static str {
188        self.name
189    }
190}
191
192// Drive one generated field table through immutable visitor traversal.
193pub fn drive_visitable_fields<T>(
194    visitor: &mut dyn VisitorCore,
195    node: &T,
196    fields: &[VisitableFieldDescriptor<T>],
197) {
198    for field in fields {
199        (field.drive)(node, visitor);
200    }
201}
202
203// Drive one generated field table through mutable visitor traversal.
204pub fn drive_visitable_fields_mut<T>(
205    visitor: &mut dyn VisitorMutCore,
206    node: &mut T,
207    fields: &[VisitableFieldDescriptor<T>],
208) {
209    for field in fields {
210        (field.drive_mut)(node, visitor);
211    }
212}
213
214//
215// NormalizeFieldDescriptor
216//
217// Runtime normalization descriptor for one generated struct field.
218// Generated code uses this to replace repeated per-field `normalize_self`
219// bodies with one shared descriptor loop while preserving typed field access
220// at the boundary.
221//
222
223pub struct NormalizeFieldDescriptor<T> {
224    normalize: fn(&mut T, &mut dyn VisitorContext),
225}
226
227impl<T> NormalizeFieldDescriptor<T> {
228    /// Construct one normalization descriptor for one generated field.
229    #[must_use]
230    pub const fn new(normalize: fn(&mut T, &mut dyn VisitorContext)) -> Self {
231        Self { normalize }
232    }
233}
234
235// Drive one generated field table through normalization dispatch.
236pub fn drive_normalize_fields<T>(
237    node: &mut T,
238    ctx: &mut dyn VisitorContext,
239    fields: &[NormalizeFieldDescriptor<T>],
240) {
241    for field in fields {
242        (field.normalize)(node, ctx);
243    }
244}
245
246//
247// ValidateFieldDescriptor
248//
249// Runtime validation descriptor for one generated struct field.
250// Generated code uses this to replace repeated per-field `validate_self`
251// bodies with one shared descriptor loop while preserving typed field access
252// at the boundary.
253//
254
255pub struct ValidateFieldDescriptor<T> {
256    validate: fn(&T, &mut dyn VisitorContext),
257}
258
259impl<T> ValidateFieldDescriptor<T> {
260    /// Construct one validation descriptor for one generated field.
261    #[must_use]
262    pub const fn new(validate: fn(&T, &mut dyn VisitorContext)) -> Self {
263        Self { validate }
264    }
265}
266
267// Drive one generated field table through validation dispatch.
268pub fn drive_validate_fields<T>(
269    node: &T,
270    ctx: &mut dyn VisitorContext,
271    fields: &[ValidateFieldDescriptor<T>],
272) {
273    for field in fields {
274        (field.validate)(node, ctx);
275    }
276}
277
278// ============================================================================
279// Internal adapter context (fixes borrow checker)
280// ============================================================================
281
282struct AdapterContext<'a> {
283    path: &'a [PathSegment],
284    issues: &'a mut VisitorIssues,
285}
286
287impl VisitorContext for AdapterContext<'_> {
288    fn add_issue(&mut self, issue: Issue) {
289        let key = render_path(self.path, None);
290        self.issues.push(key, issue);
291    }
292
293    fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
294        let key = render_path(self.path, Some(seg));
295        self.issues.push(key, issue);
296    }
297}
298
299fn render_path(path: &[PathSegment], extra: Option<PathSegment>) -> String {
300    use std::fmt::Write;
301
302    let mut out = String::new();
303    let mut first = true;
304
305    let iter = path.iter().cloned().chain(extra);
306
307    for seg in iter {
308        match seg {
309            PathSegment::Field(s) => {
310                if !first {
311                    out.push('.');
312                }
313                out.push_str(s);
314                first = false;
315            }
316            PathSegment::Index(i) => {
317                let _ = write!(out, "[{i}]");
318                first = false;
319            }
320            PathSegment::Empty => {}
321        }
322    }
323
324    out
325}
326
327// ============================================================================
328// VisitorAdapter (immutable)
329// ============================================================================
330
331pub(crate) struct VisitorAdapter<V> {
332    visitor: V,
333    path: Vec<PathSegment>,
334    issues: VisitorIssues,
335}
336
337impl<V> VisitorAdapter<V>
338where
339    V: Visitor,
340{
341    pub(crate) const fn new(visitor: V) -> Self {
342        Self {
343            visitor,
344            path: Vec::new(),
345            issues: VisitorIssues::new(),
346        }
347    }
348
349    pub(crate) fn result(self) -> Result<(), VisitorIssues> {
350        if self.issues.is_empty() {
351            Ok(())
352        } else {
353            Err(self.issues)
354        }
355    }
356}
357
358impl<V> VisitorCore for VisitorAdapter<V>
359where
360    V: Visitor,
361{
362    fn push(&mut self, seg: PathSegment) {
363        if !matches!(seg, PathSegment::Empty) {
364            self.path.push(seg);
365        }
366    }
367
368    fn pop(&mut self) {
369        self.path.pop();
370    }
371
372    fn enter(&mut self, node: &dyn Visitable) {
373        let mut ctx = AdapterContext {
374            path: &self.path,
375            issues: &mut self.issues,
376        };
377        self.visitor.enter(node, &mut ctx);
378    }
379
380    fn exit(&mut self, node: &dyn Visitable) {
381        let mut ctx = AdapterContext {
382            path: &self.path,
383            issues: &mut self.issues,
384        };
385        self.visitor.exit(node, &mut ctx);
386    }
387}
388
389// ============================================================================
390// Traversal (immutable)
391// ============================================================================
392
393pub fn perform_visit<S: Into<PathSegment>>(
394    visitor: &mut dyn VisitorCore,
395    node: &dyn Visitable,
396    seg: S,
397) {
398    let seg = seg.into();
399    let should_push = !matches!(seg, PathSegment::Empty);
400
401    if should_push {
402        visitor.push(seg);
403    }
404
405    visitor.enter(node);
406    node.drive(visitor);
407    visitor.exit(node);
408
409    if should_push {
410        visitor.pop();
411    }
412}
413
414// ============================================================================
415// VisitorMut (mutable)
416// ============================================================================
417
418// Mutable visitor callbacks paired with a scoped visitor context.
419pub(crate) trait VisitorMut {
420    fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
421    fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
422}
423
424// ============================================================================
425// VisitorMutCore
426// ============================================================================
427
428// Object-safe mutable visitor contract used by traversal drivers.
429pub trait VisitorMutCore {
430    fn enter_mut(&mut self, node: &mut dyn Visitable);
431    fn exit_mut(&mut self, node: &mut dyn Visitable);
432
433    fn push(&mut self, _: PathSegment) {}
434    fn pop(&mut self) {}
435}
436
437// ============================================================================
438// VisitorMutAdapter
439// ============================================================================
440
441// Adapter that binds `VisitorMut` to object-safe traversal and path tracking.
442pub(crate) struct VisitorMutAdapter<V> {
443    visitor: V,
444    path: Vec<PathSegment>,
445    issues: VisitorIssues,
446}
447
448impl<V> VisitorMutAdapter<V>
449where
450    V: VisitorMut,
451{
452    pub(crate) const fn new(visitor: V) -> Self {
453        Self {
454            visitor,
455            path: Vec::new(),
456            issues: VisitorIssues::new(),
457        }
458    }
459
460    pub(crate) fn result(self) -> Result<(), VisitorIssues> {
461        if self.issues.is_empty() {
462            Ok(())
463        } else {
464            Err(self.issues)
465        }
466    }
467}
468
469impl<V> VisitorMutCore for VisitorMutAdapter<V>
470where
471    V: VisitorMut,
472{
473    fn push(&mut self, seg: PathSegment) {
474        if !matches!(seg, PathSegment::Empty) {
475            self.path.push(seg);
476        }
477    }
478
479    fn pop(&mut self) {
480        self.path.pop();
481    }
482
483    fn enter_mut(&mut self, node: &mut dyn Visitable) {
484        let mut ctx = AdapterContext {
485            path: &self.path,
486            issues: &mut self.issues,
487        };
488        self.visitor.enter_mut(node, &mut ctx);
489    }
490
491    fn exit_mut(&mut self, node: &mut dyn Visitable) {
492        let mut ctx = AdapterContext {
493            path: &self.path,
494            issues: &mut self.issues,
495        };
496        self.visitor.exit_mut(node, &mut ctx);
497    }
498}
499
500// ============================================================================
501// Traversal (mutable)
502// ============================================================================
503
504// Perform a mutable visitor traversal starting at a trait-object node.
505//
506// This is the *core* traversal entrypoint. It operates on `&mut dyn Visitable`
507// because visitor callbacks (`enter_mut` / `exit_mut`) require a trait object.
508//
509// Path segments are pushed/popped around the traversal unless the segment is
510// `PathSegment::Empty`.
511pub fn perform_visit_mut<S: Into<PathSegment>>(
512    visitor: &mut dyn VisitorMutCore,
513    node: &mut dyn Visitable,
514    seg: S,
515) {
516    let seg = seg.into();
517    let should_push = !matches!(seg, PathSegment::Empty);
518
519    if should_push {
520        visitor.push(seg);
521    }
522
523    visitor.enter_mut(node);
524    node.drive_mut(visitor);
525    visitor.exit_mut(node);
526
527    if should_push {
528        visitor.pop();
529    }
530}