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// Internal adapter context (fixes borrow checker)
146// ============================================================================
147
148struct AdapterContext<'a> {
149    path: &'a [PathSegment],
150    issues: &'a mut VisitorIssues,
151}
152
153impl VisitorContext for AdapterContext<'_> {
154    fn add_issue(&mut self, issue: Issue) {
155        let key = render_path(self.path, None);
156        self.issues
157            .entry(key)
158            .or_default()
159            .push(issue.into_message());
160    }
161
162    fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
163        let key = render_path(self.path, Some(seg));
164        self.issues
165            .entry(key)
166            .or_default()
167            .push(issue.into_message());
168    }
169}
170
171fn render_path(path: &[PathSegment], extra: Option<PathSegment>) -> String {
172    use std::fmt::Write;
173
174    let mut out = String::new();
175    let mut first = true;
176
177    let iter = path.iter().cloned().chain(extra);
178
179    for seg in iter {
180        match seg {
181            PathSegment::Field(s) => {
182                if !first {
183                    out.push('.');
184                }
185                out.push_str(s);
186                first = false;
187            }
188            PathSegment::Index(i) => {
189                let _ = write!(out, "[{i}]");
190                first = false;
191            }
192            PathSegment::Empty => {}
193        }
194    }
195
196    out
197}
198
199// ============================================================================
200// VisitorAdapter (immutable)
201// ============================================================================
202
203pub(crate) struct VisitorAdapter<V> {
204    visitor: V,
205    path: Vec<PathSegment>,
206    issues: VisitorIssues,
207}
208
209impl<V> VisitorAdapter<V>
210where
211    V: Visitor,
212{
213    pub(crate) const fn new(visitor: V) -> Self {
214        Self {
215            visitor,
216            path: Vec::new(),
217            issues: VisitorIssues::new(),
218        }
219    }
220
221    pub(crate) fn result(self) -> Result<(), VisitorIssues> {
222        if self.issues.is_empty() {
223            Ok(())
224        } else {
225            Err(self.issues)
226        }
227    }
228}
229
230impl<V> VisitorCore for VisitorAdapter<V>
231where
232    V: Visitor,
233{
234    fn push(&mut self, seg: PathSegment) {
235        if !matches!(seg, PathSegment::Empty) {
236            self.path.push(seg);
237        }
238    }
239
240    fn pop(&mut self) {
241        self.path.pop();
242    }
243
244    fn enter(&mut self, node: &dyn Visitable) {
245        let mut ctx = AdapterContext {
246            path: &self.path,
247            issues: &mut self.issues,
248        };
249        self.visitor.enter(node, &mut ctx);
250    }
251
252    fn exit(&mut self, node: &dyn Visitable) {
253        let mut ctx = AdapterContext {
254            path: &self.path,
255            issues: &mut self.issues,
256        };
257        self.visitor.exit(node, &mut ctx);
258    }
259}
260
261// ============================================================================
262// Traversal (immutable)
263// ============================================================================
264
265pub fn perform_visit<S: Into<PathSegment>>(
266    visitor: &mut dyn VisitorCore,
267    node: &dyn Visitable,
268    seg: S,
269) {
270    let seg = seg.into();
271    let should_push = !matches!(seg, PathSegment::Empty);
272
273    if should_push {
274        visitor.push(seg);
275    }
276
277    visitor.enter(node);
278    node.drive(visitor);
279    visitor.exit(node);
280
281    if should_push {
282        visitor.pop();
283    }
284}
285
286// ============================================================================
287// VisitorMut (mutable)
288// ============================================================================
289
290/// Mutable visitor callbacks paired with a scoped visitor context.
291pub(crate) trait VisitorMut {
292    fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
293    fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
294}
295
296// ============================================================================
297// VisitorMutCore
298// ============================================================================
299
300/// Object-safe mutable visitor contract used by traversal drivers.
301pub trait VisitorMutCore {
302    fn enter_mut(&mut self, node: &mut dyn Visitable);
303    fn exit_mut(&mut self, node: &mut dyn Visitable);
304
305    fn push(&mut self, _: PathSegment) {}
306    fn pop(&mut self) {}
307}
308
309// ============================================================================
310// VisitorMutAdapter
311// ============================================================================
312
313/// Adapter that binds `VisitorMut` to object-safe traversal and path tracking.
314pub(crate) struct VisitorMutAdapter<V> {
315    visitor: V,
316    path: Vec<PathSegment>,
317    issues: VisitorIssues,
318}
319
320impl<V> VisitorMutAdapter<V>
321where
322    V: VisitorMut,
323{
324    pub(crate) const fn new(visitor: V) -> Self {
325        Self {
326            visitor,
327            path: Vec::new(),
328            issues: VisitorIssues::new(),
329        }
330    }
331
332    pub(crate) fn result(self) -> Result<(), VisitorIssues> {
333        if self.issues.is_empty() {
334            Ok(())
335        } else {
336            Err(self.issues)
337        }
338    }
339}
340
341impl<V> VisitorMutCore for VisitorMutAdapter<V>
342where
343    V: VisitorMut,
344{
345    fn push(&mut self, seg: PathSegment) {
346        if !matches!(seg, PathSegment::Empty) {
347            self.path.push(seg);
348        }
349    }
350
351    fn pop(&mut self) {
352        self.path.pop();
353    }
354
355    fn enter_mut(&mut self, node: &mut dyn Visitable) {
356        let mut ctx = AdapterContext {
357            path: &self.path,
358            issues: &mut self.issues,
359        };
360        self.visitor.enter_mut(node, &mut ctx);
361    }
362
363    fn exit_mut(&mut self, node: &mut dyn Visitable) {
364        let mut ctx = AdapterContext {
365            path: &self.path,
366            issues: &mut self.issues,
367        };
368        self.visitor.exit_mut(node, &mut ctx);
369    }
370}
371
372// ============================================================================
373// Traversal (mutable)
374// ============================================================================
375
376/// Perform a mutable visitor traversal starting at a trait-object node.
377///
378/// This is the *core* traversal entrypoint. It operates on `&mut dyn Visitable`
379/// because visitor callbacks (`enter_mut` / `exit_mut`) require a trait object.
380///
381/// Path segments are pushed/popped around the traversal unless the segment is
382/// `PathSegment::Empty`.
383pub fn perform_visit_mut<S: Into<PathSegment>>(
384    visitor: &mut dyn VisitorMutCore,
385    node: &mut dyn Visitable,
386    seg: S,
387) {
388    let seg = seg.into();
389    let should_push = !matches!(seg, PathSegment::Empty);
390
391    if should_push {
392        visitor.push(seg);
393    }
394
395    visitor.enter_mut(node);
396    node.drive_mut(visitor);
397    visitor.exit_mut(node);
398
399    if should_push {
400        visitor.pop();
401    }
402}