use crate::error::{Error, ValidationErrors};
pub const EMAIL_PATTERN: &str = r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$";
pub const ALPHANUMERIC_PATTERN: &str = r"^[a-zA-Z0-9]+$";
pub const SLUG_PATTERN: &str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$";
pub struct Validator {
errors: ValidationErrors,
}
impl Default for Validator {
fn default() -> Self {
Self::new()
}
}
impl Validator {
pub fn new() -> Self {
Self {
errors: ValidationErrors::new(),
}
}
pub fn str_field(&mut self, value: &str, name: &'static str) -> StrFieldValidator<'_> {
StrFieldValidator {
validator: self,
name,
value: value.to_owned(),
}
}
pub fn i64_field(&mut self, value: i64, name: &'static str) -> I64FieldValidator<'_> {
I64FieldValidator {
validator: self,
name,
value,
}
}
pub fn check(self) -> crate::error::Result<()> {
if self.errors.is_empty() {
Ok(())
} else {
Err(Error::Validation(self.errors))
}
}
}
pub struct StrFieldValidator<'a> {
validator: &'a mut Validator,
name: &'static str,
value: String,
}
impl<'a> StrFieldValidator<'a> {
pub fn not_empty(&mut self) -> &mut Self {
if self.value.trim().is_empty() {
self.validator
.errors
.add(self.name, format!("{} must not be empty", self.name));
}
self
}
pub fn max_length(&mut self, max: usize) -> &mut Self {
if self.value.len() > max {
self.validator.errors.add(
self.name,
format!("{} must be at most {max} characters", self.name),
);
}
self
}
pub fn min_length(&mut self, min: usize) -> &mut Self {
if self.value.len() < min {
self.validator.errors.add(
self.name,
format!("{} must be at least {min} characters", self.name),
);
}
self
}
pub fn pattern(&mut self, regex: &str, message: &str) -> &mut Self {
match regex::Regex::new(regex) {
Ok(re) => {
if !re.is_match(&self.value) {
self.validator
.errors
.add(self.name, format!("{} {message}", self.name));
}
}
Err(_) => {
self.validator.errors.add(
self.name,
format!("{}: invalid validation pattern", self.name),
);
}
}
self
}
}
pub struct I64FieldValidator<'a> {
validator: &'a mut Validator,
name: &'static str,
value: i64,
}
impl<'a> I64FieldValidator<'a> {
pub fn range(&mut self, min: i64, max: i64) -> &mut Self {
if self.value < min || self.value > max {
self.validator.errors.add(
self.name,
format!("{} must be between {min} and {max}", self.name),
);
}
self
}
pub fn positive(&mut self) -> &mut Self {
if self.value <= 0 {
self.validator
.errors
.add(self.name, format!("{} must be positive", self.name));
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_empty_rejects_empty_string() {
let mut v = Validator::new();
v.str_field("", "title").not_empty();
assert!(v.check().is_err());
}
#[test]
fn not_empty_rejects_whitespace_only() {
let mut v = Validator::new();
v.str_field(" ", "title").not_empty();
assert!(v.check().is_err());
}
#[test]
fn not_empty_accepts_valid_string() {
let mut v = Validator::new();
v.str_field("hello", "title").not_empty();
assert!(v.check().is_ok());
}
#[test]
fn max_length_at_boundary() {
let mut v = Validator::new();
v.str_field("abc", "name").max_length(3);
assert!(v.check().is_ok());
}
#[test]
fn max_length_over_boundary() {
let mut v = Validator::new();
v.str_field("abcd", "name").max_length(3);
assert!(v.check().is_err());
}
#[test]
fn min_length_at_boundary() {
let mut v = Validator::new();
v.str_field("ab", "password").min_length(2);
assert!(v.check().is_ok());
}
#[test]
fn min_length_under_boundary() {
let mut v = Validator::new();
v.str_field("a", "password").min_length(2);
assert!(v.check().is_err());
}
#[test]
fn pattern_valid_email() {
let mut v = Validator::new();
v.str_field("user@example.com", "email")
.pattern(EMAIL_PATTERN, "must be a valid email");
assert!(v.check().is_ok());
}
#[test]
fn pattern_invalid_email() {
let mut v = Validator::new();
v.str_field("not-an-email", "email")
.pattern(EMAIL_PATTERN, "must be a valid email");
assert!(v.check().is_err());
}
#[test]
fn pattern_invalid_regex_adds_error() {
let mut v = Validator::new();
v.str_field("anything", "field").pattern("[invalid", "msg");
assert!(v.check().is_err());
}
#[test]
fn range_at_lower_boundary() {
let mut v = Validator::new();
v.i64_field(1, "priority").range(1, 5);
assert!(v.check().is_ok());
}
#[test]
fn range_at_upper_boundary() {
let mut v = Validator::new();
v.i64_field(5, "priority").range(1, 5);
assert!(v.check().is_ok());
}
#[test]
fn range_below_lower_boundary() {
let mut v = Validator::new();
v.i64_field(0, "priority").range(1, 5);
assert!(v.check().is_err());
}
#[test]
fn range_above_upper_boundary() {
let mut v = Validator::new();
v.i64_field(6, "priority").range(1, 5);
assert!(v.check().is_err());
}
#[test]
fn positive_rejects_zero() {
let mut v = Validator::new();
v.i64_field(0, "count").positive();
assert!(v.check().is_err());
}
#[test]
fn positive_rejects_negative() {
let mut v = Validator::new();
v.i64_field(-1, "count").positive();
assert!(v.check().is_err());
}
#[test]
fn positive_accepts_one() {
let mut v = Validator::new();
v.i64_field(1, "count").positive();
assert!(v.check().is_ok());
}
#[test]
fn multiple_fields_collect_errors() {
let mut v = Validator::new();
v.str_field("", "title").not_empty();
v.i64_field(0, "priority").range(1, 5);
let err = v.check().unwrap_err();
if let Error::Validation(errors) = err {
assert!(errors.errors.contains_key("title"));
assert!(errors.errors.contains_key("priority"));
} else {
panic!("expected Validation error");
}
}
#[test]
fn check_returns_ok_when_no_errors() {
let v = Validator::new();
assert!(v.check().is_ok());
}
#[test]
fn error_messages_reference_field_names() {
let mut v = Validator::new();
v.str_field("secret_value", "username")
.not_empty()
.min_length(50);
let err = v.check().unwrap_err();
let msg = err.to_string();
assert!(msg.contains("username"));
assert!(!msg.contains("secret_value"));
}
#[test]
fn chained_string_rules() {
let mut v = Validator::new();
v.str_field("", "title").not_empty().max_length(255);
let err = v.check().unwrap_err();
if let Error::Validation(errors) = err {
let title_errors = &errors.errors["title"];
assert_eq!(title_errors.len(), 1); } else {
panic!("expected Validation error");
}
}
#[test]
fn slug_pattern_valid() {
let mut v = Validator::new();
v.str_field("my-cool-slug", "slug")
.pattern(SLUG_PATTERN, "must be a valid slug");
assert!(v.check().is_ok());
}
#[test]
fn slug_pattern_invalid() {
let mut v = Validator::new();
v.str_field("NOT A SLUG!", "slug")
.pattern(SLUG_PATTERN, "must be a valid slug");
assert!(v.check().is_err());
}
}