use std::collections::BTreeMap;
#[derive(Debug, Clone, Default)]
pub struct ValidationError {
fields: BTreeMap<String, String>,
}
impl ValidationError {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
self.fields.insert(field.into(), message.into());
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn fields(&self) -> &BTreeMap<String, String> {
&self.fields
}
pub fn finish(self) -> Result<(), Self> {
if self.is_empty() { Ok(()) } else { Err(self) }
}
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "validation failed")
}
}
impl std::error::Error for ValidationError {}
pub fn require_non_empty(value: &str, field: &str) -> Result<(), String> {
if value.trim().is_empty() {
Err(format!("{field} must not be empty"))
} else {
Ok(())
}
}
pub fn require_email(value: &str, field: &str) -> Result<(), String> {
let Some((user, domain)) = value.split_once('@') else {
return Err(format!("{field} must be a valid email"));
};
if user.is_empty()
|| domain.is_empty()
|| !domain.contains('.')
|| domain.starts_with('.')
|| domain.ends_with('.')
{
return Err(format!("{field} must be a valid email"));
}
Ok(())
}
pub fn require_in_range<T: PartialOrd + std::fmt::Display>(
value: T,
min: T,
max: T,
field: &str,
) -> Result<(), String> {
if value < min || value > max {
Err(format!("{field} must be between {min} and {max}"))
} else {
Ok(())
}
}
pub fn push_result(err: &mut ValidationError, field: &str, result: Result<(), String>) {
if let Err(message) = result {
err.add(field, message);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_field_fails() {
let msg = require_non_empty("", "name").unwrap_err();
assert!(msg.contains("name"));
}
#[test]
fn email_shape() {
assert!(require_email("a@b.com", "email").is_ok());
assert!(require_email("not-an-email", "email").is_err());
assert!(require_email("@x.com", "email").is_err());
assert!(require_email("a@com", "email").is_err());
}
#[test]
fn range() {
assert!(require_in_range(5u32, 1, 10, "n").is_ok());
assert!(require_in_range(0u32, 1, 10, "n").is_err());
}
#[test]
fn finish_ok_when_empty() {
ValidationError::new().finish().unwrap();
}
}