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