use rustrails_support::inflector::humanize;
use serde_json::Value;
use super::{Validator, ValidatorOptions};
use crate::errors::{ErrorType, Errors};
#[derive(Debug, Clone)]
pub struct ConfirmationValidator {
confirmation_attribute: String,
case_sensitive: bool,
message: Option<String>,
pub(crate) options: ValidatorOptions,
}
impl ConfirmationValidator {
#[must_use]
pub fn new(confirmation_attribute: &str) -> Self {
Self {
confirmation_attribute: confirmation_attribute.to_owned(),
case_sensitive: true,
message: None,
options: ValidatorOptions::default(),
}
}
crate::validations::impl_common_validator_methods!();
#[must_use]
pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
self.case_sensitive = case_sensitive;
self
}
#[must_use]
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
fn error_message(&self, attribute: &str) -> String {
self.message
.clone()
.unwrap_or_else(|| format!("doesn't match {}", humanize(attribute)))
}
fn matches(&self, value: Option<&Value>, confirmation: &Value) -> bool {
match (value, confirmation) {
(Some(Value::String(left)), Value::String(right)) if !self.case_sensitive => {
left.to_lowercase() == right.to_lowercase()
}
(Some(left), right) => left == right,
(None, Value::Null) => true,
_ => false,
}
}
}
impl Validator for ConfirmationValidator {
fn validate(&self, _attribute: &str, _value: Option<&Value>, _errors: &mut Errors) {}
fn validate_with_attrs(
&self,
attribute: &str,
value: Option<&Value>,
attrs: &dyn Fn(&str) -> Option<Value>,
errors: &mut Errors,
) {
let Some(confirmation) = attrs(&self.confirmation_attribute) else {
return;
};
if confirmation.is_null() {
return;
}
if !self.matches(value, &confirmation) {
errors.add(
&self.confirmation_attribute,
ErrorType::Confirmation,
self.error_message(attribute),
);
}
}
fn name(&self) -> &str {
"confirmation"
}
fn options(&self) -> &ValidatorOptions {
&self.options
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use serde_json::json;
use super::ConfirmationValidator;
use crate::{
errors::{ErrorType, Errors},
validations::{ValidationSet, Validator},
};
fn validate_confirmation(
validator: &ConfirmationValidator,
attribute: &str,
value: Option<&serde_json::Value>,
attrs: &HashMap<String, serde_json::Value>,
) -> Errors {
let mut errors = Errors::new();
validator.validate_with_attrs(
attribute,
value,
&|name| attrs.get(name).cloned(),
&mut errors,
);
errors
}
#[test]
fn missing_confirmation_attribute_is_ignored() {
let validator = ConfirmationValidator::new("password_confirmation");
let attrs = HashMap::from([(String::from("password"), json!("secret"))]);
let mut errors = Errors::new();
validator.validate_with_attrs(
"password",
attrs.get("password"),
&|name| attrs.get(name).cloned(),
&mut errors,
);
assert!(errors.is_empty());
}
#[test]
fn mismatch_adds_error_on_confirmation_attribute() {
let validator = ConfirmationValidator::new("password_confirmation");
let attrs = HashMap::from([
(String::from("password"), json!("secret")),
(String::from("password_confirmation"), json!("other")),
]);
let mut errors = Errors::new();
validator.validate_with_attrs(
"password",
attrs.get("password"),
&|name| attrs.get(name).cloned(),
&mut errors,
);
assert_eq!(
errors.on("password_confirmation")[0].error_type,
ErrorType::Confirmation
);
}
#[test]
fn matching_values_pass() {
let validator = ConfirmationValidator::new("email_confirmation");
let attrs = HashMap::from([
(String::from("email"), json!("alice@example.com")),
(
String::from("email_confirmation"),
json!("alice@example.com"),
),
]);
let mut errors = Errors::new();
validator.validate_with_attrs(
"email",
attrs.get("email"),
&|name| attrs.get(name).cloned(),
&mut errors,
);
assert!(errors.is_empty());
}
#[test]
fn case_insensitive_strings_can_match() {
let validator = ConfirmationValidator::new("email_confirmation").case_sensitive(false);
let attrs = HashMap::from([
(String::from("email"), json!("Alice@example.com")),
(
String::from("email_confirmation"),
json!("alice@example.com"),
),
]);
let mut errors = Errors::new();
validator.validate_with_attrs(
"email",
attrs.get("email"),
&|name| attrs.get(name).cloned(),
&mut errors,
);
assert!(errors.is_empty());
}
#[test]
fn validation_set_uses_sibling_attribute_lookup() {
let mut set = ValidationSet::new();
set.add(
"password",
ConfirmationValidator::new("password_confirmation"),
);
let attrs = HashMap::from([
(String::from("password"), json!("secret")),
(String::from("password_confirmation"), json!("other")),
]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert_eq!(
errors.on("password_confirmation")[0].message,
"doesn't match Password"
);
}
#[test]
fn null_confirmation_is_ignored() {
let validator = ConfirmationValidator::new("password_confirmation");
let attrs = HashMap::from([
(String::from("password"), json!("secret")),
(String::from("password_confirmation"), json!(null)),
]);
let errors = validate_confirmation(&validator, "password", attrs.get("password"), &attrs);
assert!(errors.is_empty());
}
#[test]
fn missing_primary_value_with_non_null_confirmation_fails() {
let validator = ConfirmationValidator::new("password_confirmation");
let attrs = HashMap::from([(String::from("password_confirmation"), json!("secret"))]);
let errors = validate_confirmation(&validator, "password", None, &attrs);
assert_eq!(
errors.on("password_confirmation")[0].error_type,
ErrorType::Confirmation
);
}
#[test]
fn custom_message_override_is_used() {
let validator = ConfirmationValidator::new("password_confirmation").message("must match");
let attrs = HashMap::from([
(String::from("password"), json!("secret")),
(String::from("password_confirmation"), json!("other")),
]);
let errors = validate_confirmation(&validator, "password", attrs.get("password"), &attrs);
assert_eq!(errors.on("password_confirmation")[0].message, "must match");
}
#[test]
fn explicit_case_sensitive_matching_rejects_casing_difference() {
let validator = ConfirmationValidator::new("email_confirmation").case_sensitive(true);
let attrs = HashMap::from([
(String::from("email"), json!("Alice@example.com")),
(
String::from("email_confirmation"),
json!("alice@example.com"),
),
]);
let errors = validate_confirmation(&validator, "email", attrs.get("email"), &attrs);
assert_eq!(
errors.on("email_confirmation")[0].error_type,
ErrorType::Confirmation
);
}
#[test]
fn case_insensitive_matching_still_rejects_different_strings() {
let validator = ConfirmationValidator::new("email_confirmation").case_sensitive(false);
let attrs = HashMap::from([
(String::from("email"), json!("alice@example.com")),
(String::from("email_confirmation"), json!("bob@example.com")),
]);
let errors = validate_confirmation(&validator, "email", attrs.get("email"), &attrs);
assert_eq!(
errors.on("email_confirmation")[0].error_type,
ErrorType::Confirmation
);
}
#[test]
fn matching_boolean_values_pass() {
let validator = ConfirmationValidator::new("published_confirmation");
let attrs = HashMap::from([
(String::from("published"), json!(true)),
(String::from("published_confirmation"), json!(true)),
]);
let errors = validate_confirmation(&validator, "published", attrs.get("published"), &attrs);
assert!(errors.is_empty());
}
#[test]
fn mismatched_boolean_values_fail() {
let validator = ConfirmationValidator::new("published_confirmation");
let attrs = HashMap::from([
(String::from("published"), json!(true)),
(String::from("published_confirmation"), json!(false)),
]);
let errors = validate_confirmation(&validator, "published", attrs.get("published"), &attrs);
assert_eq!(
errors.on("published_confirmation")[0].error_type,
ErrorType::Confirmation
);
}
#[test]
fn direct_validate_is_a_no_op() {
let validator = ConfirmationValidator::new("password_confirmation");
let mut errors = Errors::new();
validator.validate("password", Some(&json!("secret")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn validation_set_passes_when_confirmation_matches() {
let mut set = ValidationSet::new();
set.add(
"password",
ConfirmationValidator::new("password_confirmation"),
);
let attrs = HashMap::from([
(String::from("password"), json!("secret")),
(String::from("password_confirmation"), json!("secret")),
]);
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn full_message_humanizes_confirmation_attribute() {
let validator = ConfirmationValidator::new("password_confirmation");
let attrs = HashMap::from([
(String::from("password"), json!("secret")),
(String::from("password_confirmation"), json!("other")),
]);
let errors = validate_confirmation(&validator, "password", attrs.get("password"), &attrs);
assert_eq!(
errors.full_messages(),
vec!["Password confirmation doesn't match Password".to_string()],
);
}
}