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