Skip to main content

icydb_core/visitor/
mod.rs

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