#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintViolation {
pub path: String,
pub message: String,
pub kind: ConstraintKind,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintError {
pub kind: ConstraintKind,
pub message: String,
}
impl core::fmt::Display for ConstraintError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ConstraintError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintKind {
MinLength,
MaxLength,
Pattern,
MinInclusive,
MaxInclusive,
TotalDigits,
FractionDigits,
}
pub trait Validatable {
fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>);
}
pub trait IsoMessage: Validatable {
fn message_type(&self) -> &'static str;
fn root_path(&self) -> &'static str;
fn validate_message(&self) -> Vec<ConstraintViolation> {
let mut violations = Vec::new();
self.validate_constraints(self.root_path(), &mut violations);
violations
}
}
impl<T: Validatable> Validatable for Option<T> {
fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
if let Some(ref inner) = self {
inner.validate_constraints(path, violations);
}
}
}
impl<T: Validatable> Validatable for Vec<T> {
fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
for (i, item) in self.iter().enumerate() {
let snap = violations.len();
item.validate_constraints("", violations);
if violations.len() > snap {
let pfx = format!("{path}[{i}]");
for v in &mut violations[snap..] {
v.path.insert_str(0, &pfx);
}
}
}
}
}
impl Validatable for String {
fn validate_constraints(&self, _path: &str, _violations: &mut Vec<ConstraintViolation>) {}
}
impl Validatable for bool {
fn validate_constraints(&self, _path: &str, _violations: &mut Vec<ConstraintViolation>) {}
}
impl<T: Validatable> Validatable for super::ChoiceWrapper<T> {
fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
self.inner.validate_constraints(path, violations);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constraint_error_display() {
let err = ConstraintError {
kind: ConstraintKind::Pattern,
message: "value does not match pattern [A-Z]{3,3}".to_string(),
};
assert_eq!(err.to_string(), "value does not match pattern [A-Z]{3,3}");
}
#[test]
fn constraint_error_debug() {
let err = ConstraintError {
kind: ConstraintKind::MaxLength,
message: "too long".to_string(),
};
let debug = format!("{err:?}");
assert!(debug.contains("ConstraintError"));
assert!(debug.contains("MaxLength"));
}
#[test]
fn constraint_error_eq() {
let a = ConstraintError {
kind: ConstraintKind::MinLength,
message: "too short".to_string(),
};
let b = ConstraintError {
kind: ConstraintKind::MinLength,
message: "too short".to_string(),
};
let c = ConstraintError {
kind: ConstraintKind::MaxLength,
message: "too short".to_string(),
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn constraint_error_is_std_error() {
let err = ConstraintError {
kind: ConstraintKind::Pattern,
message: "bad pattern".to_string(),
};
let _: &dyn std::error::Error = &err;
}
struct AlwaysViolates;
impl Validatable for AlwaysViolates {
fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
violations.push(ConstraintViolation {
path: path.to_string(),
message: "always fails".to_string(),
kind: ConstraintKind::Pattern,
});
}
}
#[test]
fn option_none_produces_no_violations() {
let val: Option<AlwaysViolates> = None;
let mut v = vec![];
val.validate_constraints("/root", &mut v);
assert!(v.is_empty());
}
#[test]
fn option_some_delegates_with_path() {
let val: Option<AlwaysViolates> = Some(AlwaysViolates);
let mut v = vec![];
val.validate_constraints("/root/field", &mut v);
assert_eq!(v.len(), 1);
assert_eq!(v[0].path, "/root/field");
}
#[test]
fn vec_empty_produces_no_violations() {
let val: Vec<AlwaysViolates> = vec![];
let mut v = vec![];
val.validate_constraints("/root", &mut v);
assert!(v.is_empty());
}
#[test]
fn vec_indexes_path_correctly() {
let val = vec![AlwaysViolates, AlwaysViolates, AlwaysViolates];
let mut v = vec![];
val.validate_constraints("/root/items", &mut v);
assert_eq!(v.len(), 3);
assert_eq!(v[0].path, "/root/items[0]");
assert_eq!(v[1].path, "/root/items[1]");
assert_eq!(v[2].path, "/root/items[2]");
}
#[test]
fn choice_wrapper_delegates_with_same_path() {
let val = super::super::ChoiceWrapper {
inner: AlwaysViolates,
};
let mut v = vec![];
val.validate_constraints("/root/choice", &mut v);
assert_eq!(v.len(), 1);
assert_eq!(v[0].path, "/root/choice");
}
#[test]
fn string_produces_no_violations() {
let val = "hello".to_string();
let mut v = vec![];
val.validate_constraints("/root", &mut v);
assert!(v.is_empty());
}
}