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>,
}
#[derive(Clone)]
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 with_array_items<'a, T: 'a>(
&mut self,
iter: impl Iterator<Item = &'a T>,
mut f: impl FnMut(&mut ValidationCtx, &T),
) {
for (i, item) in iter.enumerate() {
self.with_elem(LocationElem::Index(i), |ctx| f(ctx, item))
}
}
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;
if self.location.len() < 2 {
return f.write_str("no parent location available");
}
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.with_array_items(self.iter(), |ctx, item| 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) {
if let Some(b) = self.as_ref() {
b.validate_impl(ctx);
}
}
}
impl<T: Validate> Validate for Option<T> {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
if let Some(t) = self {
t.validate_impl(ctx)
}
}
}
impl<T: Validate> Validate for BTreeSet<T> {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
ctx.with_array_items(self.iter(), |ctx, item| item.validate_impl(ctx))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanity_check_array_validation() {
#[derive(Clone, Debug, Copy)]
struct Derp(i16);
struct DerpStore {
derps: Vec<Derp>,
}
impl Validate for Derp {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
if self.0 > 7 {
ctx.report("this derp is too big!!");
}
}
}
impl Validate for DerpStore {
fn validate_impl(&self, ctx: &mut ValidationCtx) {
ctx.in_table("DerpStore", |ctx| {
ctx.in_field("derps", |ctx| self.derps.validate_impl(ctx))
})
}
}
let my_derps = DerpStore {
derps: [1i16, 0, 3, 4, 12, 7, 6].into_iter().map(Derp).collect(),
};
let report = my_derps.validate().err().unwrap();
assert_eq!(report.errors.len(), 1);
assert!(report.to_string().contains(".derps[4]"));
}
}