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