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::{error::InternalError, traits::Visitable};
12use candid::CandidType;
13use derive_more::{Deref, DerefMut};
14use serde::{Deserialize, Serialize};
15use std::{collections::BTreeMap, fmt};
16use thiserror::Error as ThisError;
17
18// re-exports
19pub use context::{Issue, PathSegment, ScopedContext, VisitorContext};
20
21///
22/// VisitorError
23/// Structured error type for visitor-based sanitization and validation.
24///
25
26#[derive(Debug, ThisError)]
27#[error("{issues}")]
28pub struct VisitorError {
29    issues: VisitorIssues,
30}
31
32impl VisitorError {
33    #[must_use]
34    pub const fn issues(&self) -> &VisitorIssues {
35        &self.issues
36    }
37}
38
39impl From<VisitorIssues> for VisitorError {
40    fn from(issues: VisitorIssues) -> Self {
41        Self { issues }
42    }
43}
44
45impl From<VisitorError> for VisitorIssues {
46    fn from(err: VisitorError) -> Self {
47        err.issues
48    }
49}
50
51impl From<VisitorError> for InternalError {
52    fn from(err: VisitorError) -> Self {
53        Self::executor_unsupported(err.to_string())
54    }
55}
56
57///
58/// VisitorIssues
59/// Aggregated visitor diagnostics.
60///
61/// NOTE: This is not an error type. It does not represent failure.
62/// It is converted into a `VisitorError` at the runtime boundary and
63/// may be lifted into an `InternalError` as needed.
64///
65
66#[derive(
67    Clone, Debug, Default, Deserialize, Deref, DerefMut, Serialize, CandidType, Eq, PartialEq,
68)]
69pub struct VisitorIssues(BTreeMap<String, Vec<String>>);
70
71impl VisitorIssues {
72    #[must_use]
73    pub const fn new() -> Self {
74        Self(BTreeMap::new())
75    }
76}
77
78impl From<BTreeMap<String, Vec<String>>> for VisitorIssues {
79    fn from(map: BTreeMap<String, Vec<String>>) -> Self {
80        Self(map)
81    }
82}
83
84impl fmt::Display for VisitorIssues {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        let mut wrote = false;
87
88        for (path, messages) in &self.0 {
89            for message in messages {
90                if wrote {
91                    writeln!(f)?;
92                }
93
94                if path.is_empty() {
95                    write!(f, "{message}")?;
96                } else {
97                    write!(f, "{path}: {message}")?;
98                }
99
100                wrote = true;
101            }
102        }
103
104        if !wrote {
105            write!(f, "no visitor issues")?;
106        }
107
108        Ok(())
109    }
110}
111
112impl std::error::Error for VisitorIssues {}
113
114///
115/// Visitor
116/// (immutable)
117///
118
119pub(crate) trait Visitor {
120    fn enter(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
121    fn exit(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
122}
123
124// ============================================================================
125// VisitorCore (object-safe traversal)
126// ============================================================================
127
128/// Object-safe visitor contract for immutable traversal dispatch.
129pub trait VisitorCore {
130    fn enter(&mut self, node: &dyn Visitable);
131    fn exit(&mut self, node: &dyn Visitable);
132
133    fn push(&mut self, _: PathSegment) {}
134    fn pop(&mut self) {}
135}
136
137// ============================================================================
138// Internal adapter context (fixes borrow checker)
139// ============================================================================
140
141struct AdapterContext<'a> {
142    path: &'a [PathSegment],
143    issues: &'a mut VisitorIssues,
144}
145
146impl VisitorContext for AdapterContext<'_> {
147    fn add_issue(&mut self, issue: Issue) {
148        let key = render_path(self.path, None);
149        self.issues
150            .entry(key)
151            .or_default()
152            .push(issue.into_message());
153    }
154
155    fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
156        let key = render_path(self.path, Some(seg));
157        self.issues
158            .entry(key)
159            .or_default()
160            .push(issue.into_message());
161    }
162}
163
164fn render_path(path: &[PathSegment], extra: Option<PathSegment>) -> String {
165    use std::fmt::Write;
166
167    let mut out = String::new();
168    let mut first = true;
169
170    let iter = path.iter().cloned().chain(extra);
171
172    for seg in iter {
173        match seg {
174            PathSegment::Field(s) => {
175                if !first {
176                    out.push('.');
177                }
178                out.push_str(s);
179                first = false;
180            }
181            PathSegment::Index(i) => {
182                let _ = write!(out, "[{i}]");
183                first = false;
184            }
185            PathSegment::Empty => {}
186        }
187    }
188
189    out
190}
191
192// ============================================================================
193// VisitorAdapter (immutable)
194// ============================================================================
195
196pub(crate) struct VisitorAdapter<V> {
197    visitor: V,
198    path: Vec<PathSegment>,
199    issues: VisitorIssues,
200}
201
202impl<V> VisitorAdapter<V>
203where
204    V: Visitor,
205{
206    pub(crate) const fn new(visitor: V) -> Self {
207        Self {
208            visitor,
209            path: Vec::new(),
210            issues: VisitorIssues::new(),
211        }
212    }
213
214    pub(crate) fn result(self) -> Result<(), VisitorIssues> {
215        if self.issues.is_empty() {
216            Ok(())
217        } else {
218            Err(self.issues)
219        }
220    }
221}
222
223impl<V> VisitorCore for VisitorAdapter<V>
224where
225    V: Visitor,
226{
227    fn push(&mut self, seg: PathSegment) {
228        if !matches!(seg, PathSegment::Empty) {
229            self.path.push(seg);
230        }
231    }
232
233    fn pop(&mut self) {
234        self.path.pop();
235    }
236
237    fn enter(&mut self, node: &dyn Visitable) {
238        let mut ctx = AdapterContext {
239            path: &self.path,
240            issues: &mut self.issues,
241        };
242        self.visitor.enter(node, &mut ctx);
243    }
244
245    fn exit(&mut self, node: &dyn Visitable) {
246        let mut ctx = AdapterContext {
247            path: &self.path,
248            issues: &mut self.issues,
249        };
250        self.visitor.exit(node, &mut ctx);
251    }
252}
253
254// ============================================================================
255// Traversal (immutable)
256// ============================================================================
257
258pub fn perform_visit<S: Into<PathSegment>>(
259    visitor: &mut dyn VisitorCore,
260    node: &dyn Visitable,
261    seg: S,
262) {
263    let seg = seg.into();
264    let should_push = !matches!(seg, PathSegment::Empty);
265
266    if should_push {
267        visitor.push(seg);
268    }
269
270    visitor.enter(node);
271    node.drive(visitor);
272    visitor.exit(node);
273
274    if should_push {
275        visitor.pop();
276    }
277}
278
279// ============================================================================
280// VisitorMut (mutable)
281// ============================================================================
282
283/// Mutable visitor callbacks paired with a scoped visitor context.
284pub(crate) trait VisitorMut {
285    fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
286    fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
287}
288
289// ============================================================================
290// VisitorMutCore
291// ============================================================================
292
293/// Object-safe mutable visitor contract used by traversal drivers.
294pub trait VisitorMutCore {
295    fn enter_mut(&mut self, node: &mut dyn Visitable);
296    fn exit_mut(&mut self, node: &mut dyn Visitable);
297
298    fn push(&mut self, _: PathSegment) {}
299    fn pop(&mut self) {}
300}
301
302// ============================================================================
303// VisitorMutAdapter
304// ============================================================================
305
306/// Adapter that binds `VisitorMut` to object-safe traversal and path tracking.
307pub(crate) struct VisitorMutAdapter<V> {
308    visitor: V,
309    path: Vec<PathSegment>,
310    issues: VisitorIssues,
311}
312
313impl<V> VisitorMutAdapter<V>
314where
315    V: VisitorMut,
316{
317    pub(crate) const fn new(visitor: V) -> Self {
318        Self {
319            visitor,
320            path: Vec::new(),
321            issues: VisitorIssues::new(),
322        }
323    }
324
325    pub(crate) fn result(self) -> Result<(), VisitorIssues> {
326        if self.issues.is_empty() {
327            Ok(())
328        } else {
329            Err(self.issues)
330        }
331    }
332}
333
334impl<V> VisitorMutCore for VisitorMutAdapter<V>
335where
336    V: VisitorMut,
337{
338    fn push(&mut self, seg: PathSegment) {
339        if !matches!(seg, PathSegment::Empty) {
340            self.path.push(seg);
341        }
342    }
343
344    fn pop(&mut self) {
345        self.path.pop();
346    }
347
348    fn enter_mut(&mut self, node: &mut dyn Visitable) {
349        let mut ctx = AdapterContext {
350            path: &self.path,
351            issues: &mut self.issues,
352        };
353        self.visitor.enter_mut(node, &mut ctx);
354    }
355
356    fn exit_mut(&mut self, node: &mut dyn Visitable) {
357        let mut ctx = AdapterContext {
358            path: &self.path,
359            issues: &mut self.issues,
360        };
361        self.visitor.exit_mut(node, &mut ctx);
362    }
363}
364
365// ============================================================================
366// Traversal (mutable)
367// ============================================================================
368
369/// Perform a mutable visitor traversal starting at a trait-object node.
370///
371/// This is the *core* traversal entrypoint. It operates on `&mut dyn Visitable`
372/// because visitor callbacks (`enter_mut` / `exit_mut`) require a trait object.
373///
374/// Path segments are pushed/popped around the traversal unless the segment is
375/// `PathSegment::Empty`.
376pub fn perform_visit_mut<S: Into<PathSegment>>(
377    visitor: &mut dyn VisitorMutCore,
378    node: &mut dyn Visitable,
379    seg: S,
380) {
381    let seg = seg.into();
382    let should_push = !matches!(seg, PathSegment::Empty);
383
384    if should_push {
385        visitor.push(seg);
386    }
387
388    visitor.enter_mut(node);
389    node.drive_mut(visitor);
390    visitor.exit_mut(node);
391
392    if should_push {
393        visitor.pop();
394    }
395}