Skip to main content

icydb_core/visitor/
mod.rs

1pub(crate) mod context;
2pub(crate) mod sanitize;
3pub(crate) mod validate;
4
5use crate::{
6    error::{ErrorClass, ErrorOrigin, InternalError},
7    traits::Visitable,
8};
9use candid::CandidType;
10use derive_more::{Deref, DerefMut};
11use serde::{Deserialize, Serialize};
12use std::{collections::BTreeMap, fmt};
13use thiserror::Error as ThisError;
14
15// re-exports
16pub use context::{Issue, PathSegment, ScopedContext, VisitorContext};
17
18///
19/// VisitorError
20/// Structured error type for visitor-based sanitization and validation.
21///
22
23#[derive(Debug, ThisError)]
24#[error("{issues}")]
25pub struct VisitorError {
26    issues: VisitorIssues,
27}
28
29impl VisitorError {
30    #[must_use]
31    pub const fn issues(&self) -> &VisitorIssues {
32        &self.issues
33    }
34}
35
36impl From<VisitorIssues> for VisitorError {
37    fn from(issues: VisitorIssues) -> Self {
38        Self { issues }
39    }
40}
41
42impl From<VisitorError> for VisitorIssues {
43    fn from(err: VisitorError) -> Self {
44        err.issues
45    }
46}
47
48impl From<VisitorError> for InternalError {
49    fn from(err: VisitorError) -> Self {
50        Self::new(
51            ErrorClass::Unsupported,
52            ErrorOrigin::Executor,
53            err.to_string(),
54        )
55    }
56}
57
58///
59/// VisitorIssues
60/// Aggregated visitor diagnostics.
61///
62/// NOTE: This is not an error type. It does not represent failure.
63/// It is converted into a `VisitorError` at the runtime boundary and
64/// may be lifted into an `InternalError` as needed.
65///
66
67#[derive(
68    Clone, Debug, Default, Deserialize, Deref, DerefMut, Serialize, CandidType, Eq, PartialEq,
69)]
70pub struct VisitorIssues(BTreeMap<String, Vec<String>>);
71
72impl VisitorIssues {
73    #[must_use]
74    pub const fn new() -> Self {
75        Self(BTreeMap::new())
76    }
77}
78
79impl From<BTreeMap<String, Vec<String>>> for VisitorIssues {
80    fn from(map: BTreeMap<String, Vec<String>>) -> Self {
81        Self(map)
82    }
83}
84
85impl fmt::Display for VisitorIssues {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        let mut wrote = false;
88
89        for (path, messages) in &self.0 {
90            for message in messages {
91                if wrote {
92                    writeln!(f)?;
93                }
94
95                if path.is_empty() {
96                    write!(f, "{message}")?;
97                } else {
98                    write!(f, "{path}: {message}")?;
99                }
100
101                wrote = true;
102            }
103        }
104
105        if !wrote {
106            write!(f, "no visitor issues")?;
107        }
108
109        Ok(())
110    }
111}
112
113impl std::error::Error for VisitorIssues {}
114
115///
116/// Visitor
117/// (immutable)
118///
119
120pub(crate) trait Visitor {
121    fn enter(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
122    fn exit(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
123}
124
125// ============================================================================
126// VisitorCore (object-safe traversal)
127// ============================================================================
128
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.entry(key).or_default().push(issue.message);
150    }
151
152    fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
153        let key = render_path(self.path, Some(seg));
154        self.issues.entry(key).or_default().push(issue.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
277pub(crate) trait VisitorMut {
278    fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
279    fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
280}
281
282// ============================================================================
283// VisitorMutCore
284// ============================================================================
285
286pub trait VisitorMutCore {
287    fn enter_mut(&mut self, node: &mut dyn Visitable);
288    fn exit_mut(&mut self, node: &mut dyn Visitable);
289
290    fn push(&mut self, _: PathSegment) {}
291    fn pop(&mut self) {}
292}
293
294// ============================================================================
295// VisitorMutAdapter
296// ============================================================================
297
298pub(crate) struct VisitorMutAdapter<V> {
299    visitor: V,
300    path: Vec<PathSegment>,
301    issues: VisitorIssues,
302}
303
304impl<V> VisitorMutAdapter<V>
305where
306    V: VisitorMut,
307{
308    pub(crate) const fn new(visitor: V) -> Self {
309        Self {
310            visitor,
311            path: Vec::new(),
312            issues: VisitorIssues::new(),
313        }
314    }
315
316    pub(crate) fn result(self) -> Result<(), VisitorIssues> {
317        if self.issues.is_empty() {
318            Ok(())
319        } else {
320            Err(self.issues)
321        }
322    }
323}
324
325impl<V> VisitorMutCore for VisitorMutAdapter<V>
326where
327    V: VisitorMut,
328{
329    fn push(&mut self, seg: PathSegment) {
330        if !matches!(seg, PathSegment::Empty) {
331            self.path.push(seg);
332        }
333    }
334
335    fn pop(&mut self) {
336        self.path.pop();
337    }
338
339    fn enter_mut(&mut self, node: &mut dyn Visitable) {
340        let mut ctx = AdapterContext {
341            path: &self.path,
342            issues: &mut self.issues,
343        };
344        self.visitor.enter_mut(node, &mut ctx);
345    }
346
347    fn exit_mut(&mut self, node: &mut dyn Visitable) {
348        let mut ctx = AdapterContext {
349            path: &self.path,
350            issues: &mut self.issues,
351        };
352        self.visitor.exit_mut(node, &mut ctx);
353    }
354}
355
356// ============================================================================
357// Traversal (mutable)
358// ============================================================================
359
360/// Perform a mutable visitor traversal starting at a trait-object node.
361///
362/// This is the *core* traversal entrypoint. It operates on `&mut dyn Visitable`
363/// because visitor callbacks (`enter_mut` / `exit_mut`) require a trait object.
364///
365/// Path segments are pushed/popped around the traversal unless the segment is
366/// `PathSegment::Empty`.
367pub fn perform_visit_mut<S: Into<PathSegment>>(
368    visitor: &mut dyn VisitorMutCore,
369    node: &mut dyn Visitable,
370    seg: S,
371) {
372    let seg = seg.into();
373    let should_push = !matches!(seg, PathSegment::Empty);
374
375    if should_push {
376        visitor.push(seg);
377    }
378
379    visitor.enter_mut(node);
380    node.drive_mut(visitor);
381    visitor.exit_mut(node);
382
383    if should_push {
384        visitor.pop();
385    }
386}