1pub(crate) mod context;
8pub(crate) mod sanitize;
9pub(crate) mod validate;
10
11use crate::{
12 error::{ErrorClass, ErrorOrigin, InternalError},
13 sanitize::SanitizeWriteContext,
14 traits::Visitable,
15};
16use candid::CandidType;
17use serde::Deserialize;
18use std::{collections::BTreeMap, fmt};
19use thiserror::Error as ThisError;
20
21pub use context::{Issue, PathSegment, ScopedContext, VisitorContext};
23
24#[derive(Debug, ThisError)]
30#[error("{issues}")]
31pub struct VisitorError {
32 issues: VisitorIssues,
33}
34
35impl VisitorError {
36 #[must_use]
37 pub const fn issues(&self) -> &VisitorIssues {
38 &self.issues
39 }
40}
41
42impl From<VisitorIssues> for VisitorError {
43 fn from(issues: VisitorIssues) -> Self {
44 Self { issues }
45 }
46}
47
48impl From<VisitorError> for VisitorIssues {
49 fn from(err: VisitorError) -> Self {
50 err.issues
51 }
52}
53
54impl From<VisitorError> for InternalError {
55 fn from(err: VisitorError) -> Self {
56 Self::classified(
57 ErrorClass::Unsupported,
58 ErrorOrigin::Executor,
59 err.to_string(),
60 )
61 }
62}
63
64#[derive(CandidType, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
74pub struct VisitorIssues(BTreeMap<String, Vec<String>>);
75
76impl VisitorIssues {
77 #[must_use]
78 pub const fn new() -> Self {
79 Self(BTreeMap::new())
80 }
81
82 #[must_use]
83 pub fn is_empty(&self) -> bool {
84 self.0.is_empty()
85 }
86
87 #[must_use]
89 pub fn len(&self) -> usize {
90 self.0.len()
91 }
92
93 #[must_use]
94 pub fn get(&self, path: impl AsRef<str>) -> Option<&[String]> {
95 self.0.get(path.as_ref()).map(Vec::as_slice)
96 }
97
98 pub fn push(&mut self, path: String, issue: Issue) {
99 self.0.entry(path).or_default().push(issue.into_message());
100 }
101}
102
103impl From<BTreeMap<String, Vec<String>>> for VisitorIssues {
104 fn from(map: BTreeMap<String, Vec<String>>) -> Self {
105 Self(map)
106 }
107}
108
109impl fmt::Display for VisitorIssues {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 let mut wrote = false;
112
113 for (path, messages) in &self.0 {
114 for message in messages {
115 if wrote {
116 writeln!(f)?;
117 }
118
119 if path.is_empty() {
120 write!(f, "{message}")?;
121 } else {
122 write!(f, "{path}: {message}")?;
123 }
124
125 wrote = true;
126 }
127 }
128
129 if !wrote {
130 write!(f, "no visitor issues")?;
131 }
132
133 Ok(())
134 }
135}
136
137impl std::error::Error for VisitorIssues {}
138
139pub(crate) trait Visitor {
145 fn enter(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
146 fn exit(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
147}
148
149pub trait VisitorCore {
155 fn enter(&mut self, node: &dyn Visitable);
156 fn exit(&mut self, node: &dyn Visitable);
157
158 fn push(&mut self, _: PathSegment) {}
159 fn pop(&mut self) {}
160}
161
162pub struct VisitableFieldDescriptor<T> {
172 name: &'static str,
173 drive: fn(&T, &mut dyn VisitorCore),
174 drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
175}
176
177impl<T> VisitableFieldDescriptor<T> {
178 #[must_use]
180 pub const fn new(
181 name: &'static str,
182 drive: fn(&T, &mut dyn VisitorCore),
183 drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
184 ) -> Self {
185 Self {
186 name,
187 drive,
188 drive_mut,
189 }
190 }
191
192 #[must_use]
194 pub const fn name(&self) -> &'static str {
195 self.name
196 }
197}
198
199pub fn drive_visitable_fields<T>(
201 visitor: &mut dyn VisitorCore,
202 node: &T,
203 fields: &[VisitableFieldDescriptor<T>],
204) {
205 for field in fields {
206 (field.drive)(node, visitor);
207 }
208}
209
210pub fn drive_visitable_fields_mut<T>(
212 visitor: &mut dyn VisitorMutCore,
213 node: &mut T,
214 fields: &[VisitableFieldDescriptor<T>],
215) {
216 for field in fields {
217 (field.drive_mut)(node, visitor);
218 }
219}
220
221pub struct SanitizeFieldDescriptor<T> {
231 sanitize: fn(&mut T, &mut dyn VisitorContext),
232}
233
234impl<T> SanitizeFieldDescriptor<T> {
235 #[must_use]
237 pub const fn new(sanitize: fn(&mut T, &mut dyn VisitorContext)) -> Self {
238 Self { sanitize }
239 }
240}
241
242pub fn drive_sanitize_fields<T>(
244 node: &mut T,
245 ctx: &mut dyn VisitorContext,
246 fields: &[SanitizeFieldDescriptor<T>],
247) {
248 for field in fields {
249 (field.sanitize)(node, ctx);
250 }
251}
252
253pub struct ValidateFieldDescriptor<T> {
263 validate: fn(&T, &mut dyn VisitorContext),
264}
265
266impl<T> ValidateFieldDescriptor<T> {
267 #[must_use]
269 pub const fn new(validate: fn(&T, &mut dyn VisitorContext)) -> Self {
270 Self { validate }
271 }
272}
273
274pub fn drive_validate_fields<T>(
276 node: &T,
277 ctx: &mut dyn VisitorContext,
278 fields: &[ValidateFieldDescriptor<T>],
279) {
280 for field in fields {
281 (field.validate)(node, ctx);
282 }
283}
284
285struct AdapterContext<'a> {
290 path: &'a [PathSegment],
291 issues: &'a mut VisitorIssues,
292 sanitize_write_context: Option<SanitizeWriteContext>,
293}
294
295impl VisitorContext for AdapterContext<'_> {
296 fn add_issue(&mut self, issue: Issue) {
297 let key = render_path(self.path, None);
298 self.issues.push(key, issue);
299 }
300
301 fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
302 let key = render_path(self.path, Some(seg));
303 self.issues.push(key, issue);
304 }
305
306 fn sanitize_write_context(&self) -> Option<SanitizeWriteContext> {
307 self.sanitize_write_context
308 }
309}
310
311fn render_path(path: &[PathSegment], extra: Option<PathSegment>) -> String {
312 use std::fmt::Write;
313
314 let mut out = String::new();
315 let mut first = true;
316
317 let iter = path.iter().cloned().chain(extra);
318
319 for seg in iter {
320 match seg {
321 PathSegment::Field(s) => {
322 if !first {
323 out.push('.');
324 }
325 out.push_str(s);
326 first = false;
327 }
328 PathSegment::Index(i) => {
329 let _ = write!(out, "[{i}]");
330 first = false;
331 }
332 PathSegment::Empty => {}
333 }
334 }
335
336 out
337}
338
339pub(crate) struct VisitorAdapter<V> {
344 visitor: V,
345 path: Vec<PathSegment>,
346 issues: VisitorIssues,
347}
348
349impl<V> VisitorAdapter<V>
350where
351 V: Visitor,
352{
353 pub(crate) const fn new(visitor: V) -> Self {
354 Self {
355 visitor,
356 path: Vec::new(),
357 issues: VisitorIssues::new(),
358 }
359 }
360
361 pub(crate) fn result(self) -> Result<(), VisitorIssues> {
362 if self.issues.is_empty() {
363 Ok(())
364 } else {
365 Err(self.issues)
366 }
367 }
368}
369
370impl<V> VisitorCore for VisitorAdapter<V>
371where
372 V: Visitor,
373{
374 fn push(&mut self, seg: PathSegment) {
375 if !matches!(seg, PathSegment::Empty) {
376 self.path.push(seg);
377 }
378 }
379
380 fn pop(&mut self) {
381 self.path.pop();
382 }
383
384 fn enter(&mut self, node: &dyn Visitable) {
385 let mut ctx = AdapterContext {
386 path: &self.path,
387 issues: &mut self.issues,
388 sanitize_write_context: None,
389 };
390 self.visitor.enter(node, &mut ctx);
391 }
392
393 fn exit(&mut self, node: &dyn Visitable) {
394 let mut ctx = AdapterContext {
395 path: &self.path,
396 issues: &mut self.issues,
397 sanitize_write_context: None,
398 };
399 self.visitor.exit(node, &mut ctx);
400 }
401}
402
403pub fn perform_visit<S: Into<PathSegment>>(
408 visitor: &mut dyn VisitorCore,
409 node: &dyn Visitable,
410 seg: S,
411) {
412 let seg = seg.into();
413 let should_push = !matches!(seg, PathSegment::Empty);
414
415 if should_push {
416 visitor.push(seg);
417 }
418
419 visitor.enter(node);
420 node.drive(visitor);
421 visitor.exit(node);
422
423 if should_push {
424 visitor.pop();
425 }
426}
427
428pub(crate) trait VisitorMut {
434 fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
435 fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
436}
437
438pub trait VisitorMutCore {
444 fn enter_mut(&mut self, node: &mut dyn Visitable);
445 fn exit_mut(&mut self, node: &mut dyn Visitable);
446
447 fn push(&mut self, _: PathSegment) {}
448 fn pop(&mut self) {}
449}
450
451pub(crate) struct VisitorMutAdapter<V> {
457 visitor: V,
458 path: Vec<PathSegment>,
459 issues: VisitorIssues,
460 sanitize_write_context: Option<SanitizeWriteContext>,
461}
462
463impl<V> VisitorMutAdapter<V>
464where
465 V: VisitorMut,
466{
467 pub(crate) const fn with_sanitize_write_context(
468 visitor: V,
469 sanitize_write_context: Option<SanitizeWriteContext>,
470 ) -> Self {
471 Self {
472 visitor,
473 path: Vec::new(),
474 issues: VisitorIssues::new(),
475 sanitize_write_context,
476 }
477 }
478
479 pub(crate) fn result(self) -> Result<(), VisitorIssues> {
480 if self.issues.is_empty() {
481 Ok(())
482 } else {
483 Err(self.issues)
484 }
485 }
486}
487
488impl<V> VisitorMutCore for VisitorMutAdapter<V>
489where
490 V: VisitorMut,
491{
492 fn push(&mut self, seg: PathSegment) {
493 if !matches!(seg, PathSegment::Empty) {
494 self.path.push(seg);
495 }
496 }
497
498 fn pop(&mut self) {
499 self.path.pop();
500 }
501
502 fn enter_mut(&mut self, node: &mut dyn Visitable) {
503 let mut ctx = AdapterContext {
504 path: &self.path,
505 issues: &mut self.issues,
506 sanitize_write_context: self.sanitize_write_context,
507 };
508 self.visitor.enter_mut(node, &mut ctx);
509 }
510
511 fn exit_mut(&mut self, node: &mut dyn Visitable) {
512 let mut ctx = AdapterContext {
513 path: &self.path,
514 issues: &mut self.issues,
515 sanitize_write_context: self.sanitize_write_context,
516 };
517 self.visitor.exit_mut(node, &mut ctx);
518 }
519}
520
521pub fn perform_visit_mut<S: Into<PathSegment>>(
533 visitor: &mut dyn VisitorMutCore,
534 node: &mut dyn Visitable,
535 seg: S,
536) {
537 let seg = seg.into();
538 let should_push = !matches!(seg, PathSegment::Empty);
539
540 if should_push {
541 visitor.push(seg);
542 }
543
544 visitor.enter_mut(node);
545 node.drive_mut(visitor);
546 visitor.exit_mut(node);
547
548 if should_push {
549 visitor.pop();
550 }
551}