use serde_json::Value;
use super::{Validator, ValidatorOptions};
use crate::errors::{ErrorType, Errors};
#[derive(Debug, Clone, Default)]
pub struct ExclusionValidator {
values: Vec<Value>,
message: Option<String>,
pub(crate) options: ValidatorOptions,
}
impl ExclusionValidator {
#[must_use]
pub fn new<T>(values: T) -> Self
where
T: Into<Vec<Value>>,
{
Self {
values: values.into(),
message: None,
options: ValidatorOptions::default(),
}
}
crate::validations::impl_common_validator_methods!();
#[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("is reserved"))
}
}
impl Validator for ExclusionValidator {
fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
if value.is_some_and(|candidate| self.values.iter().any(|forbidden| forbidden == candidate))
{
errors.add(attribute, ErrorType::Exclusion, self.error_message());
}
}
fn name(&self) -> &str {
"exclusion"
}
fn options(&self) -> &ValidatorOptions {
&self.options
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use serde_json::json;
use super::ExclusionValidator;
use crate::{
errors::{ErrorType, Errors},
validations::{ValidationSet, Validator},
};
fn validate_exclusion(
validator: ExclusionValidator,
value: Option<serde_json::Value>,
) -> Errors {
let mut errors = Errors::new();
validator.validate("field", value.as_ref(), &mut errors);
errors
}
#[test]
fn rejects_forbidden_value() {
let validator = ExclusionValidator::new(vec![json!("admin")]);
let mut errors = Errors::new();
validator.validate("role", Some(&json!("admin")), &mut errors);
assert_eq!(errors.on("role")[0].error_type, ErrorType::Exclusion);
}
#[test]
fn allows_non_member() {
let validator = ExclusionValidator::new(vec![json!("admin")]);
let mut errors = Errors::new();
validator.validate("role", Some(&json!("user")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn allows_nil_value() {
let validator = ExclusionValidator::new(vec![json!("admin")]);
let mut errors = Errors::new();
validator.validate("role", None, &mut errors);
assert!(errors.is_empty());
}
#[test]
fn custom_message_is_used() {
let validator = ExclusionValidator::new(vec![json!("root")]).message("blocked");
let mut errors = Errors::new();
validator.validate("username", Some(&json!("root")), &mut errors);
assert_eq!(errors.on("username")[0].message, "blocked");
}
#[test]
fn rejects_null_when_null_is_forbidden() {
let errors = validate_exclusion(
ExclusionValidator::new(vec![json!(null)]),
Some(json!(null)),
);
assert_eq!(errors.on("field")[0].error_type, ErrorType::Exclusion);
}
#[test]
fn allows_null_when_null_is_not_forbidden() {
let errors = validate_exclusion(ExclusionValidator::new(vec![json!(1)]), Some(json!(null)));
assert!(errors.is_empty());
}
#[test]
fn allows_any_value_when_forbidden_set_is_empty() {
let errors = validate_exclusion(ExclusionValidator::new(Vec::new()), Some(json!("guest")));
assert!(errors.is_empty());
}
#[test]
fn rejects_forbidden_object_by_exact_equality() {
let errors = validate_exclusion(
ExclusionValidator::new(vec![json!({ "kind": "vip", "level": 2 })]),
Some(json!({ "kind": "vip", "level": 2 })),
);
assert_eq!(errors.on("field")[0].error_type, ErrorType::Exclusion);
}
#[test]
fn rejects_forbidden_array_by_exact_equality() {
let errors = validate_exclusion(
ExclusionValidator::new(vec![json!([1, 2])]),
Some(json!([1, 2])),
);
assert_eq!(errors.on("field")[0].error_type, ErrorType::Exclusion);
}
#[test]
fn distinguishes_strings_from_numbers() {
let errors = validate_exclusion(ExclusionValidator::new(vec![json!(1)]), Some(json!("1")));
assert!(errors.is_empty());
}
#[test]
fn allow_nil_skips_missing_values_in_validation_set() {
let mut set = ValidationSet::new();
set.add(
"role",
ExclusionValidator::new(vec![json!("admin")]).allow_nil(),
);
let mut errors = Errors::new();
let _ = set.validate(&|_| None, &mut errors);
assert!(errors.is_empty());
}
#[test]
fn allow_blank_skips_blank_values_in_validation_set() {
let mut set = ValidationSet::new();
set.add(
"role",
ExclusionValidator::new(vec![json!("admin")]).allow_blank(),
);
let attrs = HashMap::from([("role".to_string(), json!(" "))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn allow_blank_does_not_skip_non_blank_forbidden_values() {
let mut set = ValidationSet::new();
set.add(
"role",
ExclusionValidator::new(vec![json!("admin")]).allow_blank(),
);
let attrs = HashMap::from([("role".to_string(), json!("admin"))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert_eq!(errors.on("role")[0].error_type, ErrorType::Exclusion);
}
#[test]
fn multiple_forbidden_values_still_report_single_error() {
let errors = validate_exclusion(
ExclusionValidator::new(vec![json!("admin"), json!("root")]),
Some(json!("root")),
);
assert_eq!(errors.count(), 1);
}
#[test]
fn full_message_humanizes_attribute_name() {
let mut errors = Errors::new();
ExclusionValidator::new(vec![json!("root")]).validate(
"user_role",
Some(&json!("root")),
&mut errors,
);
assert_eq!(
errors.full_messages(),
vec!["User role is reserved".to_string()]
);
}
}