icydb_core/visitor/
mod.rs

1pub mod sanitize;
2pub mod validate;
3
4pub use sanitize::*;
5pub use validate::*;
6
7use crate::traits::Visitable;
8use candid::CandidType;
9use derive_more::{Deref, DerefMut};
10use serde::{Deserialize, Serialize};
11use std::{collections::BTreeMap, fmt};
12
13///
14/// VisitorIssues
15///
16
17#[derive(
18    Clone, Debug, Default, Deserialize, Deref, DerefMut, Serialize, CandidType, Eq, PartialEq,
19)]
20pub struct VisitorIssues(BTreeMap<String, Vec<String>>);
21
22impl VisitorIssues {
23    #[must_use]
24    pub const fn new() -> Self {
25        Self(BTreeMap::new())
26    }
27}
28
29impl fmt::Display for VisitorIssues {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        let mut wrote = false;
32
33        for (path, messages) in &self.0 {
34            for message in messages {
35                if wrote {
36                    writeln!(f)?;
37                }
38
39                if path.is_empty() {
40                    write!(f, "{message}")?;
41                } else {
42                    write!(f, "{path}: {message}")?;
43                }
44
45                wrote = true;
46            }
47        }
48
49        if !wrote {
50            write!(f, "no visitor issues")?;
51        }
52
53        Ok(())
54    }
55}
56
57impl std::error::Error for VisitorIssues {}
58
59///
60/// PathSegment
61///
62
63#[derive(Clone, Debug)]
64pub enum PathSegment {
65    Empty,
66    Field(&'static str),
67    Index(usize),
68}
69
70impl From<&'static str> for PathSegment {
71    fn from(s: &'static str) -> Self {
72        Self::Field(s)
73    }
74}
75
76impl From<usize> for PathSegment {
77    fn from(i: usize) -> Self {
78        Self::Index(i)
79    }
80}
81
82impl From<Option<&'static str>> for PathSegment {
83    fn from(opt: Option<&'static str>) -> Self {
84        match opt {
85            Some(s) if !s.is_empty() => Self::Field(s),
86            _ => Self::Empty,
87        }
88    }
89}
90
91///
92/// VisitorContext
93/// Narrow interface exposed to visitors for reporting non-fatal issues.
94/// Implemented by adapters via a short-lived context object.
95///
96
97pub trait VisitorContext {
98    fn add_issue(&mut self, message: String);
99    fn add_issue_at(&mut self, seg: PathSegment, message: String);
100}
101
102// ============================================================================
103// Visitor (immutable)
104// ============================================================================
105
106pub trait Visitor {
107    fn enter(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
108    fn exit(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
109}
110
111// ============================================================================
112// VisitorCore (object-safe traversal)
113// ============================================================================
114
115pub trait VisitorCore {
116    fn enter(&mut self, node: &dyn Visitable);
117    fn exit(&mut self, node: &dyn Visitable);
118
119    fn push(&mut self, _: PathSegment) {}
120    fn pop(&mut self) {}
121}
122
123// ============================================================================
124// Internal adapter context (fixes borrow checker)
125// ============================================================================
126
127struct AdapterContext<'a> {
128    path: &'a [PathSegment],
129    issues: &'a mut VisitorIssues,
130}
131
132impl VisitorContext for AdapterContext<'_> {
133    fn add_issue(&mut self, message: String) {
134        let key = render_path(self.path, None);
135        self.issues.entry(key).or_default().push(message);
136    }
137
138    fn add_issue_at(&mut self, seg: PathSegment, message: String) {
139        let key = render_path(self.path, Some(seg));
140        self.issues.entry(key).or_default().push(message);
141    }
142}
143
144fn render_path(path: &[PathSegment], extra: Option<PathSegment>) -> String {
145    use std::fmt::Write;
146
147    let mut out = String::new();
148    let mut first = true;
149
150    let iter = path.iter().cloned().chain(extra);
151
152    for seg in iter {
153        match seg {
154            PathSegment::Field(s) => {
155                if !first {
156                    out.push('.');
157                }
158                out.push_str(s);
159            }
160            PathSegment::Index(i) => {
161                let _ = write!(out, "[{i}]");
162            }
163            PathSegment::Empty => {}
164        }
165        first = false;
166    }
167
168    out
169}
170
171// ============================================================================
172// VisitorAdapter (immutable)
173// ============================================================================
174
175pub struct VisitorAdapter<V> {
176    visitor: V,
177    path: Vec<PathSegment>,
178    issues: VisitorIssues,
179}
180
181impl<V> VisitorAdapter<V>
182where
183    V: Visitor,
184{
185    pub const fn new(visitor: V) -> Self {
186        Self {
187            visitor,
188            path: Vec::new(),
189            issues: VisitorIssues::new(),
190        }
191    }
192
193    pub const fn issues(&self) -> &VisitorIssues {
194        &self.issues
195    }
196
197    pub fn result(self) -> Result<(), VisitorIssues> {
198        if self.issues.is_empty() {
199            Ok(())
200        } else {
201            Err(self.issues)
202        }
203    }
204}
205
206impl<V> VisitorCore for VisitorAdapter<V>
207where
208    V: Visitor,
209{
210    fn push(&mut self, seg: PathSegment) {
211        if !matches!(seg, PathSegment::Empty) {
212            self.path.push(seg);
213        }
214    }
215
216    fn pop(&mut self) {
217        self.path.pop();
218    }
219
220    fn enter(&mut self, node: &dyn Visitable) {
221        let mut ctx = AdapterContext {
222            path: &self.path,
223            issues: &mut self.issues,
224        };
225        self.visitor.enter(node, &mut ctx);
226    }
227
228    fn exit(&mut self, node: &dyn Visitable) {
229        let mut ctx = AdapterContext {
230            path: &self.path,
231            issues: &mut self.issues,
232        };
233        self.visitor.exit(node, &mut ctx);
234    }
235}
236
237// ============================================================================
238// Traversal (immutable)
239// ============================================================================
240
241pub fn perform_visit<S: Into<PathSegment>>(
242    visitor: &mut dyn VisitorCore,
243    node: &dyn Visitable,
244    seg: S,
245) {
246    let seg = seg.into();
247    let should_push = !matches!(seg, PathSegment::Empty);
248
249    if should_push {
250        visitor.push(seg);
251    }
252
253    visitor.enter(node);
254    node.drive(visitor);
255    visitor.exit(node);
256
257    if should_push {
258        visitor.pop();
259    }
260}
261
262// ============================================================================
263// VisitorMut (mutable)
264// ============================================================================
265
266pub trait VisitorMut {
267    fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
268    fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
269}
270
271// ============================================================================
272// VisitorMutCore
273// ============================================================================
274
275pub trait VisitorMutCore {
276    fn enter_mut(&mut self, node: &mut dyn Visitable);
277    fn exit_mut(&mut self, node: &mut dyn Visitable);
278
279    fn push(&mut self, _: PathSegment) {}
280    fn pop(&mut self) {}
281}
282
283// ============================================================================
284// VisitorMutAdapter
285// ============================================================================
286
287pub struct VisitorMutAdapter<V> {
288    visitor: V,
289    path: Vec<PathSegment>,
290    issues: VisitorIssues,
291}
292
293impl<V> VisitorMutAdapter<V>
294where
295    V: VisitorMut,
296{
297    pub const fn new(visitor: V) -> Self {
298        Self {
299            visitor,
300            path: Vec::new(),
301            issues: VisitorIssues::new(),
302        }
303    }
304
305    pub const fn issues(&self) -> &VisitorIssues {
306        &self.issues
307    }
308
309    pub 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
353pub fn perform_visit_mut<S: Into<PathSegment>>(
354    visitor: &mut dyn VisitorMutCore,
355    node: &mut dyn Visitable,
356    seg: S,
357) {
358    let seg = seg.into();
359    let should_push = !matches!(seg, PathSegment::Empty);
360
361    if should_push {
362        visitor.push(seg);
363    }
364
365    visitor.enter_mut(node);
366    node.drive_mut(visitor);
367    visitor.exit_mut(node);
368
369    if should_push {
370        visitor.pop();
371    }
372}