rustrails-model 0.1.2

Model layer (ActiveModel equivalent)
Documentation
use serde_json::Value;

use super::{Validator, ValidatorOptions, value_is_blank};
use crate::errors::{ErrorType, Errors};

/// Validates that an attribute is present and not blank.
#[derive(Debug, Clone, Default)]
pub struct PresenceValidator {
    /// Custom message used when the value is blank.
    pub message: Option<String>,
    pub(crate) options: ValidatorOptions,
}

impl PresenceValidator {
    /// Creates a new presence validator.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    crate::validations::impl_common_validator_methods!();

    /// Overrides the default blank message.
    #[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(|| "can't be blank".to_string())
    }
}

impl Validator for PresenceValidator {
    fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
        if value_is_blank(value) {
            errors.add(attribute, ErrorType::Blank, self.error_message());
        }
    }

    fn name(&self) -> &str {
        "presence"
    }

    fn options(&self) -> &ValidatorOptions {
        &self.options
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::PresenceValidator;
    use crate::{
        errors::{ErrorType, Errors},
        validations::{ValidationSet, Validator},
    };

    fn validate_value(validator: PresenceValidator, value: Option<serde_json::Value>) -> Errors {
        let mut errors = Errors::new();
        validator.validate("field", value.as_ref(), &mut errors);
        errors
    }

    #[test]
    fn nil_fails() {
        let validator = PresenceValidator::new();
        let mut errors = Errors::new();

        validator.validate("name", None, &mut errors);

        assert_eq!(errors.on("name")[0].error_type, ErrorType::Blank);
    }

    #[test]
    fn null_fails() {
        let validator = PresenceValidator::new();
        let mut errors = Errors::new();

        validator.validate("name", Some(&json!(null)), &mut errors);

        assert_eq!(errors.on("name")[0].error_type, ErrorType::Blank);
    }

    #[test]
    fn empty_string_fails() {
        let validator = PresenceValidator::new();
        let mut errors = Errors::new();

        validator.validate("name", Some(&json!("")), &mut errors);

        assert_eq!(errors.on("name")[0].message, "can't be blank");
    }

    #[test]
    fn whitespace_string_fails() {
        let validator = PresenceValidator::new();
        let mut errors = Errors::new();

        validator.validate("name", Some(&json!("   ")), &mut errors);

        assert_eq!(errors.on("name")[0].error_type, ErrorType::Blank);
    }

    #[test]
    fn false_is_treated_as_blank() {
        let validator = PresenceValidator::new();
        let mut errors = Errors::new();

        validator.validate("published", Some(&json!(false)), &mut errors);

        assert_eq!(errors.on("published")[0].error_type, ErrorType::Blank);
    }

    #[test]
    fn present_string_passes() {
        let validator = PresenceValidator::new();
        let mut errors = Errors::new();

        validator.validate("name", Some(&json!("Alice")), &mut errors);

        assert!(errors.is_empty());
    }

    #[test]
    fn number_passes() {
        let validator = PresenceValidator::new();
        let mut errors = Errors::new();

        validator.validate("age", Some(&json!(42)), &mut errors);

        assert!(errors.is_empty());
    }

    #[test]
    fn custom_message_is_used() {
        let validator = PresenceValidator::new().message("must exist");
        let mut errors = Errors::new();

        validator.validate("name", None, &mut errors);

        assert_eq!(errors.on("name")[0].message, "must exist");
    }

    #[test]
    fn empty_array_fails() {
        let errors = validate_value(PresenceValidator::new(), Some(json!([])));

        assert_eq!(errors.on("field")[0].error_type, ErrorType::Blank);
    }

    #[test]
    fn empty_object_fails() {
        let errors = validate_value(PresenceValidator::new(), Some(json!({})));

        assert_eq!(errors.on("field")[0].error_type, ErrorType::Blank);
    }

    #[test]
    fn non_empty_array_passes() {
        let errors = validate_value(PresenceValidator::new(), Some(json!(["tag"])));

        assert!(errors.is_empty());
    }

    #[test]
    fn non_empty_object_passes() {
        let errors = validate_value(PresenceValidator::new(), Some(json!({ "name": "Alice" })));

        assert!(errors.is_empty());
    }

    #[test]
    fn true_is_present() {
        let errors = validate_value(PresenceValidator::new(), Some(json!(true)));

        assert!(errors.is_empty());
    }

    #[test]
    fn tab_and_newline_only_string_fails() {
        let errors = validate_value(PresenceValidator::new(), Some(json!("\t\n")));

        assert_eq!(errors.on("field")[0].error_type, ErrorType::Blank);
    }

    #[test]
    fn allow_nil_skips_none_in_validation_set() {
        let mut set = ValidationSet::new();
        set.add("nickname", PresenceValidator::new().allow_nil());
        let mut errors = Errors::new();

        let _ = set.validate(&|_| None, &mut errors);

        assert!(errors.is_empty());
    }

    #[test]
    fn allow_blank_skips_whitespace_in_validation_set() {
        let mut set = ValidationSet::new();
        set.add("nickname", PresenceValidator::new().allow_blank());
        let mut errors = Errors::new();

        let _ = set.validate(&|_| Some(json!("   ")), &mut errors);

        assert!(errors.is_empty());
    }

    #[test]
    fn allow_blank_skips_empty_collection_in_validation_set() {
        let mut set = ValidationSet::new();
        set.add("tags", PresenceValidator::new().allow_blank());
        let mut errors = Errors::new();

        let _ = set.validate(&|_| Some(json!([])), &mut errors);

        assert!(errors.is_empty());
    }

    #[test]
    fn allow_blank_skips_empty_object_in_validation_set() {
        let mut set = ValidationSet::new();
        set.add("profile", PresenceValidator::new().allow_blank());
        let mut errors = Errors::new();

        let _ = set.validate(&|_| Some(json!({})), &mut errors);

        assert!(errors.is_empty());
    }

    #[test]
    fn full_message_is_humanized() {
        let errors = validate_value(PresenceValidator::new(), None);

        assert_eq!(
            errors.full_messages(),
            vec!["Field can't be blank".to_string()]
        );
    }
}