use std::fmt;
use serde_json::Value;
use super::{Validator, ValidatorOptions};
use crate::errors::{ErrorType, Errors};
type UniquenessCheckFn = dyn Fn(&str, &Value) -> bool + Send + Sync;
#[derive(Default)]
pub struct UniquenessValidator {
pub scope: Vec<String>,
pub case_sensitive: bool,
pub message: Option<String>,
pub check: Option<Box<UniquenessCheckFn>>,
pub(crate) options: ValidatorOptions,
}
impl fmt::Debug for UniquenessValidator {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("UniquenessValidator")
.field("has_check", &self.check.is_some())
.field("message", &self.message)
.field("options", &self.options)
.finish()
}
}
impl UniquenessValidator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
crate::validations::impl_common_validator_methods!();
#[must_use]
pub fn with_check<F>(mut self, check: F) -> Self
where
F: Fn(&str, &Value) -> bool + Send + Sync + 'static,
{
self.check = Some(Box::new(check));
self
}
#[must_use]
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
fn error_message(&self) -> String {
self.message
.clone()
.unwrap_or_else(|| String::from("has already been taken"))
}
}
impl Validator for UniquenessValidator {
fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
let Some(check) = &self.check else {
return;
};
let Some(value) = value else {
return;
};
if !check(attribute, value) {
errors.add(attribute, ErrorType::Taken, self.error_message());
}
}
fn name(&self) -> &str {
"uniqueness"
}
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::UniquenessValidator;
use crate::{
errors::{ErrorType, Errors},
validations::{ValidationContext, ValidationSet, Validator},
};
#[test]
fn passes_without_check_function() {
let validator = UniquenessValidator::new();
let mut errors = Errors::new();
validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn failing_check_marks_value_as_taken() {
let validator = UniquenessValidator::new().with_check(|_, value| value != &json!("taken"));
let mut errors = Errors::new();
validator.validate("slug", Some(&json!("taken")), &mut errors);
assert_eq!(errors.on("slug")[0].error_type, ErrorType::Taken);
}
#[test]
fn successful_check_passes() {
let validator = UniquenessValidator::new().with_check(|_, value| value == &json!("free"));
let mut errors = Errors::new();
validator.validate("slug", Some(&json!("free")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn custom_message_is_used() {
let validator = UniquenessValidator::new()
.with_check(|_, _| false)
.message("already used");
let mut errors = Errors::new();
validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
assert_eq!(errors.on("email")[0].message, "already used");
}
#[test]
fn missing_values_are_ignored_even_with_a_check() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let validator = UniquenessValidator::new().with_check(move |_, _| {
called_clone.store(true, Ordering::Relaxed);
true
});
let mut errors = Errors::new();
validator.validate("email", None, &mut errors);
assert!(errors.is_empty());
assert!(!called.load(Ordering::Relaxed));
}
#[test]
fn check_receives_attribute_name() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let validator = UniquenessValidator::new().with_check(move |attribute, _| {
called_clone.store(attribute == "email", Ordering::Relaxed);
true
});
let mut errors = Errors::new();
validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
assert!(called.load(Ordering::Relaxed));
}
#[test]
fn check_receives_attribute_value() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let validator = UniquenessValidator::new().with_check(move |_, value| {
called_clone.store(value == &json!("alice@example.com"), Ordering::Relaxed);
true
});
let mut errors = Errors::new();
validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
assert!(called.load(Ordering::Relaxed));
}
#[test]
fn allow_nil_skips_null_values_in_validation_set() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"email",
UniquenessValidator::new()
.with_check(move |_, _| {
called_clone.store(true, Ordering::Relaxed);
false
})
.allow_nil(),
);
let attrs = HashMap::from([("email".to_string(), json!(null))]);
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 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(
"email",
UniquenessValidator::new()
.with_check(move |_, _| {
called_clone.store(true, Ordering::Relaxed);
false
})
.allow_blank(),
);
let attrs = HashMap::from([("email".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 allow_blank_does_not_skip_present_values_in_validation_set() {
let mut set = ValidationSet::new();
set.add(
"email",
UniquenessValidator::new()
.with_check(|_, _| false)
.allow_blank(),
);
let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert_eq!(errors.on("email")[0].error_type, ErrorType::Taken);
}
#[test]
fn on_context_filters_validation_set_execution() {
let mut set = ValidationSet::new();
set.add(
"email",
UniquenessValidator::new()
.with_check(|_, _| false)
.on(vec![ValidationContext::Update]),
);
let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
let mut errors = Errors::new();
let _ = set.validate_with_context(
&|name| attrs.get(name).cloned(),
&mut errors,
&ValidationContext::Create,
);
assert!(errors.is_empty());
let _ = set.validate_with_context(
&|name| attrs.get(name).cloned(),
&mut errors,
&ValidationContext::Update,
);
assert_eq!(errors.on("email")[0].error_type, ErrorType::Taken);
}
#[test]
fn if_condition_false_skips_uniqueness_check() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"email",
UniquenessValidator::new()
.with_check(move |_, _| {
called_clone.store(true, Ordering::Relaxed);
false
})
.if_cond(|value| value == &json!("other@example.com")),
);
let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
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 unless_condition_true_skips_uniqueness_check() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = Arc::clone(&called);
let mut set = ValidationSet::new();
set.add(
"email",
UniquenessValidator::new()
.with_check(move |_, _| {
called_clone.store(true, Ordering::Relaxed);
false
})
.unless_cond(|value| value == &json!("alice@example.com")),
);
let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
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 full_message_humanizes_attribute_name() {
let validator = UniquenessValidator::new().with_check(|_, _| false);
let mut errors = Errors::new();
validator.validate(
"email_address",
Some(&json!("alice@example.com")),
&mut errors,
);
assert_eq!(
errors.full_messages(),
vec!["Email address has already been taken".to_string()],
);
}
#[test]
fn debug_output_reports_check_presence_without_closure_details() {
let debug = format!("{:?}", UniquenessValidator::new().with_check(|_, _| true));
assert!(debug.contains("has_check"));
assert!(!debug.contains("0x"));
}
}