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