use serde_json::Value;
use super::{Validator, ValidatorOptions};
use crate::errors::{ErrorType, Errors};
#[derive(Debug, Clone, Default)]
pub struct InclusionValidator {
values: Vec<Value>,
message: Option<String>,
pub(crate) options: ValidatorOptions,
}
impl InclusionValidator {
#[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 not included in the list"))
}
}
impl Validator for InclusionValidator {
fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
if value.is_none_or(|candidate| !self.values.iter().any(|allowed| allowed == candidate)) {
errors.add(attribute, ErrorType::Inclusion, self.error_message());
}
}
fn name(&self) -> &str {
"inclusion"
}
fn options(&self) -> &ValidatorOptions {
&self.options
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use serde_json::json;
use super::InclusionValidator;
use crate::{
errors::{ErrorType, Errors},
validations::{ValidationSet, Validator},
};
fn validate_inclusion(
validator: InclusionValidator,
value: Option<serde_json::Value>,
) -> Errors {
let mut errors = Errors::new();
validator.validate("field", value.as_ref(), &mut errors);
errors
}
#[test]
fn allows_member_of_set() {
let validator = InclusionValidator::new(vec![json!("admin"), json!("editor")]);
let mut errors = Errors::new();
validator.validate("role", Some(&json!("admin")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn rejects_value_outside_set() {
let validator = InclusionValidator::new(vec![json!(1), json!(2)]);
let mut errors = Errors::new();
validator.validate("priority", Some(&json!(3)), &mut errors);
assert_eq!(errors.on("priority")[0].error_type, ErrorType::Inclusion);
}
#[test]
fn rejects_nil_value() {
let validator = InclusionValidator::new(vec![json!(true), json!(false)]);
let mut errors = Errors::new();
validator.validate("published", None, &mut errors);
assert_eq!(
errors.on("published")[0].message,
"is not included in the list"
);
}
#[test]
fn custom_message_is_used() {
let validator = InclusionValidator::new(vec![json!("usd")]).message("unsupported");
let mut errors = Errors::new();
validator.validate("currency", Some(&json!("eur")), &mut errors);
assert_eq!(errors.on("currency")[0].message, "unsupported");
}
#[test]
fn rejects_empty_allowed_list() {
let errors = validate_inclusion(InclusionValidator::new(Vec::new()), Some(json!("usd")));
assert_eq!(errors.on("field")[0].error_type, ErrorType::Inclusion);
}
#[test]
fn allows_null_when_null_is_in_allowed_list() {
let errors = validate_inclusion(
InclusionValidator::new(vec![json!(null)]),
Some(json!(null)),
);
assert!(errors.is_empty());
}
#[test]
fn rejects_null_when_null_is_not_in_allowed_list() {
let errors = validate_inclusion(InclusionValidator::new(vec![json!(1)]), Some(json!(null)));
assert_eq!(errors.on("field")[0].error_type, ErrorType::Inclusion);
}
#[test]
fn allows_object_members_by_exact_equality() {
let errors = validate_inclusion(
InclusionValidator::new(vec![json!({ "kind": "vip", "level": 2 })]),
Some(json!({ "kind": "vip", "level": 2 })),
);
assert!(errors.is_empty());
}
#[test]
fn allows_array_members_by_exact_equality() {
let errors = validate_inclusion(
InclusionValidator::new(vec![json!([1, 2]), json!([3, 4])]),
Some(json!([1, 2])),
);
assert!(errors.is_empty());
}
#[test]
fn distinguishes_strings_from_numbers() {
let errors = validate_inclusion(InclusionValidator::new(vec![json!(1)]), Some(json!("1")));
assert_eq!(errors.on("field")[0].error_type, ErrorType::Inclusion);
}
#[test]
fn allow_nil_skips_missing_values_in_validation_set() {
let mut set = ValidationSet::new();
set.add(
"role",
InclusionValidator::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",
InclusionValidator::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_non_members() {
let mut set = ValidationSet::new();
set.add(
"role",
InclusionValidator::new(vec![json!("admin")]).allow_blank(),
);
let attrs = HashMap::from([("role".to_string(), json!("guest"))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert_eq!(errors.on("role")[0].error_type, ErrorType::Inclusion);
}
#[test]
fn range_like_value_lists_are_supported() {
let errors = validate_inclusion(
InclusionValidator::new(vec![json!(1), json!(2), json!(3), json!(4)]),
Some(json!(3)),
);
assert!(errors.is_empty());
}
#[test]
fn full_message_humanizes_attribute_name() {
let mut errors = Errors::new();
InclusionValidator::new(vec![json!("usd")]).validate(
"preferred_currency",
Some(&json!("eur")),
&mut errors,
);
assert_eq!(
errors.full_messages(),
vec!["Preferred currency is not included in the list".to_string()],
);
}
}