use std::{
collections::BTreeSet,
fmt::{Debug, Display},
ops::Deref,
};
use crate::offsets::{NullableOffsetMarker, OffsetMarker};
pub trait Validate {
fn validate(&self) -> Result<(), ValidationReport> {
let mut ctx = Default::default();
self.validate_impl(&mut ctx);
if ctx.errors.is_empty() {
Ok(())
} else {
Err(ValidationReport { errors: ctx.errors })
}
}
#[allow(unused_variables)]
fn validate_impl(&self, ctx: &mut ValidationCtx);
}
#[derive(Clone, Debug, Default)]
pub struct ValidationCtx {
cur_location: Vec<LocationElem>,
errors: Vec<ValidationError>,
}
#[derive(Debug, Clone)]
struct ValidationError {
error: String,
location: Vec<LocationElem>,
}
pub struct ValidationReport {
errors: Vec<ValidationError>,
}
#[derive(Debug, Clone)]
enum LocationElem {
Table(&'static str),
Field(&'static str),
Index(usize),
}
impl ValidationCtx {
pub fn in_table(&mut self, name: &'static str, f: impl FnOnce(&mut ValidationCtx)) {
self.with_elem(LocationElem::Table(name), f);
}
pub fn in_field(&mut self, name: &'static str, f: impl FnOnce(&mut ValidationCtx)) {
self.with_elem(LocationElem::Field(name), f);
}
pub fn in_array(&mut self, f: impl FnOnce(&mut ValidationCtx)) {
self.with_elem(LocationElem::Index(0), f);
}
pub fn array_item(&mut self, f: impl FnOnce(&mut ValidationCtx)) {
assert!(matches!(
self.cur_location.last(),
Some(LocationElem::Index(_))
));
f(self);
match self.cur_location.last_mut() {
Some(LocationElem::Index(i)) => *i += 1,
_ => panic!("array_item called outside of array"),
}
}
pub fn report(&mut self, msg: impl Display) {
self.errors.push(ValidationError {
location: self.cur_location.clone(),
error: msg.to_string(),
});
}
fn with_elem(&mut self, elem: LocationElem, f: impl FnOnce(&mut ValidationCtx)) {
self.cur_location.push(elem);
f(self);
self.cur_location.pop();
}
}
impl Display for ValidationReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.errors.len() == 1 {
return writeln!(f, "Validation error:\n{}", self.errors.first().unwrap());
}
writeln!(f, "{} validation errors:", self.errors.len())?;
for (i, error) in self.errors.iter().enumerate() {
writeln!(f, "#{}\n{error}", i + 1)?;
}
Ok(())
}
}
impl Debug for ValidationReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}
static MANY_SPACES: &str = " ";
impl Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "\"{}\"", self.error)?;
let mut indent = 0;
for (i, window) in self.location.windows(2).enumerate() {
let prev = &window[0];
let current = &window[1];
if i == 0 {
if let LocationElem::Table(name) = prev {
write!(f, "in: {name}")?;
} else {
panic!("first item always table");
}
}
match current {
LocationElem::Table(name) => {
indent += 1;
let indent_str = &MANY_SPACES[..indent * 2];
write!(f, "\n{indent_str}{name}")
}
LocationElem::Field(name) => write!(f, ".{name}"),
LocationElem::Index(idx) => write!(f, "[{idx}]"),
}?;
}
writeln!(f)
}
}
impl<T: Validate> Validate for Vec<T> {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
ctx.in_array(|ctx| {
for item in self.iter() {
ctx.array_item(|ctx| {
item.validate_impl(ctx);
})
}
});
}
}
impl<const N: usize, T: Validate> Validate for OffsetMarker<T, N> {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
self.deref().validate_impl(ctx)
}
}
impl<const N: usize, T: Validate> Validate for NullableOffsetMarker<T, N> {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
self.deref().validate_impl(ctx)
}
}
impl<T: Validate> Validate for Option<T> {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
match self {
Some(t) => t.validate_impl(ctx),
None => (),
}
}
}
impl<T: Validate> Validate for BTreeSet<T> {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
ctx.in_array(|ctx| {
for item in self.iter() {
ctx.array_item(|ctx| {
item.validate_impl(ctx);
})
}
});
}
}