use serde_json::Value;
use super::{Validator, ValidatorOptions};
use crate::errors::{ErrorType, Errors};
#[derive(Debug, Clone)]
pub struct AcceptanceValidator {
accepted_values: Vec<Value>,
message: Option<String>,
pub(crate) options: ValidatorOptions,
}
impl Default for AcceptanceValidator {
fn default() -> Self {
Self {
accepted_values: vec![
Value::Bool(true),
Value::String("1".to_string()),
Value::String("yes".to_string()),
],
message: None,
options: ValidatorOptions {
allow_nil: true,
..ValidatorOptions::default()
},
}
}
}
impl AcceptanceValidator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
crate::validations::impl_common_validator_methods!();
#[must_use]
pub fn accept<T>(mut self, values: T) -> Self
where
T: Into<Vec<Value>>,
{
self.accepted_values = values.into();
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("must be accepted"))
}
}
impl Validator for AcceptanceValidator {
fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
if value.is_some_and(|candidate| {
!self
.accepted_values
.iter()
.any(|accepted| accepted == candidate)
}) {
errors.add(attribute, ErrorType::Accepted, self.error_message());
}
}
fn name(&self) -> &str {
"acceptance"
}
fn options(&self) -> &ValidatorOptions {
&self.options
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use serde_json::json;
use super::AcceptanceValidator;
use crate::{
errors::{ErrorType, Errors},
validations::{ValidationSet, Validator},
};
fn validate_acceptance(
validator: AcceptanceValidator,
value: Option<serde_json::Value>,
) -> Errors {
let mut errors = Errors::new();
validator.validate("field", value.as_ref(), &mut errors);
errors
}
#[test]
fn nil_value_is_ignored() {
let validator = AcceptanceValidator::new();
let mut errors = Errors::new();
validator.validate("terms", None, &mut errors);
assert!(errors.is_empty());
}
#[test]
fn default_true_is_accepted() {
let validator = AcceptanceValidator::new();
let mut errors = Errors::new();
validator.validate("terms", Some(&json!(true)), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn default_yes_is_accepted() {
let validator = AcceptanceValidator::new();
let mut errors = Errors::new();
validator.validate("terms", Some(&json!("yes")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn blank_string_is_rejected() {
let validator = AcceptanceValidator::new();
let mut errors = Errors::new();
validator.validate("terms", Some(&json!("")), &mut errors);
assert_eq!(errors.on("terms")[0].error_type, ErrorType::Accepted);
}
#[test]
fn custom_accept_values_are_used() {
let validator = AcceptanceValidator::new().accept(vec![json!(1), json!("I agree")]);
let mut errors = Errors::new();
validator.validate("terms", Some(&json!(1)), &mut errors);
validator.validate("terms", Some(&json!("nope")), &mut errors);
assert_eq!(errors.count(), 1);
}
#[test]
fn validation_set_skips_nil_when_default_allow_nil_applies() {
let mut set = ValidationSet::new();
set.add("terms", AcceptanceValidator::new());
let mut errors = Errors::new();
let _ = set.validate(&|_| None, &mut errors);
assert!(errors.is_empty());
}
#[test]
fn default_string_one_is_accepted() {
let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!("1")));
assert!(errors.is_empty());
}
#[test]
fn false_is_rejected_by_default() {
let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!(false)));
assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
}
#[test]
fn no_is_rejected_by_default() {
let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!("no")));
assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
}
#[test]
fn numeric_one_is_not_equal_to_default_string_one() {
let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!(1)));
assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
}
#[test]
fn custom_accept_values_replace_defaults() {
let errors = validate_acceptance(
AcceptanceValidator::new().accept(vec![json!("I agree")]),
Some(json!(true)),
);
assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
}
#[test]
fn custom_message_is_used() {
let errors = validate_acceptance(
AcceptanceValidator::new().message("must be accepted explicitly"),
Some(json!(false)),
);
assert_eq!(errors.on("field")[0].message, "must be accepted explicitly");
}
#[test]
fn allow_blank_skips_blank_values_in_validation_set() {
let mut set = ValidationSet::new();
set.add("terms", AcceptanceValidator::new().allow_blank());
let attrs = HashMap::from([("terms".to_string(), json!(" "))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn custom_boolean_accept_values_are_supported() {
let errors = validate_acceptance(
AcceptanceValidator::new().accept(vec![json!(false)]),
Some(json!(false)),
);
assert!(errors.is_empty());
}
#[test]
fn validation_set_adds_error_for_rejected_value() {
let mut set = ValidationSet::new();
set.add("terms", AcceptanceValidator::new());
let attrs = HashMap::from([("terms".to_string(), json!("no"))]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert_eq!(errors.on("terms")[0].error_type, ErrorType::Accepted);
}
#[test]
fn full_message_humanizes_attribute_name() {
let mut errors = Errors::new();
AcceptanceValidator::new().validate("terms_of_service", Some(&json!(false)), &mut errors);
assert_eq!(
errors.full_messages(),
vec!["Terms of service must be accepted".to_string()],
);
}
#[test]
fn explicit_null_values_are_rejected_by_direct_validation() {
let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!(null)));
assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
}
#[test]
fn empty_accept_list_rejects_present_values() {
let errors = validate_acceptance(
AcceptanceValidator::new().accept(Vec::new()),
Some(json!(true)),
);
assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
}
}