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