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