#![allow(clippy::type_complexity)]
use std::rc::Rc;
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ValidationError {
pub message: Arc<str>,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ValidationError {}
type Rule<T> = Rc<dyn Fn(&T) -> Result<(), ValidationError>>;
pub struct Validator<T: ?Sized> {
rules: Vec<Rule<T>>,
}
impl<T: ?Sized> std::fmt::Debug for Validator<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Validator")
.field("rules", &self.rules.len())
.finish()
}
}
impl<T: ?Sized> Clone for Validator<T> {
fn clone(&self) -> Self {
Self {
rules: self.rules.clone(),
}
}
}
impl<T: ?Sized> Validator<T> {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn rule(self, check: impl Fn(&T) -> Result<(), ValidationError> + 'static) -> Self {
let mut this = self;
this.rules.push(Rc::new(check));
this
}
pub fn validate(&self, value: &T) -> Result<(), ValidationError> {
for rule in &self.rules {
rule(value)?;
}
Ok(())
}
pub fn validate_all(&self, value: &T) -> Vec<ValidationError> {
self.rules
.iter()
.filter_map(|rule| rule(value).err())
.collect()
}
}
impl<T: ?Sized> Default for Validator<T> {
fn default() -> Self {
Self::new()
}
}
pub type StringValidator = Validator<str>;
impl Validator<str> {
pub fn required(self, msg: impl Into<Arc<str>>) -> Self {
let msg = msg.into();
self.rule(move |s: &str| {
if s.trim().is_empty() {
Err(ValidationError {
message: msg.clone(),
})
} else {
Ok(())
}
})
}
pub fn min_length(self, n: usize, msg: impl Into<Arc<str>>) -> Self {
let msg = msg.into();
self.rule(move |s: &str| {
if s.chars().count() < n {
Err(ValidationError {
message: msg.clone(),
})
} else {
Ok(())
}
})
}
pub fn max_length(self, n: usize, msg: impl Into<Arc<str>>) -> Self {
let msg = msg.into();
self.rule(move |s: &str| {
if s.chars().count() > n {
Err(ValidationError {
message: msg.clone(),
})
} else {
Ok(())
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn required_rejects_empty() {
let v = StringValidator::new().required(Arc::from("required"));
assert!(v.validate("").is_err());
assert!(v.validate(" ").is_err());
}
#[test]
fn required_accepts_non_empty() {
let v = StringValidator::new().required(Arc::from("required"));
assert!(v.validate("hello").is_ok());
}
#[test]
fn min_length_rejects_short() {
let v = StringValidator::new().min_length(3, Arc::from("too short"));
assert!(v.validate("ab").is_err());
}
#[test]
fn min_length_accepts_exact() {
let v = StringValidator::new().min_length(3, Arc::from("too short"));
assert!(v.validate("abc").is_ok());
}
#[test]
fn max_length_rejects_long() {
let v = StringValidator::new().max_length(5, Arc::from("too long"));
assert!(v.validate("abcdef").is_err());
}
#[test]
fn max_length_accepts_exact() {
let v = StringValidator::new().max_length(5, Arc::from("too long"));
assert!(v.validate("abcde").is_ok());
}
#[test]
fn validate_returns_first_failure() {
let v = StringValidator::new()
.required(Arc::from("required"))
.min_length(3, Arc::from("too short"));
let err = v.validate("").unwrap_err();
assert_eq!(&*err.message, "required");
}
#[test]
fn validate_all_collects_all_failures() {
let v = StringValidator::new()
.min_length(5, Arc::from("too short"))
.max_length(2, Arc::from("too long"));
let errs = v.validate_all("abc");
assert_eq!(errs.len(), 2);
assert_eq!(&*errs[0].message, "too short");
assert_eq!(&*errs[1].message, "too long");
}
#[test]
fn chained_rules_compose_left_to_right() {
let v = StringValidator::new()
.required(Arc::from("required"))
.min_length(3, Arc::from("min 3"))
.max_length(10, Arc::from("max 10"));
let err = v.validate("ab").unwrap_err();
assert_eq!(&*err.message, "min 3");
assert!(v.validate("hello").is_ok());
}
#[test]
fn empty_validator_always_succeeds() {
let v: StringValidator = Validator::new();
assert!(v.validate("").is_ok());
assert!(v.validate("anything").is_ok());
assert!(v.validate_all("").is_empty());
}
}