use std::fmt;
use serde_json::Value;
use super::{Validator, ValidatorOptions};
use crate::errors::Errors;
type CustomValidateFn = dyn Fn(&str, Option<&Value>, &mut Errors) + Send + Sync;
pub struct CustomValidator {
pub validate_fn: Box<CustomValidateFn>,
pub(crate) options: ValidatorOptions,
}
impl fmt::Debug for CustomValidator {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CustomValidator")
.field("options", &self.options)
.finish()
}
}
impl CustomValidator {
#[must_use]
pub fn new<F>(validate_fn: F) -> Self
where
F: Fn(&str, Option<&Value>, &mut Errors) + Send + Sync + 'static,
{
Self {
validate_fn: Box::new(validate_fn),
options: ValidatorOptions::default(),
}
}
crate::validations::impl_common_validator_methods!();
}
impl Validator for CustomValidator {
fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
(self.validate_fn)(attribute, value, errors);
}
fn name(&self) -> &str {
"custom"
}
fn options(&self) -> &ValidatorOptions {
&self.options
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use serde_json::json;
use super::CustomValidator;
use crate::{
errors::{ErrorType, Errors},
validations::{ValidationContext, ValidationSet, Validator},
};
#[test]
fn invokes_user_closure() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let validator = CustomValidator::new(move |_attribute, _value, _errors| {
called_clone.store(true, Ordering::Relaxed);
});
let mut errors = Errors::new();
validator.validate("name", Some(&json!("Alice")), &mut errors);
assert!(called.load(Ordering::Relaxed));
}
#[test]
fn closure_can_add_errors() {
let validator = CustomValidator::new(|attribute, _value, errors| {
errors.add(
attribute,
ErrorType::Custom(String::from("custom")),
"failed",
);
});
let mut errors = Errors::new();
validator.validate("name", Some(&json!("Alice")), &mut errors);
assert_eq!(errors.on("name")[0].message, "failed");
}
#[test]
fn validation_set_can_skip_custom_validator_via_options() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let validator = CustomValidator::new(move |_attribute, _value, _errors| {
called_clone.store(true, Ordering::Relaxed);
})
.allow_nil();
let mut set = ValidationSet::new();
set.add("nickname", validator);
let mut errors = Errors::new();
let _ = set.validate(&|_| None, &mut errors);
assert!(errors.is_empty());
assert!(!called.load(Ordering::Relaxed));
}
#[test]
fn closure_receives_attribute_name() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let validator = CustomValidator::new(move |attribute, _value, _errors| {
called_clone.store(attribute == "email", Ordering::Relaxed);
});
let mut errors = Errors::new();
validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
assert!(called.load(Ordering::Relaxed));
}
#[test]
fn closure_receives_missing_values() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let validator = CustomValidator::new(move |_attribute, value, _errors| {
called_clone.store(value.is_none(), Ordering::Relaxed);
});
let mut errors = Errors::new();
validator.validate("email", None, &mut errors);
assert!(called.load(Ordering::Relaxed));
}
#[test]
fn allow_blank_skips_blank_values_in_validation_set() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"nickname",
CustomValidator::new(move |_attribute, _value, _errors| {
called_clone.store(true, Ordering::Relaxed);
})
.allow_blank(),
);
let attrs = HashMap::from([("nickname".to_string(), json!(" "))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(errors.is_empty());
assert!(!called.load(Ordering::Relaxed));
}
#[test]
fn on_context_runs_only_when_matching() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"name",
CustomValidator::new(move |_attribute, _value, _errors| {
called_clone.store(true, Ordering::Relaxed);
})
.on(vec![ValidationContext::Update]),
);
let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
let mut errors = Errors::new();
let _ = set.validate_with_context(
&|name| attrs.get(name).cloned(),
&mut errors,
&ValidationContext::Create,
);
assert!(!called.load(Ordering::Relaxed));
let _ = set.validate_with_context(
&|name| attrs.get(name).cloned(),
&mut errors,
&ValidationContext::Update,
);
assert!(called.load(Ordering::Relaxed));
}
#[test]
fn if_condition_false_skips_custom_validator() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"age",
CustomValidator::new(move |_attribute, _value, _errors| {
called_clone.store(true, Ordering::Relaxed);
})
.if_cond(|value| value == &json!(21)),
);
let attrs = HashMap::from([("age".to_string(), json!(18))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(!called.load(Ordering::Relaxed));
}
#[test]
fn unless_condition_true_skips_custom_validator() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"age",
CustomValidator::new(move |_attribute, _value, _errors| {
called_clone.store(true, Ordering::Relaxed);
})
.unless_cond(|value| value == &json!(18)),
);
let attrs = HashMap::from([("age".to_string(), json!(18))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(!called.load(Ordering::Relaxed));
}
#[test]
fn allow_nil_does_not_skip_present_values() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"name",
CustomValidator::new(move |_attribute, _value, _errors| {
called_clone.store(true, Ordering::Relaxed);
})
.allow_nil(),
);
let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(called.load(Ordering::Relaxed));
}
#[test]
fn custom_errors_full_message_humanizes_attribute_name() {
let validator = CustomValidator::new(|attribute, _value, errors| {
errors.add(
attribute,
ErrorType::Custom(String::from("state")),
"is unavailable",
);
});
let mut errors = Errors::new();
validator.validate("display_name", Some(&json!("Alice")), &mut errors);
assert_eq!(
errors.full_messages(),
vec!["Display name is unavailable".to_string()]
);
}
#[test]
fn debug_output_hides_closure_details() {
let debug = format!("{:?}", CustomValidator::new(|_, _, _| {}));
assert!(debug.contains("CustomValidator"));
assert!(!debug.contains("validate_fn"));
}
#[test]
fn validation_set_passes_value_through_to_closure() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"name",
CustomValidator::new(move |_attribute, value, _errors| {
called_clone.store(value == Some(&json!("Alice")), Ordering::Relaxed);
}),
);
let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(called.load(Ordering::Relaxed));
}
}