Skip to main content

icydb_core/visitor/
mod.rs

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