use core::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Violation {
pub pointer: String,
pub code: ViolationCode,
pub message: String,
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let at = if self.pointer.is_empty() { "/" } else { &self.pointer };
write!(f, "{at}: {}", self.message)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ViolationCode {
TooLong,
IllegalCharacter,
EmptyRequiredList,
OutOfRange,
Inconsistent,
MissingConditional,
Imprecise,
}
impl ViolationCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TooLong => "too_long",
Self::IllegalCharacter => "illegal_character",
Self::EmptyRequiredList => "empty_required_list",
Self::OutOfRange => "out_of_range",
Self::Inconsistent => "inconsistent",
Self::MissingConditional => "missing_conditional",
Self::Imprecise => "imprecise",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Violations(Vec<Violation>);
impl Violations {
#[must_use]
pub fn as_slice(&self) -> &[Violation] {
&self.0
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn into_vec(self) -> Vec<Violation> {
self.0
}
pub fn iter(&self) -> core::slice::Iter<'_, Violation> {
self.0.iter()
}
}
impl IntoIterator for Violations {
type Item = Violation;
type IntoIter = std::vec::IntoIter<Violation>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Violations {
type Item = &'a Violation;
type IntoIter = core::slice::Iter<'a, Violation>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl fmt::Display for Violations {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, v) in self.0.iter().enumerate() {
if i > 0 {
f.write_str("; ")?;
}
write!(f, "{v}")?;
}
Ok(())
}
}
impl std::error::Error for Violations {}
#[derive(Debug, Default)]
pub struct Validator {
path: String,
found: Vec<Violation>,
}
impl Validator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn report(&mut self, code: ViolationCode, message: impl Into<String>) {
self.found.push(Violation { pointer: self.path.clone(), code, message: message.into() });
}
pub fn report_at(&mut self, field: &str, code: ViolationCode, message: impl Into<String>) {
self.enter(field);
self.report(code, message);
self.leave();
}
pub fn enter(&mut self, segment: &str) {
self.path.push('/');
for ch in segment.chars() {
match ch {
'~' => self.path.push_str("~0"),
'/' => self.path.push_str("~1"),
c => self.path.push(c),
}
}
}
pub fn leave(&mut self) {
let cut = self.path.rfind('/').expect("leave() without a matching enter()");
self.path.truncate(cut);
}
pub fn field(&mut self, segment: &str, value: &impl Validate) {
self.enter(segment);
value.validate_in(self);
self.leave();
}
#[must_use]
pub fn pointer(&self) -> &str {
&self.path
}
#[must_use]
pub fn finish(self) -> Violations {
Violations(self.found)
}
}
pub trait Validate {
fn validate_in(&self, v: &mut Validator);
fn validate(&self) -> Result<(), Violations> {
let mut v = Validator::new();
self.validate_in(&mut v);
let found = v.finish();
if found.is_empty() { Ok(()) } else { Err(found) }
}
}
impl<T: Validate> Validate for Option<T> {
fn validate_in(&self, v: &mut Validator) {
if let Some(inner) = self {
inner.validate_in(v);
}
}
}
impl<T: Validate> Validate for Vec<T> {
fn validate_in(&self, v: &mut Validator) {
for (i, item) in self.iter().enumerate() {
v.enter(&i.to_string());
item.validate_in(v);
v.leave();
}
}
}
impl<T: Validate> Validate for Box<T> {
fn validate_in(&self, v: &mut Validator) {
T::validate_in(self, v);
}
}
macro_rules! impl_validate_noop {
($($t:ty),* $(,)?) => {
$(impl Validate for $t {
fn validate_in(&self, _v: &mut Validator) {}
})*
};
}
impl_validate_noop!(bool, i8, i16, i32, i64, u8, u16, u32, u64, usize, String, serde_json::Value);
macro_rules! validate_fields {
($self:ident, $v:ident, $($field:ident $(as $wire:literal)?),* $(,)?) => {
$( $v.field(validate_fields!(@wire $field $(, $wire)?), &$self.$field); )*
};
(@wire $field:ident) => { stringify!($field) };
(@wire $field:ident, $wire:literal) => { $wire };
}
pub(crate) use validate_fields;
#[cfg(test)]
mod tests {
use super::*;
struct Leaf(bool);
impl Validate for Leaf {
fn validate_in(&self, v: &mut Validator) {
if !self.0 {
v.report(ViolationCode::OutOfRange, "leaf is false");
}
}
}
#[test]
fn pointer_tracks_nesting_and_escapes_rfc6901() {
let mut v = Validator::new();
v.enter("a/b");
v.enter("c~d");
v.report(ViolationCode::TooLong, "boom");
v.leave();
v.leave();
let found = v.finish();
assert_eq!(found.as_slice()[0].pointer, "/a~1b/c~0d");
}
#[test]
fn vec_and_option_are_walked_with_indices() {
let value = vec![Leaf(true), Leaf(false), Leaf(false)];
let err = value.validate().unwrap_err();
assert_eq!(err.len(), 2);
assert_eq!(err.as_slice()[0].pointer, "/1");
assert_eq!(err.as_slice()[1].pointer, "/2");
assert!(Some(Leaf(true)).validate().is_ok());
assert!(Option::<Leaf>::None.validate().is_ok());
}
}