#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Violation {
pub field: Option<&'static str>,
pub message: &'static str,
}
impl Violation {
pub const fn new(message: &'static str) -> Self {
Self {
field: None,
message,
}
}
pub const fn with_field(field: &'static str, message: &'static str) -> Self {
Self {
field: Some(field),
message,
}
}
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.field {
Some(field) => write!(f, "{field}: {}", self.message),
None => f.write_str(self.message),
}
}
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
violations: Vec<Violation>,
}
#[cfg(feature = "alloc")]
pub type ValidateResult<T = ()> = Result<T, ValidationError>;
#[cfg(feature = "alloc")]
impl ValidationError {
pub fn new(message: &'static str) -> Self {
Self {
violations: alloc::vec![Violation::new(message)],
}
}
pub fn field(field: &'static str, message: &'static str) -> Self {
Self {
violations: alloc::vec![Violation::with_field(field, message)],
}
}
pub fn empty() -> Self {
Self {
violations: Vec::new(),
}
}
pub fn with(mut self, violation: Violation) -> Self {
self.violations.push(violation);
self
}
pub fn push(&mut self, violation: Violation) {
self.violations.push(violation);
}
pub fn merge(mut self, other: Self) -> Self {
self.violations.extend(other.violations);
self
}
pub fn require(mut self, condition: bool, violation: Violation) -> Self {
if !condition {
self.violations.push(violation);
}
self
}
pub fn require_field(
self,
condition: bool,
field: &'static str,
message: &'static str,
) -> Self {
self.require(condition, Violation::with_field(field, message))
}
pub fn finish(self) -> ValidateResult {
if self.is_empty() {
Ok(())
} else {
Err(self)
}
}
pub fn violations(&self) -> &[Violation] {
&self.violations
}
pub fn is_empty(&self) -> bool {
self.violations.is_empty()
}
pub fn len(&self) -> usize {
self.violations.len()
}
}
#[cfg(feature = "alloc")]
impl From<Violation> for ValidationError {
fn from(v: Violation) -> Self {
Self {
violations: alloc::vec![v],
}
}
}
#[cfg(feature = "alloc")]
impl From<&'static str> for ValidationError {
fn from(message: &'static str) -> Self {
Self::new(message)
}
}
#[cfg(feature = "alloc")]
impl FromIterator<Violation> for ValidationError {
fn from_iter<I: IntoIterator<Item = Violation>>(iter: I) -> Self {
Self {
violations: iter.into_iter().collect(),
}
}
}
#[cfg(feature = "alloc")]
impl Extend<Violation> for ValidationError {
fn extend<I: IntoIterator<Item = Violation>>(&mut self, iter: I) {
self.violations.extend(iter);
}
}
#[cfg(feature = "alloc")]
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.violations.as_slice() {
[] => f.write_str("validation failed"),
[single] => fmt::Display::fmt(single, f),
violations => {
for (i, v) in violations.iter().enumerate() {
if i > 0 {
write!(f, "; ")?;
}
fmt::Display::fmt(v, f)?;
}
Ok(())
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ValidationError {}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::{ValidateResult, ValidationError, Violation};
use alloc::string::ToString;
#[test]
fn violation_new() {
let v = Violation::new("must not be empty");
assert_eq!(v.field, None);
assert_eq!(v.message, "must not be empty");
}
#[test]
fn violation_with_field() {
let v = Violation::with_field("email", "invalid format");
assert_eq!(v.field, Some("email"));
assert_eq!(v.message, "invalid format");
}
#[test]
fn violation_display_no_field() {
assert_eq!(Violation::new("bad value").to_string(), "bad value");
}
#[test]
fn violation_display_with_field() {
assert_eq!(
Violation::with_field("age", "must be positive").to_string(),
"age: must be positive"
);
}
#[test]
fn validation_error_single_violation() {
let e = ValidationError::new("value is required");
assert_eq!(e.len(), 1);
assert!(!e.is_empty());
assert_eq!(e.to_string(), "value is required");
}
#[test]
fn validation_error_field() {
let e = ValidationError::field("name", "too short");
assert_eq!(e.violations()[0].field, Some("name"));
assert_eq!(e.to_string(), "name: too short");
}
#[test]
fn validation_error_empty() {
let e = ValidationError::empty();
assert!(e.is_empty());
assert_eq!(e.len(), 0);
assert_eq!(e.to_string(), "validation failed");
}
#[test]
fn validation_error_add_chaining() {
let e = ValidationError::empty()
.with(Violation::with_field("name", "too short"))
.with(Violation::with_field("email", "invalid format"));
assert_eq!(e.len(), 2);
}
#[test]
fn validation_error_push() {
let mut e = ValidationError::empty();
e.push(Violation::new("first"));
e.push(Violation::new("second"));
assert_eq!(e.len(), 2);
}
#[test]
fn validation_error_merge() {
let a = ValidationError::new("first");
let b = ValidationError::new("second");
let merged = a.merge(b);
assert_eq!(merged.len(), 2);
}
#[test]
fn validation_error_display_multiple() {
let e = ValidationError::empty()
.with(Violation::new("first error"))
.with(Violation::new("second error"));
assert_eq!(e.to_string(), "first error; second error");
}
#[test]
fn validation_error_from_violation() {
let e = ValidationError::from(Violation::new("bad"));
assert_eq!(e.len(), 1);
}
#[test]
fn validation_error_from_str() {
let e = ValidationError::from("bad input");
assert_eq!(e.violations()[0].message, "bad input");
}
#[test]
fn validate_result_type_alias() {
let ok: ValidateResult = Ok(());
let err: ValidateResult = Err(ValidationError::new("fail"));
assert!(ok.is_ok());
assert!(err.is_err());
}
#[test]
fn require_adds_only_on_false_condition() {
let e = ValidationError::empty()
.require(true, Violation::new("kept ok"))
.require(false, Violation::with_field("name", "must not be empty"))
.require_field(false, "age", "must be at least 18");
assert_eq!(e.len(), 2);
assert_eq!(e.violations()[0].field, Some("name"));
assert_eq!(e.violations()[1].field, Some("age"));
}
#[test]
fn finish_maps_empty_to_ok_and_nonempty_to_err() {
assert!(ValidationError::empty().finish().is_ok());
let all_passing = ValidationError::empty()
.require(true, Violation::new("a"))
.require(true, Violation::new("b"))
.finish();
assert!(all_passing.is_ok());
let failing = ValidationError::empty()
.require_field(false, "x", "bad")
.finish();
assert_eq!(failing.unwrap_err().len(), 1);
}
#[test]
fn from_iter_and_extend_collect_violations() {
let e: ValidationError = [
Violation::new("first"),
Violation::with_field("name", "second"),
]
.into_iter()
.collect();
assert_eq!(e.len(), 2);
let mut acc = ValidationError::empty();
acc.extend([Violation::new("a"), Violation::new("b")]);
assert_eq!(acc.len(), 2);
}
}