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