Skip to main content

icydb_core/visitor/
mod.rs

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