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