use std::sync::Arc;
pub trait Validate: Send + Sync + 'static {
fn check(&self, value: &str) -> Result<(), String>;
}
impl<F> Validate for F
where
F: Fn(&str) -> Result<(), String> + Send + Sync + 'static,
{
fn check(&self, value: &str) -> Result<(), String> {
self(value)
}
}
#[derive(Clone)]
pub struct Validator(Arc<dyn Validate>);
impl Validator {
pub fn new<V: Validate>(v: V) -> Self {
Self(Arc::new(v))
}
pub fn check(&self, v: &str) -> Result<(), String> {
self.0.check(v)
}
pub fn and(self, other: Validator) -> Validator {
let a = self.0;
let b = other.0;
Validator::new(move |v: &str| {
a.check(v)?;
b.check(v)
})
}
pub fn or(self, other: Validator) -> Validator {
let a = self.0;
let b = other.0;
Validator::new(move |v: &str| {
if a.check(v).is_ok() {
return Ok(());
}
b.check(v)
})
}
pub fn msg(self, message: impl Into<String>) -> Validator {
let inner = self.0;
let msg = message.into();
Validator::new(move |v: &str| inner.check(v).map_err(|_| msg.clone()))
}
}
impl Validate for Validator {
fn check(&self, v: &str) -> Result<(), String> {
self.0.check(v)
}
}
impl<F> From<F> for Validator
where
F: Fn(&str) -> Result<(), String> + Send + Sync + 'static,
{
fn from(f: F) -> Self {
Validator::new(f)
}
}
pub fn required() -> Validator {
Validator::new(|v: &str| {
if v.trim().is_empty() {
Err("Value is required".into())
} else {
Ok(())
}
})
}
pub fn min_chars(n: usize) -> Validator {
Validator::new(move |v: &str| {
if v.chars().count() < n {
Err(format!("Must be at least {n} characters"))
} else {
Ok(())
}
})
}
pub fn max_chars(n: usize) -> Validator {
Validator::new(move |v: &str| {
if v.chars().count() > n {
Err(format!("Must be at most {n} characters"))
} else {
Ok(())
}
})
}
pub fn exact_chars(n: usize) -> Validator {
Validator::new(move |v: &str| {
let c = v.chars().count();
if c != n {
Err(format!("Must be exactly {n} characters (got {c})"))
} else {
Ok(())
}
})
}
pub fn word_count(n: usize) -> Validator {
Validator::new(move |v: &str| {
let c = v.split_whitespace().count();
if c != n {
Err(format!("Must be exactly {n} words (got {c})"))
} else {
Ok(())
}
})
}
pub fn words_between(min: usize, max: usize) -> Validator {
Validator::new(move |v: &str| {
let c = v.split_whitespace().count();
if c < min || c > max {
Err(format!("Must be {min}–{max} words (got {c})"))
} else {
Ok(())
}
})
}
pub fn has_upper() -> Validator {
Validator::new(|v: &str| {
if v.chars().any(|c| c.is_ascii_uppercase()) {
Ok(())
} else {
Err("Must contain an uppercase letter".into())
}
})
}
pub fn has_lower() -> Validator {
Validator::new(|v: &str| {
if v.chars().any(|c| c.is_ascii_lowercase()) {
Ok(())
} else {
Err("Must contain a lowercase letter".into())
}
})
}
pub fn has_digit() -> Validator {
Validator::new(|v: &str| {
if v.chars().any(|c| c.is_ascii_digit()) {
Ok(())
} else {
Err("Must contain a digit".into())
}
})
}
pub fn has_special() -> Validator {
Validator::new(|v: &str| {
if v.chars().any(|c| c.is_ascii_punctuation()) {
Ok(())
} else {
Err("Must contain a special character".into())
}
})
}
pub fn alpha_only() -> Validator {
Validator::new(|v: &str| {
if v.chars().all(|c| c.is_alphabetic()) {
Ok(())
} else {
Err("Letters only".into())
}
})
}
pub fn alphanumeric() -> Validator {
Validator::new(|v: &str| {
if v.chars().all(|c| c.is_alphanumeric()) {
Ok(())
} else {
Err("Letters and digits only".into())
}
})
}
pub fn only_chars(allowed: &'static str) -> Validator {
Validator::new(move |v: &str| {
if v.chars().all(|c| allowed.contains(c)) {
Ok(())
} else {
Err(format!("Allowed characters: {allowed}"))
}
})
}
pub fn forbid_chars(forbidden: &'static str) -> Validator {
Validator::new(move |v: &str| {
if let Some(c) = v.chars().find(|c| forbidden.contains(*c)) {
Err(format!("'{c}' is not allowed"))
} else {
Ok(())
}
})
}
pub fn int_between(min: i64, max: i64) -> Validator {
Validator::new(move |v: &str| match v.trim().parse::<i64>() {
Ok(n) if (min..=max).contains(&n) => Ok(()),
Ok(n) => Err(format!("Must be between {min} and {max} (got {n})")),
Err(_) => Err("Must be an integer".into()),
})
}
pub fn float_between(min: f64, max: f64) -> Validator {
Validator::new(move |v: &str| match v.trim().parse::<f64>() {
Ok(n) if n >= min && n <= max => Ok(()),
Ok(n) => Err(format!("Must be between {min} and {max} (got {n})")),
Err(_) => Err("Must be a number".into()),
})
}
pub fn email() -> Validator {
Validator::new(|v: &str| {
let s = v.trim();
let parts: Vec<&str> = s.splitn(2, '@').collect();
if parts.len() == 2
&& !parts[0].is_empty()
&& parts[1].contains('.')
&& !parts[1].starts_with('.')
&& !parts[1].ends_with('.')
{
Ok(())
} else {
Err("Must be a valid email".into())
}
})
}
pub fn starts_with(prefix: &'static str) -> Validator {
Validator::new(move |v: &str| {
if v.starts_with(prefix) {
Ok(())
} else {
Err(format!("Must start with `{prefix}`"))
}
})
}
pub fn ends_with(suffix: &'static str) -> Validator {
Validator::new(move |v: &str| {
if v.ends_with(suffix) {
Ok(())
} else {
Err(format!("Must end with `{suffix}`"))
}
})
}
pub fn one_of(values: &'static [&'static str]) -> Validator {
Validator::new(move |v: &str| {
if values.contains(&v) {
Ok(())
} else {
Err(format!("Must be one of: {}", values.join(", ")))
}
})
}