use crate::validation::{Rule, ValidationError};
use serde_json::Value;
use std::collections::HashMap;
pub struct Validator<'a> {
data: &'a Value,
rules: HashMap<String, Vec<Box<dyn Rule>>>,
custom_messages: HashMap<String, String>,
custom_attributes: HashMap<String, String>,
stop_on_first_failure: bool,
}
impl<'a> Validator<'a> {
pub fn new(data: &'a Value) -> Self {
Self {
data,
rules: HashMap::new(),
custom_messages: HashMap::new(),
custom_attributes: HashMap::new(),
stop_on_first_failure: false,
}
}
pub fn rule<R: Rule + 'static>(mut self, field: impl Into<String>, rule: R) -> Self {
let field = field.into();
self.rules
.entry(field)
.or_default()
.push(Box::new(rule) as Box<dyn Rule>);
self
}
pub fn rules(mut self, field: impl Into<String>, rules: Vec<Box<dyn Rule>>) -> Self {
self.rules.insert(field.into(), rules);
self
}
pub fn boxed_rules(mut self, field: impl Into<String>, rules: Vec<Box<dyn Rule>>) -> Self {
self.rules.insert(field.into(), rules);
self
}
pub fn message(mut self, key: impl Into<String>, message: impl Into<String>) -> Self {
self.custom_messages.insert(key.into(), message.into());
self
}
pub fn messages(mut self, messages: HashMap<String, String>) -> Self {
self.custom_messages.extend(messages);
self
}
pub fn attribute(mut self, field: impl Into<String>, name: impl Into<String>) -> Self {
self.custom_attributes.insert(field.into(), name.into());
self
}
pub fn attributes(mut self, attributes: HashMap<String, String>) -> Self {
self.custom_attributes.extend(attributes);
self
}
pub fn stop_on_first_failure(mut self) -> Self {
self.stop_on_first_failure = true;
self
}
pub fn validate(self) -> Result<(), ValidationError> {
let mut errors = ValidationError::new();
for (field, rules) in &self.rules {
let value = self.get_value(field);
let display_field = self.get_display_field(field);
let has_nullable = rules.iter().any(|r| r.name() == "nullable");
if has_nullable && value.is_null() {
continue;
}
for rule in rules {
if rule.name() == "nullable" {
continue;
}
if let Err(default_message) = rule.validate(&display_field, &value, self.data) {
let message_key = format!("{}.{}", field, rule.name());
let message = self
.custom_messages
.get(&message_key)
.cloned()
.unwrap_or(default_message);
errors.add(field, message);
}
}
if self.stop_on_first_failure && errors.has(field) {
break;
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn passes(&self) -> bool {
let mut errors = ValidationError::new();
for (field, rules) in &self.rules {
let value = self.get_value(field);
let display_field = self.get_display_field(field);
let has_nullable = rules.iter().any(|r| r.name() == "nullable");
if has_nullable && value.is_null() {
continue;
}
for rule in rules {
if rule.name() == "nullable" {
continue;
}
if rule.validate(&display_field, &value, self.data).is_err() {
errors.add(field, "failed");
}
}
}
errors.is_empty()
}
pub fn fails(&self) -> bool {
!self.passes()
}
fn get_value(&self, field: &str) -> Value {
get_nested_value(self.data, field)
.cloned()
.unwrap_or(Value::Null)
}
fn get_display_field(&self, field: &str) -> String {
self.custom_attributes
.get(field)
.cloned()
.unwrap_or_else(|| {
field.split('_').collect::<Vec<_>>().join(" ")
})
}
}
fn get_nested_value<'a>(data: &'a Value, path: &str) -> Option<&'a Value> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = data;
for part in parts {
if let Value::Object(map) = current {
current = map.get(part)?;
}
else if let Value::Array(arr) = current {
let index: usize = part.parse().ok()?;
current = arr.get(index)?;
} else {
return None;
}
}
Some(current)
}
pub fn validate<I, F>(data: &Value, rules: I) -> Result<(), ValidationError>
where
I: IntoIterator<Item = (F, Vec<Box<dyn Rule>>)>,
F: Into<String>,
{
let mut validator = Validator::new(data);
for (field, field_rules) in rules {
validator = validator.rules(field, field_rules);
}
validator.validate()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules;
use crate::validation::rules::*;
use serde_json::json;
#[test]
fn test_validator_passes() {
let data = json!({
"email": "test@example.com",
"name": "John Doe"
});
let result = Validator::new(&data)
.rules("email", rules![required(), email()])
.rules("name", rules![required(), string()])
.validate();
assert!(result.is_ok());
}
#[test]
fn test_validator_fails() {
let data = json!({
"email": "invalid-email",
"name": ""
});
let result = Validator::new(&data)
.rules("email", rules![required(), email()])
.rules("name", rules![required()])
.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.has("email"));
assert!(errors.has("name"));
}
#[test]
fn test_validator_custom_message() {
let data = json!({"email": ""});
let result = Validator::new(&data)
.rules("email", rules![required()])
.message("email.required", "We need your email!")
.validate();
let errors = result.unwrap_err();
assert_eq!(
errors.first("email"),
Some(&"We need your email!".to_string())
);
}
#[test]
fn test_validator_custom_attribute() {
let data = json!({"user_email": ""});
let validator = Validator::new(&data)
.rules("user_email", rules![required()])
.attribute("user_email", "email address");
let result = validator.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors.first("user_email").is_some(),
"Expected error for 'user_email'"
);
}
#[test]
fn test_validator_nullable() {
let data = json!({"nickname": null});
let result = Validator::new(&data)
.rules("nickname", rules![nullable(), string(), min(3)])
.validate();
assert!(result.is_ok());
}
#[test]
fn test_nested_value() {
let data = json!({
"user": {
"profile": {
"email": "test@example.com"
}
}
});
let value = get_nested_value(&data, "user.profile.email");
assert_eq!(value, Some(&json!("test@example.com")));
}
#[test]
fn test_validate_function() {
let data = json!({"email": "test@example.com"});
let result = validate(&data, vec![("email", rules![required(), email()])]);
assert!(result.is_ok());
}
#[test]
fn test_passes_and_fails() {
let data = json!({"email": "invalid"});
let validator = Validator::new(&data).rules("email", rules![email()]);
assert!(validator.fails());
}
}