icydb_core/visitor/
mod.rs

1pub mod context;
2pub mod sanitize;
3pub mod validate;
4
5pub use context::*;
6pub use sanitize::SanitizeVisitor;
7pub use validate::ValidateVisitor;
8
9use crate::error::{ErrorClass, ErrorOrigin, InternalError};
10use crate::traits::Visitable;
11use candid::CandidType;
12use derive_more::{Deref, DerefMut};
13use serde::{Deserialize, Serialize};
14use std::{collections::BTreeMap, fmt};
15use thiserror::Error as ThisError;
16
17///
18/// VisitorError
19/// Structured error type for visitor-based sanitization and validation.
20///
21
22#[derive(Debug, ThisError)]
23#[error("{issues}")]
24pub struct VisitorError {
25    issues: VisitorIssues,
26}
27
28impl VisitorError {
29    #[must_use]
30    pub const fn issues(&self) -> &VisitorIssues {
31        &self.issues
32    }
33}
34
35impl From<VisitorIssues> for VisitorError {
36    fn from(issues: VisitorIssues) -> Self {
37        Self { issues }
38    }
39}
40
41impl From<VisitorError> for VisitorIssues {
42    fn from(err: VisitorError) -> Self {
43        err.issues
44    }
45}
46
47impl From<VisitorError> for InternalError {
48    fn from(err: VisitorError) -> Self {
49        Self::new(
50            ErrorClass::Unsupported,
51            ErrorOrigin::Executor,
52            err.to_string(),
53        )
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 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
128pub trait VisitorCore {
129    fn enter(&mut self, node: &dyn Visitable);
130    fn exit(&mut self, node: &dyn Visitable);
131
132    fn push(&mut self, _: PathSegment) {}
133    fn pop(&mut self) {}
134}
135
136// ============================================================================
137// Internal adapter context (fixes borrow checker)
138// ============================================================================
139
140struct AdapterContext<'a> {
141    path: &'a [PathSegment],
142    issues: &'a mut VisitorIssues,
143}
144
145impl VisitorContext for AdapterContext<'_> {
146    fn add_issue(&mut self, issue: Issue) {
147        let key = render_path(self.path, None);
148        self.issues.entry(key).or_default().push(issue.message);
149    }
150
151    fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
152        let key = render_path(self.path, Some(seg));
153        self.issues.entry(key).or_default().push(issue.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 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 const fn new(visitor: V) -> Self {
200        Self {
201            visitor,
202            path: Vec::new(),
203            issues: VisitorIssues::new(),
204        }
205    }
206
207    pub const fn issues(&self) -> &VisitorIssues {
208        &self.issues
209    }
210
211    pub fn result(self) -> Result<(), VisitorIssues> {
212        if self.issues.is_empty() {
213            Ok(())
214        } else {
215            Err(self.issues)
216        }
217    }
218}
219
220impl<V> VisitorCore for VisitorAdapter<V>
221where
222    V: Visitor,
223{
224    fn push(&mut self, seg: PathSegment) {
225        if !matches!(seg, PathSegment::Empty) {
226            self.path.push(seg);
227        }
228    }
229
230    fn pop(&mut self) {
231        self.path.pop();
232    }
233
234    fn enter(&mut self, node: &dyn Visitable) {
235        let mut ctx = AdapterContext {
236            path: &self.path,
237            issues: &mut self.issues,
238        };
239        self.visitor.enter(node, &mut ctx);
240    }
241
242    fn exit(&mut self, node: &dyn Visitable) {
243        let mut ctx = AdapterContext {
244            path: &self.path,
245            issues: &mut self.issues,
246        };
247        self.visitor.exit(node, &mut ctx);
248    }
249}
250
251// ============================================================================
252// Traversal (immutable)
253// ============================================================================
254
255pub fn perform_visit<S: Into<PathSegment>>(
256    visitor: &mut dyn VisitorCore,
257    node: &dyn Visitable,
258    seg: S,
259) {
260    let seg = seg.into();
261    let should_push = !matches!(seg, PathSegment::Empty);
262
263    if should_push {
264        visitor.push(seg);
265    }
266
267    visitor.enter(node);
268    node.drive(visitor);
269    visitor.exit(node);
270
271    if should_push {
272        visitor.pop();
273    }
274}
275
276// ============================================================================
277// VisitorMut (mutable)
278// ============================================================================
279
280pub trait VisitorMut {
281    fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
282    fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
283}
284
285// ============================================================================
286// VisitorMutCore
287// ============================================================================
288
289pub trait VisitorMutCore {
290    fn enter_mut(&mut self, node: &mut dyn Visitable);
291    fn exit_mut(&mut self, node: &mut dyn Visitable);
292
293    fn push(&mut self, _: PathSegment) {}
294    fn pop(&mut self) {}
295}
296
297// ============================================================================
298// VisitorMutAdapter
299// ============================================================================
300
301pub 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 const fn new(visitor: V) -> Self {
312        Self {
313            visitor,
314            path: Vec::new(),
315            issues: VisitorIssues::new(),
316        }
317    }
318
319    pub const fn issues(&self) -> &VisitorIssues {
320        &self.issues
321    }
322
323    pub fn result(self) -> Result<(), VisitorIssues> {
324        if self.issues.is_empty() {
325            Ok(())
326        } else {
327            Err(self.issues)
328        }
329    }
330}
331
332impl<V> VisitorMutCore for VisitorMutAdapter<V>
333where
334    V: VisitorMut,
335{
336    fn push(&mut self, seg: PathSegment) {
337        if !matches!(seg, PathSegment::Empty) {
338            self.path.push(seg);
339        }
340    }
341
342    fn pop(&mut self) {
343        self.path.pop();
344    }
345
346    fn enter_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.enter_mut(node, &mut ctx);
352    }
353
354    fn exit_mut(&mut self, node: &mut dyn Visitable) {
355        let mut ctx = AdapterContext {
356            path: &self.path,
357            issues: &mut self.issues,
358        };
359        self.visitor.exit_mut(node, &mut ctx);
360    }
361}
362
363// ============================================================================
364// Traversal (mutable)
365// ============================================================================
366
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}