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