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 derive_more::{Deref, DerefMut};
18use serde::{Deserialize, Serialize};
19use std::{collections::BTreeMap, fmt};
20use thiserror::Error as ThisError;
21
22pub use context::{Issue, PathSegment, ScopedContext, VisitorContext};
24
25#[derive(Debug, ThisError)]
31#[error("{issues}")]
32pub struct VisitorError {
33 issues: VisitorIssues,
34}
35
36impl VisitorError {
37 #[must_use]
38 pub const fn issues(&self) -> &VisitorIssues {
39 &self.issues
40 }
41}
42
43impl From<VisitorIssues> for VisitorError {
44 fn from(issues: VisitorIssues) -> Self {
45 Self { issues }
46 }
47}
48
49impl From<VisitorError> for VisitorIssues {
50 fn from(err: VisitorError) -> Self {
51 err.issues
52 }
53}
54
55impl From<VisitorError> for InternalError {
56 fn from(err: VisitorError) -> Self {
57 Self::classified(
58 ErrorClass::Unsupported,
59 ErrorOrigin::Executor,
60 err.to_string(),
61 )
62 }
63}
64
65#[derive(
75 Clone, Debug, Default, Deserialize, Deref, DerefMut, Serialize, CandidType, Eq, PartialEq,
76)]
77pub struct VisitorIssues(BTreeMap<String, Vec<String>>);
78
79impl VisitorIssues {
80 #[must_use]
81 pub const fn new() -> Self {
82 Self(BTreeMap::new())
83 }
84}
85
86impl From<BTreeMap<String, Vec<String>>> for VisitorIssues {
87 fn from(map: BTreeMap<String, Vec<String>>) -> Self {
88 Self(map)
89 }
90}
91
92impl fmt::Display for VisitorIssues {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 let mut wrote = false;
95
96 for (path, messages) in &self.0 {
97 for message in messages {
98 if wrote {
99 writeln!(f)?;
100 }
101
102 if path.is_empty() {
103 write!(f, "{message}")?;
104 } else {
105 write!(f, "{path}: {message}")?;
106 }
107
108 wrote = true;
109 }
110 }
111
112 if !wrote {
113 write!(f, "no visitor issues")?;
114 }
115
116 Ok(())
117 }
118}
119
120impl std::error::Error for VisitorIssues {}
121
122pub(crate) trait Visitor {
128 fn enter(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
129 fn exit(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
130}
131
132pub trait VisitorCore {
138 fn enter(&mut self, node: &dyn Visitable);
139 fn exit(&mut self, node: &dyn Visitable);
140
141 fn push(&mut self, _: PathSegment) {}
142 fn pop(&mut self) {}
143}
144
145pub struct VisitableFieldDescriptor<T> {
155 name: &'static str,
156 drive: fn(&T, &mut dyn VisitorCore),
157 drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
158}
159
160impl<T> VisitableFieldDescriptor<T> {
161 #[must_use]
163 pub const fn new(
164 name: &'static str,
165 drive: fn(&T, &mut dyn VisitorCore),
166 drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
167 ) -> Self {
168 Self {
169 name,
170 drive,
171 drive_mut,
172 }
173 }
174
175 #[must_use]
177 pub const fn name(&self) -> &'static str {
178 self.name
179 }
180}
181
182pub fn drive_visitable_fields<T>(
184 visitor: &mut dyn VisitorCore,
185 node: &T,
186 fields: &[VisitableFieldDescriptor<T>],
187) {
188 for field in fields {
189 (field.drive)(node, visitor);
190 }
191}
192
193pub fn drive_visitable_fields_mut<T>(
195 visitor: &mut dyn VisitorMutCore,
196 node: &mut T,
197 fields: &[VisitableFieldDescriptor<T>],
198) {
199 for field in fields {
200 (field.drive_mut)(node, visitor);
201 }
202}
203
204pub struct SanitizeFieldDescriptor<T> {
214 sanitize: fn(&mut T, &mut dyn VisitorContext),
215}
216
217impl<T> SanitizeFieldDescriptor<T> {
218 #[must_use]
220 pub const fn new(sanitize: fn(&mut T, &mut dyn VisitorContext)) -> Self {
221 Self { sanitize }
222 }
223}
224
225pub fn drive_sanitize_fields<T>(
227 node: &mut T,
228 ctx: &mut dyn VisitorContext,
229 fields: &[SanitizeFieldDescriptor<T>],
230) {
231 for field in fields {
232 (field.sanitize)(node, ctx);
233 }
234}
235
236pub struct ValidateFieldDescriptor<T> {
246 validate: fn(&T, &mut dyn VisitorContext),
247}
248
249impl<T> ValidateFieldDescriptor<T> {
250 #[must_use]
252 pub const fn new(validate: fn(&T, &mut dyn VisitorContext)) -> Self {
253 Self { validate }
254 }
255}
256
257pub fn drive_validate_fields<T>(
259 node: &T,
260 ctx: &mut dyn VisitorContext,
261 fields: &[ValidateFieldDescriptor<T>],
262) {
263 for field in fields {
264 (field.validate)(node, ctx);
265 }
266}
267
268struct AdapterContext<'a> {
273 path: &'a [PathSegment],
274 issues: &'a mut VisitorIssues,
275 sanitize_write_context: Option<SanitizeWriteContext>,
276}
277
278impl VisitorContext for AdapterContext<'_> {
279 fn add_issue(&mut self, issue: Issue) {
280 let key = render_path(self.path, None);
281 self.issues
282 .entry(key)
283 .or_default()
284 .push(issue.into_message());
285 }
286
287 fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
288 let key = render_path(self.path, Some(seg));
289 self.issues
290 .entry(key)
291 .or_default()
292 .push(issue.into_message());
293 }
294
295 fn sanitize_write_context(&self) -> Option<SanitizeWriteContext> {
296 self.sanitize_write_context
297 }
298}
299
300fn render_path(path: &[PathSegment], extra: Option<PathSegment>) -> String {
301 use std::fmt::Write;
302
303 let mut out = String::new();
304 let mut first = true;
305
306 let iter = path.iter().cloned().chain(extra);
307
308 for seg in iter {
309 match seg {
310 PathSegment::Field(s) => {
311 if !first {
312 out.push('.');
313 }
314 out.push_str(s);
315 first = false;
316 }
317 PathSegment::Index(i) => {
318 let _ = write!(out, "[{i}]");
319 first = false;
320 }
321 PathSegment::Empty => {}
322 }
323 }
324
325 out
326}
327
328pub(crate) struct VisitorAdapter<V> {
333 visitor: V,
334 path: Vec<PathSegment>,
335 issues: VisitorIssues,
336}
337
338impl<V> VisitorAdapter<V>
339where
340 V: Visitor,
341{
342 pub(crate) const fn new(visitor: V) -> Self {
343 Self {
344 visitor,
345 path: Vec::new(),
346 issues: VisitorIssues::new(),
347 }
348 }
349
350 pub(crate) fn result(self) -> Result<(), VisitorIssues> {
351 if self.issues.is_empty() {
352 Ok(())
353 } else {
354 Err(self.issues)
355 }
356 }
357}
358
359impl<V> VisitorCore for VisitorAdapter<V>
360where
361 V: Visitor,
362{
363 fn push(&mut self, seg: PathSegment) {
364 if !matches!(seg, PathSegment::Empty) {
365 self.path.push(seg);
366 }
367 }
368
369 fn pop(&mut self) {
370 self.path.pop();
371 }
372
373 fn enter(&mut self, node: &dyn Visitable) {
374 let mut ctx = AdapterContext {
375 path: &self.path,
376 issues: &mut self.issues,
377 sanitize_write_context: None,
378 };
379 self.visitor.enter(node, &mut ctx);
380 }
381
382 fn exit(&mut self, node: &dyn Visitable) {
383 let mut ctx = AdapterContext {
384 path: &self.path,
385 issues: &mut self.issues,
386 sanitize_write_context: None,
387 };
388 self.visitor.exit(node, &mut ctx);
389 }
390}
391
392pub fn perform_visit<S: Into<PathSegment>>(
397 visitor: &mut dyn VisitorCore,
398 node: &dyn Visitable,
399 seg: S,
400) {
401 let seg = seg.into();
402 let should_push = !matches!(seg, PathSegment::Empty);
403
404 if should_push {
405 visitor.push(seg);
406 }
407
408 visitor.enter(node);
409 node.drive(visitor);
410 visitor.exit(node);
411
412 if should_push {
413 visitor.pop();
414 }
415}
416
417pub(crate) trait VisitorMut {
423 fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
424 fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
425}
426
427pub trait VisitorMutCore {
433 fn enter_mut(&mut self, node: &mut dyn Visitable);
434 fn exit_mut(&mut self, node: &mut dyn Visitable);
435
436 fn push(&mut self, _: PathSegment) {}
437 fn pop(&mut self) {}
438}
439
440pub(crate) struct VisitorMutAdapter<V> {
446 visitor: V,
447 path: Vec<PathSegment>,
448 issues: VisitorIssues,
449 sanitize_write_context: Option<SanitizeWriteContext>,
450}
451
452impl<V> VisitorMutAdapter<V>
453where
454 V: VisitorMut,
455{
456 pub(crate) const fn with_sanitize_write_context(
457 visitor: V,
458 sanitize_write_context: Option<SanitizeWriteContext>,
459 ) -> Self {
460 Self {
461 visitor,
462 path: Vec::new(),
463 issues: VisitorIssues::new(),
464 sanitize_write_context,
465 }
466 }
467
468 pub(crate) fn result(self) -> Result<(), VisitorIssues> {
469 if self.issues.is_empty() {
470 Ok(())
471 } else {
472 Err(self.issues)
473 }
474 }
475}
476
477impl<V> VisitorMutCore for VisitorMutAdapter<V>
478where
479 V: VisitorMut,
480{
481 fn push(&mut self, seg: PathSegment) {
482 if !matches!(seg, PathSegment::Empty) {
483 self.path.push(seg);
484 }
485 }
486
487 fn pop(&mut self) {
488 self.path.pop();
489 }
490
491 fn enter_mut(&mut self, node: &mut dyn Visitable) {
492 let mut ctx = AdapterContext {
493 path: &self.path,
494 issues: &mut self.issues,
495 sanitize_write_context: self.sanitize_write_context,
496 };
497 self.visitor.enter_mut(node, &mut ctx);
498 }
499
500 fn exit_mut(&mut self, node: &mut dyn Visitable) {
501 let mut ctx = AdapterContext {
502 path: &self.path,
503 issues: &mut self.issues,
504 sanitize_write_context: self.sanitize_write_context,
505 };
506 self.visitor.exit_mut(node, &mut ctx);
507 }
508}
509
510pub fn perform_visit_mut<S: Into<PathSegment>>(
522 visitor: &mut dyn VisitorMutCore,
523 node: &mut dyn Visitable,
524 seg: S,
525) {
526 let seg = seg.into();
527 let should_push = !matches!(seg, PathSegment::Empty);
528
529 if should_push {
530 visitor.push(seg);
531 }
532
533 visitor.enter_mut(node);
534 node.drive_mut(visitor);
535 visitor.exit_mut(node);
536
537 if should_push {
538 visitor.pop();
539 }
540}