pub(crate) mod context;
pub(crate) mod sanitize;
pub(crate) mod validate;
use crate::{
error::{ErrorClass, ErrorOrigin, InternalError},
sanitize::SanitizeWriteContext,
traits::Visitable,
};
use candid::CandidType;
use derive_more::{Deref, DerefMut};
use serde::Deserialize;
use std::{collections::BTreeMap, fmt};
use thiserror::Error as ThisError;
pub use context::{Issue, PathSegment, ScopedContext, VisitorContext};
#[derive(Debug, ThisError)]
#[error("{issues}")]
pub struct VisitorError {
issues: VisitorIssues,
}
impl VisitorError {
#[must_use]
pub const fn issues(&self) -> &VisitorIssues {
&self.issues
}
}
impl From<VisitorIssues> for VisitorError {
fn from(issues: VisitorIssues) -> Self {
Self { issues }
}
}
impl From<VisitorError> for VisitorIssues {
fn from(err: VisitorError) -> Self {
err.issues
}
}
impl From<VisitorError> for InternalError {
fn from(err: VisitorError) -> Self {
Self::classified(
ErrorClass::Unsupported,
ErrorOrigin::Executor,
err.to_string(),
)
}
}
#[derive(CandidType, Clone, Debug, Default, Deref, DerefMut, Deserialize, Eq, PartialEq)]
pub struct VisitorIssues(BTreeMap<String, Vec<String>>);
impl VisitorIssues {
#[must_use]
pub const fn new() -> Self {
Self(BTreeMap::new())
}
}
impl From<BTreeMap<String, Vec<String>>> for VisitorIssues {
fn from(map: BTreeMap<String, Vec<String>>) -> Self {
Self(map)
}
}
impl fmt::Display for VisitorIssues {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut wrote = false;
for (path, messages) in &self.0 {
for message in messages {
if wrote {
writeln!(f)?;
}
if path.is_empty() {
write!(f, "{message}")?;
} else {
write!(f, "{path}: {message}")?;
}
wrote = true;
}
}
if !wrote {
write!(f, "no visitor issues")?;
}
Ok(())
}
}
impl std::error::Error for VisitorIssues {}
pub(crate) trait Visitor {
fn enter(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
fn exit(&mut self, node: &dyn Visitable, ctx: &mut dyn VisitorContext);
}
pub trait VisitorCore {
fn enter(&mut self, node: &dyn Visitable);
fn exit(&mut self, node: &dyn Visitable);
fn push(&mut self, _: PathSegment) {}
fn pop(&mut self) {}
}
pub struct VisitableFieldDescriptor<T> {
name: &'static str,
drive: fn(&T, &mut dyn VisitorCore),
drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
}
impl<T> VisitableFieldDescriptor<T> {
#[must_use]
pub const fn new(
name: &'static str,
drive: fn(&T, &mut dyn VisitorCore),
drive_mut: fn(&mut T, &mut dyn VisitorMutCore),
) -> Self {
Self {
name,
drive,
drive_mut,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
}
pub fn drive_visitable_fields<T>(
visitor: &mut dyn VisitorCore,
node: &T,
fields: &[VisitableFieldDescriptor<T>],
) {
for field in fields {
(field.drive)(node, visitor);
}
}
pub fn drive_visitable_fields_mut<T>(
visitor: &mut dyn VisitorMutCore,
node: &mut T,
fields: &[VisitableFieldDescriptor<T>],
) {
for field in fields {
(field.drive_mut)(node, visitor);
}
}
pub struct SanitizeFieldDescriptor<T> {
sanitize: fn(&mut T, &mut dyn VisitorContext),
}
impl<T> SanitizeFieldDescriptor<T> {
#[must_use]
pub const fn new(sanitize: fn(&mut T, &mut dyn VisitorContext)) -> Self {
Self { sanitize }
}
}
pub fn drive_sanitize_fields<T>(
node: &mut T,
ctx: &mut dyn VisitorContext,
fields: &[SanitizeFieldDescriptor<T>],
) {
for field in fields {
(field.sanitize)(node, ctx);
}
}
pub struct ValidateFieldDescriptor<T> {
validate: fn(&T, &mut dyn VisitorContext),
}
impl<T> ValidateFieldDescriptor<T> {
#[must_use]
pub const fn new(validate: fn(&T, &mut dyn VisitorContext)) -> Self {
Self { validate }
}
}
pub fn drive_validate_fields<T>(
node: &T,
ctx: &mut dyn VisitorContext,
fields: &[ValidateFieldDescriptor<T>],
) {
for field in fields {
(field.validate)(node, ctx);
}
}
struct AdapterContext<'a> {
path: &'a [PathSegment],
issues: &'a mut VisitorIssues,
sanitize_write_context: Option<SanitizeWriteContext>,
}
impl VisitorContext for AdapterContext<'_> {
fn add_issue(&mut self, issue: Issue) {
let key = render_path(self.path, None);
self.issues
.entry(key)
.or_default()
.push(issue.into_message());
}
fn add_issue_at(&mut self, seg: PathSegment, issue: Issue) {
let key = render_path(self.path, Some(seg));
self.issues
.entry(key)
.or_default()
.push(issue.into_message());
}
fn sanitize_write_context(&self) -> Option<SanitizeWriteContext> {
self.sanitize_write_context
}
}
fn render_path(path: &[PathSegment], extra: Option<PathSegment>) -> String {
use std::fmt::Write;
let mut out = String::new();
let mut first = true;
let iter = path.iter().cloned().chain(extra);
for seg in iter {
match seg {
PathSegment::Field(s) => {
if !first {
out.push('.');
}
out.push_str(s);
first = false;
}
PathSegment::Index(i) => {
let _ = write!(out, "[{i}]");
first = false;
}
PathSegment::Empty => {}
}
}
out
}
pub(crate) struct VisitorAdapter<V> {
visitor: V,
path: Vec<PathSegment>,
issues: VisitorIssues,
}
impl<V> VisitorAdapter<V>
where
V: Visitor,
{
pub(crate) const fn new(visitor: V) -> Self {
Self {
visitor,
path: Vec::new(),
issues: VisitorIssues::new(),
}
}
pub(crate) fn result(self) -> Result<(), VisitorIssues> {
if self.issues.is_empty() {
Ok(())
} else {
Err(self.issues)
}
}
}
impl<V> VisitorCore for VisitorAdapter<V>
where
V: Visitor,
{
fn push(&mut self, seg: PathSegment) {
if !matches!(seg, PathSegment::Empty) {
self.path.push(seg);
}
}
fn pop(&mut self) {
self.path.pop();
}
fn enter(&mut self, node: &dyn Visitable) {
let mut ctx = AdapterContext {
path: &self.path,
issues: &mut self.issues,
sanitize_write_context: None,
};
self.visitor.enter(node, &mut ctx);
}
fn exit(&mut self, node: &dyn Visitable) {
let mut ctx = AdapterContext {
path: &self.path,
issues: &mut self.issues,
sanitize_write_context: None,
};
self.visitor.exit(node, &mut ctx);
}
}
pub fn perform_visit<S: Into<PathSegment>>(
visitor: &mut dyn VisitorCore,
node: &dyn Visitable,
seg: S,
) {
let seg = seg.into();
let should_push = !matches!(seg, PathSegment::Empty);
if should_push {
visitor.push(seg);
}
visitor.enter(node);
node.drive(visitor);
visitor.exit(node);
if should_push {
visitor.pop();
}
}
pub(crate) trait VisitorMut {
fn enter_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
fn exit_mut(&mut self, node: &mut dyn Visitable, ctx: &mut dyn VisitorContext);
}
pub trait VisitorMutCore {
fn enter_mut(&mut self, node: &mut dyn Visitable);
fn exit_mut(&mut self, node: &mut dyn Visitable);
fn push(&mut self, _: PathSegment) {}
fn pop(&mut self) {}
}
pub(crate) struct VisitorMutAdapter<V> {
visitor: V,
path: Vec<PathSegment>,
issues: VisitorIssues,
sanitize_write_context: Option<SanitizeWriteContext>,
}
impl<V> VisitorMutAdapter<V>
where
V: VisitorMut,
{
pub(crate) const fn with_sanitize_write_context(
visitor: V,
sanitize_write_context: Option<SanitizeWriteContext>,
) -> Self {
Self {
visitor,
path: Vec::new(),
issues: VisitorIssues::new(),
sanitize_write_context,
}
}
pub(crate) fn result(self) -> Result<(), VisitorIssues> {
if self.issues.is_empty() {
Ok(())
} else {
Err(self.issues)
}
}
}
impl<V> VisitorMutCore for VisitorMutAdapter<V>
where
V: VisitorMut,
{
fn push(&mut self, seg: PathSegment) {
if !matches!(seg, PathSegment::Empty) {
self.path.push(seg);
}
}
fn pop(&mut self) {
self.path.pop();
}
fn enter_mut(&mut self, node: &mut dyn Visitable) {
let mut ctx = AdapterContext {
path: &self.path,
issues: &mut self.issues,
sanitize_write_context: self.sanitize_write_context,
};
self.visitor.enter_mut(node, &mut ctx);
}
fn exit_mut(&mut self, node: &mut dyn Visitable) {
let mut ctx = AdapterContext {
path: &self.path,
issues: &mut self.issues,
sanitize_write_context: self.sanitize_write_context,
};
self.visitor.exit_mut(node, &mut ctx);
}
}
pub fn perform_visit_mut<S: Into<PathSegment>>(
visitor: &mut dyn VisitorMutCore,
node: &mut dyn Visitable,
seg: S,
) {
let seg = seg.into();
let should_push = !matches!(seg, PathSegment::Empty);
if should_push {
visitor.push(seg);
}
visitor.enter_mut(node);
node.drive_mut(visitor);
visitor.exit_mut(node);
if should_push {
visitor.pop();
}
}