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