Skip to main content

rustrails_model/validations/
mod.rs

1use std::fmt;
2use std::sync::Arc;
3
4use serde_json::Value;
5
6use crate::errors::Errors;
7
8/// Shared predicate type used by conditional validator options.
9pub type ValidationPredicate = Arc<dyn Fn(&Value) -> bool + Send + Sync>;
10type ValidateFn = dyn Fn(&str, Option<&Value>, &mut Errors) + Send + Sync;
11pub type ModelValidationFn = dyn Fn(&dyn Fn(&str) -> Option<Value>, &mut Errors) + Send + Sync;
12
13macro_rules! impl_common_validator_methods {
14    () => {
15        /// Skips validation when the attribute value is missing or `null`.
16        #[must_use]
17        pub fn allow_nil(mut self) -> Self {
18            self.options.allow_nil = true;
19            self
20        }
21
22        /// Skips validation when the attribute value is blank.
23        #[must_use]
24        pub fn allow_blank(mut self) -> Self {
25            self.options.allow_blank = true;
26            self
27        }
28
29        /// Restricts the validator to the provided contexts.
30        #[must_use]
31        pub fn on(mut self, contexts: Vec<crate::validations::ValidationContext>) -> Self {
32            self.options.on = Some(contexts);
33            self
34        }
35
36        /// Raises immediately instead of collecting produced errors.
37        #[must_use]
38        pub fn strict(mut self) -> Self {
39            self.options.strict = true;
40            self
41        }
42
43        /// Runs the validator only when the predicate returns `true`.
44        #[must_use]
45        pub fn if_cond<F>(mut self, cond: F) -> Self
46        where
47            F: Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
48        {
49            self.options.if_cond = Some(std::sync::Arc::new(cond));
50            self
51        }
52
53        /// Skips the validator when the predicate returns `true`.
54        #[must_use]
55        pub fn unless_cond<F>(mut self, cond: F) -> Self
56        where
57            F: Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
58        {
59            self.options.unless_cond = Some(std::sync::Arc::new(cond));
60            self
61        }
62    };
63}
64
65pub(crate) use impl_common_validator_methods;
66
67pub mod acceptance;
68pub mod confirmation;
69pub mod custom;
70pub mod exclusion;
71pub mod format;
72pub mod inclusion;
73pub mod length;
74pub mod numericality;
75pub mod presence;
76pub mod uniqueness;
77
78pub use acceptance::AcceptanceValidator;
79pub use confirmation::ConfirmationValidator;
80pub use custom::CustomValidator;
81pub use exclusion::ExclusionValidator;
82pub use format::FormatValidator;
83pub use inclusion::InclusionValidator;
84pub use length::LengthValidator;
85pub use numericality::NumericalityValidator;
86pub use presence::PresenceValidator;
87pub use uniqueness::UniquenessValidator;
88
89/// Contexts that can selectively enable validation rules.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum ValidationContext {
92    /// Validation while creating a new record.
93    Create,
94    /// Validation while updating an existing record.
95    Update,
96    /// Validation during a generic save operation.
97    Save,
98    /// An application-defined validation context.
99    Custom(String),
100}
101
102/// Shared runtime options applied by the validation runner.
103#[derive(Clone, Default)]
104pub struct ValidatorOptions {
105    /// Skips validation for missing or `null` values.
106    pub allow_nil: bool,
107    /// Skips validation for blank values such as empty strings.
108    pub allow_blank: bool,
109    /// Restricts the validator to explicit contexts.
110    pub on: Option<Vec<ValidationContext>>,
111    /// Raises immediately instead of collecting produced errors.
112    pub strict: bool,
113    /// Runs the validator only when the predicate returns `true`.
114    pub if_cond: Option<ValidationPredicate>,
115    /// Skips the validator when the predicate returns `true`.
116    pub unless_cond: Option<ValidationPredicate>,
117}
118
119impl fmt::Debug for ValidatorOptions {
120    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121        formatter
122            .debug_struct("ValidatorOptions")
123            .field("allow_nil", &self.allow_nil)
124            .field("allow_blank", &self.allow_blank)
125            .field("on", &self.on)
126            .field("strict", &self.strict)
127            .field("if_cond", &self.if_cond.as_ref().map(|_| "<predicate>"))
128            .field(
129                "unless_cond",
130                &self.unless_cond.as_ref().map(|_| "<predicate>"),
131            )
132            .finish()
133    }
134}
135
136/// A validation rule that can check a value and report errors.
137pub trait Validator: Send + Sync {
138    /// Validates a single attribute value, adding any produced errors.
139    fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors);
140
141    /// Validates a single attribute with access to sibling attributes when needed.
142    fn validate_with_attrs(
143        &self,
144        attribute: &str,
145        value: Option<&Value>,
146        _attrs: &dyn Fn(&str) -> Option<Value>,
147        errors: &mut Errors,
148    ) {
149        self.validate(attribute, value, errors);
150    }
151
152    /// Returns the validator's display name.
153    fn name(&self) -> &str;
154
155    /// Returns the common runtime options for this validator.
156    fn options(&self) -> &ValidatorOptions;
157}
158
159/// A single attribute-to-validator binding.
160pub struct ValidationRule {
161    /// Attribute validated by the rule.
162    pub attribute: String,
163    /// Validator instance that performs the check.
164    pub validator: Box<dyn Validator>,
165}
166
167/// Ordered collection of validation rules for a model type.
168#[derive(Default)]
169pub struct ValidationSet {
170    rules: Vec<ValidationRule>,
171}
172
173impl ValidationSet {
174    /// Creates an empty validation set.
175    #[must_use]
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Adds a validator for the given attribute.
181    pub fn add(&mut self, attribute: impl Into<String>, validator: impl Validator + 'static) {
182        self.rules.push(ValidationRule {
183            attribute: attribute.into(),
184            validator: Box::new(validator),
185        });
186    }
187
188    /// Returns validators registered for the given attribute in registration order.
189    #[must_use]
190    pub fn validators_on(&self, attribute: &str) -> Vec<&dyn Validator> {
191        self.rules
192            .iter()
193            .filter(|rule| rule.attribute == attribute)
194            .map(|rule| rule.validator.as_ref())
195            .collect()
196    }
197
198    /// Runs all validations and appends any produced errors.
199    pub fn validate(
200        &self,
201        attrs: &dyn Fn(&str) -> Option<Value>,
202        errors: &mut Errors,
203    ) -> Result<(), String> {
204        self.validate_internal(attrs, errors, None)
205    }
206
207    /// Runs validations for a specific context.
208    pub fn validate_with_context(
209        &self,
210        attrs: &dyn Fn(&str) -> Option<Value>,
211        errors: &mut Errors,
212        context: &ValidationContext,
213    ) -> Result<(), String> {
214        self.validate_internal(attrs, errors, Some(context))
215    }
216
217    fn validate_internal(
218        &self,
219        attrs: &dyn Fn(&str) -> Option<Value>,
220        errors: &mut Errors,
221        context: Option<&ValidationContext>,
222    ) -> Result<(), String> {
223        for rule in &self.rules {
224            let value = attrs(&rule.attribute);
225            if should_skip(rule.validator.options(), value.as_ref(), context) {
226                continue;
227            }
228
229            let mut produced = Errors::new();
230            rule.validator.validate_with_attrs(
231                &rule.attribute,
232                value.as_ref(),
233                attrs,
234                &mut produced,
235            );
236
237            if produced.is_empty() {
238                continue;
239            }
240
241            if rule.validator.options().strict {
242                return Err(strict_error_message(&produced));
243            }
244
245            merge_errors(errors, &produced);
246        }
247
248        Ok(())
249    }
250}
251
252pub trait ValidationDsl {
253    fn validates_each<I, S, F>(&mut self, attributes: I, validate_fn: F)
254    where
255        I: IntoIterator<Item = S>,
256        S: Into<String>,
257        F: Fn(&str, Option<&Value>, &mut Errors) + Send + Sync + 'static;
258
259    fn validate<F>(&mut self, validate_fn: F)
260    where
261        F: Fn(&dyn Fn(&str) -> Option<Value>, &mut Errors) + Send + Sync + 'static;
262}
263
264impl ValidationDsl for ValidationSet {
265    fn validates_each<I, S, F>(&mut self, attributes: I, validate_fn: F)
266    where
267        I: IntoIterator<Item = S>,
268        S: Into<String>,
269        F: Fn(&str, Option<&Value>, &mut Errors) + Send + Sync + 'static,
270    {
271        let validate_fn: Arc<ValidateFn> = Arc::new(validate_fn);
272
273        for attribute in attributes {
274            self.add(
275                attribute.into(),
276                EachValidator::new(Arc::clone(&validate_fn)),
277            );
278        }
279    }
280
281    fn validate<F>(&mut self, validate_fn: F)
282    where
283        F: Fn(&dyn Fn(&str) -> Option<Value>, &mut Errors) + Send + Sync + 'static,
284    {
285        self.add("base", ModelValidator::new(validate_fn));
286    }
287}
288
289#[derive(Clone)]
290struct EachValidator {
291    validate_fn: Arc<ValidateFn>,
292    options: ValidatorOptions,
293}
294
295impl EachValidator {
296    fn new(validate_fn: Arc<ValidateFn>) -> Self {
297        Self {
298            validate_fn,
299            options: ValidatorOptions::default(),
300        }
301    }
302}
303
304impl Validator for EachValidator {
305    fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
306        (self.validate_fn)(attribute, value, errors);
307    }
308
309    fn name(&self) -> &str {
310        "each"
311    }
312
313    fn options(&self) -> &ValidatorOptions {
314        &self.options
315    }
316}
317
318struct ModelValidator {
319    validate_fn: Box<ModelValidationFn>,
320    options: ValidatorOptions,
321}
322
323impl ModelValidator {
324    fn new<F>(validate_fn: F) -> Self
325    where
326        F: Fn(&dyn Fn(&str) -> Option<Value>, &mut Errors) + Send + Sync + 'static,
327    {
328        Self {
329            validate_fn: Box::new(validate_fn),
330            options: ValidatorOptions::default(),
331        }
332    }
333}
334
335impl Validator for ModelValidator {
336    fn validate(&self, _attribute: &str, _value: Option<&Value>, errors: &mut Errors) {
337        (self.validate_fn)(&|_| None, errors);
338    }
339
340    fn validate_with_attrs(
341        &self,
342        _attribute: &str,
343        _value: Option<&Value>,
344        attrs: &dyn Fn(&str) -> Option<Value>,
345        errors: &mut Errors,
346    ) {
347        (self.validate_fn)(attrs, errors);
348    }
349
350    fn name(&self) -> &str {
351        "validate"
352    }
353
354    fn options(&self) -> &ValidatorOptions {
355        &self.options
356    }
357}
358
359fn merge_errors(target: &mut Errors, produced: &Errors) {
360    for error in produced.details() {
361        target.add_with_details(
362            &error.attribute,
363            error.error_type.clone(),
364            error.message.clone(),
365            error.details.clone(),
366        );
367    }
368}
369
370fn strict_error_message(errors: &Errors) -> String {
371    errors
372        .full_messages()
373        .into_iter()
374        .next()
375        .unwrap_or_else(|| String::from("validation failed"))
376}
377
378pub(crate) fn should_skip(
379    options: &ValidatorOptions,
380    value: Option<&Value>,
381    context: Option<&ValidationContext>,
382) -> bool {
383    if !context_matches(options.on.as_deref(), context) {
384        return true;
385    }
386
387    if options.allow_nil && value_is_nil(value) {
388        return true;
389    }
390
391    if options.allow_blank && value_is_blank(value) {
392        return true;
393    }
394
395    let null = Value::Null;
396    let candidate = value.unwrap_or(&null);
397
398    if let Some(predicate) = &options.if_cond
399        && !predicate(candidate)
400    {
401        return true;
402    }
403
404    if let Some(predicate) = &options.unless_cond
405        && predicate(candidate)
406    {
407        return true;
408    }
409
410    false
411}
412
413pub(crate) fn context_matches(
414    allowed: Option<&[ValidationContext]>,
415    current: Option<&ValidationContext>,
416) -> bool {
417    let Some(allowed) = allowed else {
418        return true;
419    };
420    let Some(current) = current else {
421        return true;
422    };
423
424    allowed.iter().any(|candidate| match (candidate, current) {
425        (ValidationContext::Save, _) => true,
426        (ValidationContext::Create, ValidationContext::Create)
427        | (ValidationContext::Update, ValidationContext::Update)
428        | (ValidationContext::Custom(_), ValidationContext::Custom(_)) => candidate == current,
429        _ => false,
430    })
431}
432
433pub(crate) fn value_is_nil(value: Option<&Value>) -> bool {
434    matches!(value, None | Some(Value::Null))
435}
436
437pub(crate) fn value_is_blank(value: Option<&Value>) -> bool {
438    match value {
439        None | Some(Value::Null) => true,
440        Some(Value::String(text)) => text.trim().is_empty(),
441        Some(Value::Array(values)) => values.is_empty(),
442        Some(Value::Object(values)) => values.is_empty(),
443        Some(Value::Bool(flag)) => !flag,
444        Some(Value::Number(_)) => false,
445    }
446}
447
448/// Creates a presence validator.
449#[must_use]
450pub fn presence() -> PresenceValidator {
451    PresenceValidator::new()
452}
453
454/// Creates a length validator.
455#[must_use]
456pub fn length() -> LengthValidator {
457    LengthValidator::new()
458}
459
460/// Creates a numericality validator.
461#[must_use]
462pub fn numericality() -> NumericalityValidator {
463    NumericalityValidator::new()
464}
465
466/// Creates a format validator that requires the given regex pattern to match.
467#[must_use]
468pub fn format_with(pattern: &str) -> FormatValidator {
469    FormatValidator::with_pattern(pattern)
470}
471
472/// Creates an inclusion validator over the provided values.
473#[must_use]
474pub fn inclusion<T>(values: T) -> InclusionValidator
475where
476    T: Into<Vec<Value>>,
477{
478    InclusionValidator::new(values)
479}
480
481/// Creates an exclusion validator over the provided values.
482#[must_use]
483pub fn exclusion<T>(values: T) -> ExclusionValidator
484where
485    T: Into<Vec<Value>>,
486{
487    ExclusionValidator::new(values)
488}
489
490/// Creates an acceptance validator with the default accepted values.
491#[must_use]
492pub fn acceptance() -> AcceptanceValidator {
493    AcceptanceValidator::new()
494}
495
496/// Creates a confirmation validator for the given confirmation attribute.
497#[must_use]
498pub fn confirmation(confirmation_field: &str) -> ConfirmationValidator {
499    ConfirmationValidator::new(confirmation_field)
500}
501
502/// Creates a uniqueness validator.
503#[must_use]
504pub fn uniqueness() -> UniquenessValidator {
505    UniquenessValidator::new()
506}
507
508/// Creates a custom validator from a caller-provided function.
509#[must_use]
510pub fn custom<F>(f: F) -> CustomValidator
511where
512    F: Fn(&str, Option<&Value>, &mut Errors) + Send + Sync + 'static,
513{
514    CustomValidator::new(f)
515}
516
517#[cfg(test)]
518mod tests {
519    use std::collections::HashMap;
520    use std::sync::{
521        Arc,
522        atomic::{AtomicBool, Ordering},
523    };
524
525    use serde_json::{Value, json};
526
527    use super::{
528        ValidationContext, ValidationDsl, ValidationSet, Validator, ValidatorOptions,
529        context_matches, custom, length, presence, should_skip, value_is_blank, value_is_nil,
530    };
531    use crate::errors::{ErrorType, Errors};
532
533    #[derive(Default)]
534    struct MarkerValidator {
535        options: ValidatorOptions,
536    }
537
538    impl MarkerValidator {
539        fn new() -> Self {
540            Self::default()
541        }
542
543        fn allow_nil(mut self) -> Self {
544            self.options.allow_nil = true;
545            self
546        }
547
548        fn allow_blank(mut self) -> Self {
549            self.options.allow_blank = true;
550            self
551        }
552
553        fn on(mut self, contexts: Vec<ValidationContext>) -> Self {
554            self.options.on = Some(contexts);
555            self
556        }
557
558        fn if_cond<F>(mut self, cond: F) -> Self
559        where
560            F: Fn(&Value) -> bool + Send + Sync + 'static,
561        {
562            self.options.if_cond = Some(Arc::new(cond));
563            self
564        }
565
566        fn unless_cond<F>(mut self, cond: F) -> Self
567        where
568            F: Fn(&Value) -> bool + Send + Sync + 'static,
569        {
570            self.options.unless_cond = Some(Arc::new(cond));
571            self
572        }
573    }
574
575    impl Validator for MarkerValidator {
576        fn validate(&self, attribute: &str, _value: Option<&Value>, errors: &mut Errors) {
577            errors.add(attribute, ErrorType::Custom("marker".to_string()), "ran");
578        }
579
580        fn name(&self) -> &str {
581            "marker"
582        }
583
584        fn options(&self) -> &ValidatorOptions {
585            &self.options
586        }
587    }
588
589    #[test]
590    fn blank_detection_matches_validation_needs() {
591        assert!(value_is_blank(None));
592        assert!(value_is_blank(Some(&Value::Null)));
593        assert!(value_is_blank(Some(&json!("   "))));
594        assert!(value_is_blank(Some(&json!([]))));
595        assert!(value_is_blank(Some(&json!({}))));
596        assert!(value_is_blank(Some(&json!(false))));
597        assert!(!value_is_blank(Some(&json!(0))));
598        assert!(!value_is_blank(Some(&json!("Alice"))));
599    }
600
601    #[test]
602    fn context_matching_treats_save_as_global() {
603        let allowed = vec![ValidationContext::Save];
604
605        assert!(context_matches(
606            Some(&allowed),
607            Some(&ValidationContext::Create)
608        ));
609        assert!(context_matches(
610            Some(&allowed),
611            Some(&ValidationContext::Update)
612        ));
613        assert!(context_matches(
614            Some(&allowed),
615            Some(&ValidationContext::Save)
616        ));
617    }
618
619    #[test]
620    fn nil_context_matches_every_restricted_validator() {
621        let allowed = vec![ValidationContext::Create, ValidationContext::Update];
622
623        assert!(context_matches(Some(&allowed), None));
624    }
625
626    #[test]
627    fn should_skip_honors_allow_nil_and_allow_blank() {
628        let nil_validator = MarkerValidator::new().allow_nil();
629        let blank_validator = MarkerValidator::new().allow_blank();
630
631        assert!(should_skip(
632            nil_validator.options(),
633            Some(&Value::Null),
634            Some(&ValidationContext::Save),
635        ));
636        assert!(should_skip(
637            blank_validator.options(),
638            Some(&json!("   ")),
639            Some(&ValidationContext::Save),
640        ));
641    }
642
643    #[test]
644    fn should_skip_honors_context_and_predicates() {
645        let validator = MarkerValidator::new()
646            .on(vec![ValidationContext::Create])
647            .if_cond(|value| value == &json!("run"))
648            .unless_cond(|value| value == &json!("stop"));
649
650        assert!(should_skip(
651            validator.options(),
652            Some(&json!("run")),
653            Some(&ValidationContext::Update),
654        ));
655        assert!(should_skip(
656            validator.options(),
657            Some(&json!("miss")),
658            Some(&ValidationContext::Create),
659        ));
660        assert!(should_skip(
661            validator.options(),
662            Some(&json!("stop")),
663            Some(&ValidationContext::Create),
664        ));
665        assert!(!should_skip(
666            validator.options(),
667            Some(&json!("run")),
668            Some(&ValidationContext::Create),
669        ));
670    }
671
672    #[test]
673    fn validation_set_runs_matching_validators() {
674        let mut set = ValidationSet::new();
675        set.add("name", MarkerValidator::new());
676
677        let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
678        let mut errors = Errors::new();
679
680        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
681
682        assert_eq!(errors.count(), 1);
683        assert_eq!(errors.on("name")[0].message, "ran");
684    }
685
686    #[test]
687    fn validation_set_filters_by_context() {
688        let mut set = ValidationSet::new();
689        set.add(
690            "name",
691            MarkerValidator::new().on(vec![ValidationContext::Create]),
692        );
693
694        let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
695        let mut errors = Errors::new();
696
697        let _ = set.validate_with_context(
698            &|name| attrs.get(name).cloned(),
699            &mut errors,
700            &ValidationContext::Update,
701        );
702        assert!(errors.is_empty());
703
704        let _ = set.validate_with_context(
705            &|name| attrs.get(name).cloned(),
706            &mut errors,
707            &ValidationContext::Create,
708        );
709        assert_eq!(errors.count(), 1);
710    }
711
712    #[test]
713    fn validation_set_skips_custom_validator_when_blank_is_allowed() {
714        let called = Arc::new(AtomicBool::new(false));
715        let called_clone = Arc::clone(&called);
716        let validator = custom(move |_attribute, _value, _errors| {
717            called_clone.store(true, Ordering::Relaxed);
718        })
719        .allow_blank();
720
721        let mut set = ValidationSet::new();
722        set.add("nickname", validator);
723
724        let attrs = HashMap::from([("nickname".to_string(), json!("   "))]);
725        let mut errors = Errors::new();
726        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
727
728        assert!(errors.is_empty());
729        assert!(!called.load(Ordering::Relaxed));
730    }
731
732    #[test]
733    fn custom_context_matches_same_name() {
734        let allowed = vec![ValidationContext::Custom("import".to_string())];
735
736        assert!(context_matches(
737            Some(&allowed),
738            Some(&ValidationContext::Custom("import".to_string())),
739        ));
740    }
741
742    #[test]
743    fn custom_context_does_not_match_different_name() {
744        let allowed = vec![ValidationContext::Custom("import".to_string())];
745
746        assert!(!context_matches(
747            Some(&allowed),
748            Some(&ValidationContext::Custom("export".to_string())),
749        ));
750    }
751
752    #[test]
753    fn save_context_matches_custom_context() {
754        let allowed = vec![ValidationContext::Save];
755
756        assert!(context_matches(
757            Some(&allowed),
758            Some(&ValidationContext::Custom("import".to_string())),
759        ));
760    }
761
762    #[test]
763    fn value_is_nil_treats_missing_as_nil() {
764        assert!(value_is_nil(None));
765    }
766
767    #[test]
768    fn value_is_nil_treats_null_as_nil() {
769        assert!(value_is_nil(Some(&Value::Null)));
770    }
771
772    #[test]
773    fn value_is_nil_rejects_present_value() {
774        assert!(!value_is_nil(Some(&json!(0))));
775    }
776
777    #[test]
778    fn should_skip_uses_null_for_if_predicates_when_value_is_missing() {
779        let validator = MarkerValidator::new().if_cond(Value::is_null);
780
781        assert!(!should_skip(
782            validator.options(),
783            None,
784            Some(&ValidationContext::Save),
785        ));
786    }
787
788    #[test]
789    fn should_skip_uses_null_for_unless_predicates_when_value_is_missing() {
790        let validator = MarkerValidator::new().unless_cond(Value::is_null);
791
792        assert!(should_skip(
793            validator.options(),
794            None,
795            Some(&ValidationContext::Save),
796        ));
797    }
798
799    #[test]
800    fn should_not_skip_present_values_without_options() {
801        let validator = MarkerValidator::new();
802
803        assert!(!should_skip(
804            validator.options(),
805            Some(&json!("Alice")),
806            Some(&ValidationContext::Save),
807        ));
808    }
809
810    #[test]
811    fn allow_blank_skips_empty_arrays() {
812        let validator = MarkerValidator::new().allow_blank();
813
814        assert!(should_skip(
815            validator.options(),
816            Some(&json!([])),
817            Some(&ValidationContext::Save),
818        ));
819    }
820
821    #[test]
822    fn allow_blank_skips_empty_objects() {
823        let validator = MarkerValidator::new().allow_blank();
824
825        assert!(should_skip(
826            validator.options(),
827            Some(&json!({})),
828            Some(&ValidationContext::Save),
829        ));
830    }
831
832    #[test]
833    fn validation_set_preserves_rule_order_for_same_attribute() {
834        let mut set = ValidationSet::new();
835        set.add(
836            "name",
837            custom(|attribute, _value, errors| {
838                errors.add(
839                    attribute,
840                    ErrorType::Custom("global-first".to_string()),
841                    "global-first",
842                );
843            }),
844        );
845        set.add(
846            "name",
847            custom(|attribute, _value, errors| {
848                errors.add(
849                    attribute,
850                    ErrorType::Custom("create-only".to_string()),
851                    "create-only",
852                );
853            })
854            .on(vec![ValidationContext::Create]),
855        );
856        set.add(
857            "name",
858            custom(|attribute, _value, errors| {
859                errors.add(
860                    attribute,
861                    ErrorType::Custom("update-only".to_string()),
862                    "update-only",
863                );
864            })
865            .on(vec![ValidationContext::Update]),
866        );
867        set.add(
868            "name",
869            custom(|attribute, _value, errors| {
870                errors.add(
871                    attribute,
872                    ErrorType::Custom("global-last".to_string()),
873                    "global-last",
874                );
875            }),
876        );
877
878        let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
879        let mut errors = Errors::new();
880
881        let _ = set.validate_with_context(
882            &|name| attrs.get(name).cloned(),
883            &mut errors,
884            &ValidationContext::Create,
885        );
886
887        assert_eq!(
888            errors.messages_for("name"),
889            vec![
890                "global-first".to_string(),
891                "create-only".to_string(),
892                "global-last".to_string(),
893            ]
894        );
895    }
896
897    #[test]
898    fn validation_set_skips_allow_nil_rule_but_runs_required_rule_for_missing_value() {
899        let mut set = ValidationSet::new();
900        set.add(
901            "nickname",
902            custom(|attribute, _value, errors| {
903                errors.add(
904                    attribute,
905                    ErrorType::Custom("optional".to_string()),
906                    "optional",
907                );
908            })
909            .allow_nil(),
910        );
911        set.add(
912            "nickname",
913            custom(|attribute, _value, errors| {
914                errors.add(
915                    attribute,
916                    ErrorType::Custom("required".to_string()),
917                    "required",
918                );
919            }),
920        );
921        let attrs: HashMap<String, Value> = HashMap::new();
922        let mut errors = Errors::new();
923
924        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
925
926        assert_eq!(
927            errors.messages_for("nickname"),
928            vec!["required".to_string()]
929        );
930    }
931
932    fn validate_errors(
933        set: &ValidationSet,
934        attrs: HashMap<String, Value>,
935        context: Option<ValidationContext>,
936    ) -> Errors {
937        let mut errors = Errors::new();
938
939        match context {
940            Some(context) => {
941                let _ = set.validate_with_context(
942                    &|name| attrs.get(name).cloned(),
943                    &mut errors,
944                    &context,
945                );
946            }
947            None => {
948                let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
949            }
950        }
951
952        errors
953    }
954
955    struct ProcMessageValidator {
956        include_attribute: bool,
957        options: ValidatorOptions,
958    }
959
960    impl ProcMessageValidator {
961        fn from_record() -> Self {
962            Self {
963                include_attribute: false,
964                options: ValidatorOptions::default(),
965            }
966        }
967
968        fn from_record_and_data() -> Self {
969            Self {
970                include_attribute: true,
971                options: ValidatorOptions::default(),
972            }
973        }
974
975        fn build_message(&self, attribute: &str, attrs: &dyn Fn(&str) -> Option<Value>) -> String {
976            let author_name = attrs("author_name")
977                .and_then(|value| value.as_str().map(str::to_owned))
978                .unwrap_or_default();
979
980            if self.include_attribute {
981                format!(
982                    "{} is missing. You have failed me for the last time, {}.",
983                    rustrails_support::inflector::humanize(attribute),
984                    author_name,
985                )
986            } else {
987                format!("You have failed me for the last time, {}.", author_name,)
988            }
989        }
990    }
991
992    impl Validator for ProcMessageValidator {
993        fn validate(&self, attribute: &str, _value: Option<&Value>, errors: &mut Errors) {
994            errors.add(
995                attribute,
996                ErrorType::Custom("proc_message".to_string()),
997                self.build_message(attribute, &|_| None),
998            );
999        }
1000
1001        fn validate_with_attrs(
1002            &self,
1003            attribute: &str,
1004            _value: Option<&Value>,
1005            attrs: &dyn Fn(&str) -> Option<Value>,
1006            errors: &mut Errors,
1007        ) {
1008            errors.add(
1009                attribute,
1010                ErrorType::Custom("proc_message".to_string()),
1011                self.build_message(attribute, attrs),
1012            );
1013        }
1014
1015        fn name(&self) -> &str {
1016            "proc_message"
1017        }
1018
1019        fn options(&self) -> &ValidatorOptions {
1020            &self.options
1021        }
1022    }
1023
1024    #[test]
1025    fn test_single_field_validation() {
1026        let mut set = ValidationSet::new();
1027        set.add("content", presence());
1028
1029        let invalid_errors = validate_errors(
1030            &set,
1031            HashMap::from([("title".to_string(), json!("There's no content!"))]),
1032            None,
1033        );
1034        assert!(invalid_errors.any());
1035        assert_eq!(
1036            invalid_errors.messages_for("content"),
1037            vec!["can't be blank".to_string()]
1038        );
1039
1040        let valid_errors = validate_errors(
1041            &set,
1042            HashMap::from([
1043                ("title".to_string(), json!("There's no content!")),
1044                ("content".to_string(), json!("Messa content!")),
1045            ]),
1046            None,
1047        );
1048        assert!(valid_errors.is_empty());
1049    }
1050
1051    #[test]
1052    fn test_single_attr_validation_and_error_msg() {
1053        let mut set = ValidationSet::new();
1054        set.add("content", presence());
1055
1056        let errors = validate_errors(
1057            &set,
1058            HashMap::from([("title".to_string(), json!("There's no content!"))]),
1059            None,
1060        );
1061
1062        assert_eq!(errors.count(), 1);
1063        assert_eq!(
1064            errors.messages_for("content"),
1065            vec!["can't be blank".to_string()]
1066        );
1067    }
1068
1069    #[test]
1070    fn test_double_attr_validation_and_error_msg() {
1071        let mut set = ValidationSet::new();
1072        set.add("title", presence());
1073        set.add("content", presence());
1074
1075        let errors = validate_errors(&set, HashMap::new(), None);
1076
1077        assert_eq!(errors.count(), 2);
1078        assert_eq!(
1079            errors.messages_for("title"),
1080            vec!["can't be blank".to_string()]
1081        );
1082        assert_eq!(
1083            errors.messages_for("content"),
1084            vec!["can't be blank".to_string()]
1085        );
1086    }
1087
1088    #[test]
1089    fn test_multiple_errors_per_attr_iteration_with_full_error_composition() {
1090        let mut set = ValidationSet::new();
1091        set.add("content", presence());
1092        set.add("title", presence());
1093
1094        let errors = validate_errors(
1095            &set,
1096            HashMap::from([
1097                ("title".to_string(), json!("")),
1098                ("content".to_string(), json!("")),
1099            ]),
1100            None,
1101        );
1102
1103        assert_eq!(
1104            errors.full_messages(),
1105            vec![
1106                "Content can't be blank".to_string(),
1107                "Title can't be blank".to_string(),
1108            ]
1109        );
1110        assert_eq!(errors.count(), 2);
1111    }
1112
1113    #[test]
1114    fn test_errors_on_nested_attributes_expands_name() {
1115        let mut errors = Errors::new();
1116        errors.add("replies.name", ErrorType::Blank, "can't be blank");
1117
1118        assert_eq!(
1119            errors.full_messages(),
1120            vec!["Replies name can't be blank".to_string()]
1121        );
1122    }
1123
1124    #[test]
1125    fn test_errors_on_base() {
1126        let mut errors = Errors::new();
1127        errors.add("title", ErrorType::Blank, "can't be blank");
1128        errors.add("base", ErrorType::Invalid, "Reply is not dignifying");
1129
1130        assert_eq!(
1131            errors.messages_for("base"),
1132            vec!["Reply is not dignifying".to_string()]
1133        );
1134        assert_eq!(
1135            errors.full_messages(),
1136            vec![
1137                "Title can't be blank".to_string(),
1138                "Reply is not dignifying".to_string(),
1139            ]
1140        );
1141        assert_eq!(errors.count(), 2);
1142    }
1143
1144    #[test]
1145    fn test_errors_on_base_with_symbol_message() {
1146        let mut errors = Errors::new();
1147        errors.add("title", ErrorType::Blank, "can't be blank");
1148        errors.add("base", ErrorType::Invalid, "is invalid");
1149
1150        assert_eq!(errors.messages_for("base"), vec!["is invalid".to_string()]);
1151        assert_eq!(
1152            errors.full_messages(),
1153            vec!["Title can't be blank".to_string(), "is invalid".to_string()]
1154        );
1155        assert_eq!(errors.count(), 2);
1156    }
1157
1158    #[test]
1159    fn test_errors_on_custom_attribute() {
1160        let mut errors = Errors::new();
1161        errors.add("foo_bar", ErrorType::Invalid, "is invalid");
1162
1163        assert_eq!(
1164            errors.full_messages(),
1165            vec!["Foo bar is invalid".to_string()]
1166        );
1167    }
1168
1169    #[test]
1170    fn test_errors_on_custom_attribute_with_symbol_message() {
1171        let mut errors = Errors::new();
1172        errors.add("foo_bar", ErrorType::Invalid, "is invalid");
1173
1174        assert_eq!(
1175            errors.full_messages(),
1176            vec!["Foo bar is invalid".to_string()]
1177        );
1178    }
1179
1180    #[test]
1181    fn test_errors_empty_after_errors_on_check() {
1182        let errors = Errors::new();
1183
1184        assert!(errors.messages_for("id").is_empty());
1185        assert!(errors.is_empty());
1186    }
1187
1188    #[test]
1189    fn test_validates_each() {
1190        let mut set = ValidationSet::new();
1191        set.validates_each(["first_name", "last_name"], |attribute, value, errors| {
1192            if value
1193                .and_then(Value::as_str)
1194                .is_some_and(|text| text.starts_with('z'))
1195            {
1196                errors.add(attribute, ErrorType::Invalid, "starts with z");
1197            }
1198        });
1199
1200        let errors = validate_errors(
1201            &set,
1202            HashMap::from([
1203                ("first_name".to_string(), json!("zed")),
1204                ("last_name".to_string(), json!("alpha")),
1205            ]),
1206            None,
1207        );
1208
1209        assert_eq!(
1210            errors.messages_for("first_name"),
1211            vec!["starts with z".to_string()]
1212        );
1213        assert!(errors.messages_for("last_name").is_empty());
1214    }
1215
1216    #[test]
1217    fn test_validate_block() {
1218        let mut set = ValidationSet::new();
1219        <ValidationSet as ValidationDsl>::validate(&mut set, |attrs, errors| {
1220            if attrs("admin")
1221                .and_then(|value| value.as_bool())
1222                .unwrap_or(false)
1223            {
1224                errors.add("base", ErrorType::Invalid, "admins are not allowed");
1225            }
1226        });
1227
1228        let errors = validate_errors(
1229            &set,
1230            HashMap::from([("admin".to_string(), json!(true))]),
1231            None,
1232        );
1233
1234        assert_eq!(
1235            errors.messages_for("base"),
1236            vec!["admins are not allowed".to_string()]
1237        );
1238    }
1239
1240    #[test]
1241    fn test_validate_block_with_params() {
1242        let mut set = ValidationSet::new();
1243        <ValidationSet as ValidationDsl>::validate(&mut set, |attrs, errors| {
1244            let title = attrs("title").and_then(|value| value.as_str().map(str::to_owned));
1245            let author = attrs("author_name").and_then(|value| value.as_str().map(str::to_owned));
1246
1247            if title.as_deref() == Some("Forbidden") && author.as_deref() == Some("Robot") {
1248                errors.add("title", ErrorType::Invalid, "cannot be assigned to Robot");
1249            }
1250        });
1251
1252        let errors = validate_errors(
1253            &set,
1254            HashMap::from([
1255                ("title".to_string(), json!("Forbidden")),
1256                ("author_name".to_string(), json!("Robot")),
1257            ]),
1258            None,
1259        );
1260
1261        assert_eq!(
1262            errors.messages_for("title"),
1263            vec!["cannot be assigned to Robot".to_string()]
1264        );
1265    }
1266
1267    #[test]
1268    fn test_callback_options_to_validate() {
1269        let sequence = Arc::new(std::sync::Mutex::new(Vec::new()));
1270        let mut set = ValidationSet::new();
1271
1272        let sequence_b = Arc::clone(&sequence);
1273        set.add(
1274            "title",
1275            custom(move |_attribute, _value, _errors| {
1276                sequence_b.lock().unwrap().push("b");
1277            }),
1278        );
1279
1280        let sequence_a = Arc::clone(&sequence);
1281        set.add(
1282            "title",
1283            custom(move |_attribute, _value, _errors| {
1284                sequence_a.lock().unwrap().push("a");
1285            })
1286            .if_cond(|_| true),
1287        );
1288
1289        let sequence_c = Arc::clone(&sequence);
1290        set.add(
1291            "title",
1292            custom(move |_attribute, _value, _errors| {
1293                sequence_c.lock().unwrap().push("c");
1294            })
1295            .unless_cond(|_| true),
1296        );
1297
1298        let errors = validate_errors(
1299            &set,
1300            HashMap::from([("title".to_string(), json!("whatever"))]),
1301            None,
1302        );
1303
1304        assert!(errors.is_empty());
1305        let recorded = sequence.lock().unwrap().clone();
1306        assert_eq!(recorded, vec!["b", "a"]);
1307    }
1308
1309    #[test]
1310    fn test_errors_to_json() {
1311        let mut set = ValidationSet::new();
1312        set.add("title", presence());
1313        set.add("content", presence());
1314
1315        let errors = validate_errors(&set, HashMap::new(), None);
1316
1317        assert_eq!(
1318            errors.as_json(),
1319            json!({
1320                "title": ["can't be blank"],
1321                "content": ["can't be blank"],
1322            })
1323        );
1324    }
1325
1326    #[test]
1327    fn test_validation_order() {
1328        let mut set = ValidationSet::new();
1329        set.add("title", presence());
1330        set.add("title", length().minimum(2));
1331        set.add("author_name", presence());
1332        set.add(
1333            "author_email_address",
1334            custom(|attribute, _value, errors| {
1335                errors.add(
1336                    attribute,
1337                    ErrorType::Custom("manual".to_string()),
1338                    "will never be valid",
1339                );
1340            }),
1341        );
1342        set.add("content", length().minimum(10));
1343
1344        let errors = validate_errors(
1345            &set,
1346            HashMap::from([("title".to_string(), json!(""))]),
1347            None,
1348        );
1349
1350        assert_eq!(
1351            errors.attributes(),
1352            vec!["title", "author_name", "author_email_address", "content"]
1353        );
1354        assert_eq!(
1355            errors.messages_for("title"),
1356            vec![
1357                "can't be blank".to_string(),
1358                "is too short (minimum is 2 characters)".to_string(),
1359            ]
1360        );
1361        assert_eq!(
1362            errors.messages_for("author_name"),
1363            vec!["can't be blank".to_string()]
1364        );
1365        assert_eq!(
1366            errors.messages_for("author_email_address"),
1367            vec!["will never be valid".to_string()]
1368        );
1369        assert_eq!(
1370            errors.messages_for("content"),
1371            vec!["is too short (minimum is 10 characters)".to_string()]
1372        );
1373    }
1374
1375    #[test]
1376    fn test_validation_with_if_and_on() {
1377        let called = Arc::new(AtomicBool::new(false));
1378        let called_on_update = Arc::clone(&called);
1379        let mut set = ValidationSet::new();
1380        set.add(
1381            "title",
1382            presence()
1383                .if_cond(move |_| {
1384                    called_on_update.store(true, Ordering::Relaxed);
1385                    true
1386                })
1387                .on(vec![ValidationContext::Update]),
1388        );
1389
1390        let no_context_errors = validate_errors(&set, HashMap::new(), None);
1391        assert!(no_context_errors.any());
1392
1393        let create_errors = validate_errors(&set, HashMap::new(), Some(ValidationContext::Create));
1394        assert!(create_errors.is_empty());
1395
1396        let update_errors = validate_errors(&set, HashMap::new(), Some(ValidationContext::Update));
1397        assert!(update_errors.any());
1398        assert!(called.load(Ordering::Relaxed));
1399    }
1400
1401    #[test]
1402    fn test_invalid_should_be_the_opposite_of_valid() {
1403        let mut set = ValidationSet::new();
1404        set.add("title", presence());
1405
1406        let invalid_errors = validate_errors(&set, HashMap::new(), None);
1407        assert!(invalid_errors.any());
1408
1409        let valid_errors = validate_errors(
1410            &set,
1411            HashMap::from([("title".to_string(), json!("Things are going to change"))]),
1412            None,
1413        );
1414        assert!(valid_errors.is_empty());
1415    }
1416
1417    #[test]
1418    fn test_validation_with_message_as_proc() {
1419        let mut set = ValidationSet::new();
1420        set.add(
1421            "title",
1422            custom(|attribute, _value, errors| {
1423                errors.add(
1424                    attribute,
1425                    ErrorType::Custom("proc_message".to_string()),
1426                    "NO BLANKS HERE",
1427                );
1428            }),
1429        );
1430
1431        let errors = validate_errors(&set, HashMap::new(), None);
1432
1433        assert_eq!(
1434            errors.messages_for("title"),
1435            vec!["NO BLANKS HERE".to_string()]
1436        );
1437    }
1438
1439    #[test]
1440    fn test_list_of_validators_for_model() {
1441        let mut set = ValidationSet::new();
1442        set.add("title", presence());
1443        set.add("title", length().minimum(5));
1444
1445        let validators = set.validators_on("title");
1446        let names = validators
1447            .iter()
1448            .map(|validator| validator.name())
1449            .collect::<Vec<_>>();
1450
1451        assert_eq!(names, vec!["presence", "length"]);
1452    }
1453
1454    #[test]
1455    fn test_list_of_validators_on_an_attribute() {
1456        let mut set = ValidationSet::new();
1457        set.add("title", presence());
1458
1459        let validators = set.validators_on("title");
1460
1461        assert_eq!(validators.len(), 1);
1462        assert_eq!(validators[0].name(), "presence");
1463    }
1464
1465    #[test]
1466    fn test_list_of_validators_on_multiple_attributes() {
1467        let mut set = ValidationSet::new();
1468        set.add("title", presence());
1469        set.add("author_name", length().minimum(3));
1470
1471        assert_eq!(set.validators_on("title").len(), 1);
1472        assert_eq!(set.validators_on("author_name").len(), 1);
1473        assert!(
1474            set.validators_on("title")
1475                .iter()
1476                .all(|validator| validator.name() == "presence")
1477        );
1478        assert!(
1479            set.validators_on("author_name")
1480                .iter()
1481                .all(|validator| validator.name() == "length")
1482        );
1483    }
1484
1485    #[test]
1486    fn test_list_of_validators_will_be_empty_when_empty() {
1487        let set = ValidationSet::new();
1488
1489        assert!(set.validators_on("missing").is_empty());
1490    }
1491
1492    #[test]
1493    fn test_validations_on_the_instance_level() {
1494        let mut set = ValidationSet::new();
1495        set.add("title", presence());
1496        set.add("author_name", presence());
1497        set.add("content", length().minimum(10));
1498
1499        let invalid_errors = validate_errors(&set, HashMap::new(), None);
1500        assert_eq!(invalid_errors.count(), 3);
1501
1502        let valid_errors = validate_errors(
1503            &set,
1504            HashMap::from([
1505                ("title".to_string(), json!("Some Title")),
1506                ("author_name".to_string(), json!("Some Author")),
1507                (
1508                    "content".to_string(),
1509                    json!("Some Content Whose Length is more than 10."),
1510                ),
1511            ]),
1512            None,
1513        );
1514        assert!(valid_errors.is_empty());
1515    }
1516
1517    #[test]
1518    fn test_validate() {
1519        let mut set = ValidationSet::new();
1520        set.add("title", presence());
1521        set.add("author_name", presence());
1522        set.add("content", length().minimum(10));
1523
1524        let mut errors = Errors::new();
1525        assert!(errors.is_empty());
1526
1527        let attrs: HashMap<String, Value> = HashMap::new();
1528        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
1529
1530        assert!(!errors.is_empty());
1531    }
1532
1533    #[test]
1534    fn test_strict_validation_in_validates() {
1535        let mut set = ValidationSet::new();
1536        set.add("title", presence().strict());
1537        let mut errors = Errors::new();
1538
1539        let result = set.validate(&|_| None, &mut errors);
1540
1541        assert_eq!(result, Err("Title can't be blank".to_string()));
1542        assert!(errors.is_empty());
1543    }
1544
1545    #[test]
1546    fn test_strict_validation_not_fails() {
1547        let mut set = ValidationSet::new();
1548        set.add("title", presence().strict());
1549        let mut errors = Errors::new();
1550
1551        let result = set.validate(
1552            &|name| (name == "title").then(|| json!("Present")),
1553            &mut errors,
1554        );
1555
1556        assert_eq!(result, Ok(()));
1557        assert!(errors.is_empty());
1558    }
1559
1560    #[test]
1561    fn test_strict_validation_particular_validator() {
1562        let mut set = ValidationSet::new();
1563        set.add("title", presence());
1564        set.add("title", length().minimum(5).strict());
1565        let mut errors = Errors::new();
1566
1567        let result = set.validate(&|name| (name == "title").then(|| json!("abc")), &mut errors);
1568
1569        assert_eq!(
1570            result,
1571            Err("Title is too short (minimum is 5 characters)".to_string())
1572        );
1573        assert!(errors.is_empty());
1574    }
1575
1576    #[test]
1577    fn test_strict_validation_in_custom_validator_helper() {
1578        let mut set = ValidationSet::new();
1579        set.add(
1580            "title",
1581            custom(|attribute, _value, errors| {
1582                errors.add(attribute, ErrorType::Invalid, "is forbidden");
1583            })
1584            .strict(),
1585        );
1586        let mut errors = Errors::new();
1587
1588        let result = set.validate(&|_| None, &mut errors);
1589
1590        assert_eq!(result, Err("Title is forbidden".to_string()));
1591        assert!(errors.is_empty());
1592    }
1593
1594    #[test]
1595    fn test_strict_validation_error_message() {
1596        let mut set = ValidationSet::new();
1597        set.add(
1598            "base",
1599            custom(|attribute, _value, errors| {
1600                errors.add(attribute, ErrorType::Invalid, "record is invalid");
1601            })
1602            .strict(),
1603        );
1604        let mut errors = Errors::new();
1605
1606        let result = set.validate(&|_| None, &mut errors);
1607
1608        assert_eq!(result, Err("record is invalid".to_string()));
1609        assert!(errors.is_empty());
1610    }
1611
1612    #[test]
1613    fn test_does_not_modify_options_argument() {
1614        let contexts = vec![ValidationContext::Create];
1615        let snapshot = contexts.clone();
1616        let validator = presence().on(contexts.clone());
1617
1618        assert_eq!(contexts, snapshot);
1619        assert_eq!(validator.options().on.as_deref(), Some(snapshot.as_slice()));
1620    }
1621
1622    #[test]
1623    fn test_dup_validity_is_independent() {
1624        let mut set = ValidationSet::new();
1625        set.add("title", presence());
1626
1627        let original_errors = validate_errors(
1628            &set,
1629            HashMap::from([("title".to_string(), json!("Literature"))]),
1630            None,
1631        );
1632        let duped_errors = validate_errors(&set, HashMap::new(), None);
1633
1634        assert!(original_errors.is_empty());
1635        assert_eq!(
1636            duped_errors.messages_for("title"),
1637            vec!["can't be blank".to_string()]
1638        );
1639    }
1640
1641    #[test]
1642    fn test_validation_with_message_as_proc_that_takes_a_record_as_a_parameter() {
1643        let mut set = ValidationSet::new();
1644        set.add("title", ProcMessageValidator::from_record());
1645
1646        let errors = validate_errors(
1647            &set,
1648            HashMap::from([("author_name".to_string(), json!("Admiral"))]),
1649            None,
1650        );
1651
1652        assert_eq!(
1653            errors.messages_for("title"),
1654            vec!["You have failed me for the last time, Admiral.".to_string()]
1655        );
1656    }
1657
1658    #[test]
1659    fn test_validation_with_message_as_proc_that_takes_record_and_data_as_a_parameters() {
1660        let mut set = ValidationSet::new();
1661        set.add("title", ProcMessageValidator::from_record_and_data());
1662
1663        let errors = validate_errors(
1664            &set,
1665            HashMap::from([("author_name".to_string(), json!("Admiral"))]),
1666            None,
1667        );
1668
1669        assert_eq!(
1670            errors.messages_for("title"),
1671            vec!["Title is missing. You have failed me for the last time, Admiral.".to_string()]
1672        );
1673    }
1674
1675    #[test]
1676    fn strict_validation_preserves_earlier_non_strict_errors_before_returning() {
1677        let mut set = ValidationSet::new();
1678        set.add("title", presence());
1679        set.add("title", length().minimum(5).strict());
1680        let mut errors = Errors::new();
1681
1682        let result = set.validate(&|name| (name == "title").then(|| json!("")), &mut errors);
1683
1684        assert_eq!(
1685            result,
1686            Err("Title is too short (minimum is 5 characters)".to_string())
1687        );
1688        assert_eq!(
1689            errors.messages_for("title"),
1690            vec!["can't be blank".to_string()]
1691        );
1692    }
1693
1694    #[test]
1695    #[ignore = "Rails-specific: strict validator error message format depends on full ActiveModel error pipeline"]
1696    fn test_invalid_validator() {}
1697
1698    #[test]
1699    #[ignore = "Rails-specific: validate options hash parsing depends on Ruby metaprogramming"]
1700    fn test_invalid_options_to_validate() {}
1701
1702    #[test]
1703    #[ignore = "Rails-specific: frozen model validation depends on Ruby freeze semantics"]
1704    fn test_frozen_models_can_be_validated() {}
1705
1706    #[test]
1707    #[ignore = "Rails-specific: :except_on context filtering is not implemented"]
1708    fn test_validate_with_except_on() {}
1709
1710    #[test]
1711    #[ignore = "Rails-specific: :except_on context filtering is not implemented"]
1712    fn test_validations_some_with_except() {}
1713
1714    #[test]
1715    #[ignore = "Rails-specific: custom attribute readers are not supported in ValidationSet"]
1716    fn test_validates_each_custom_reader() {}
1717
1718    #[test]
1719    #[ignore = "Rails-specific: array condition mutation testing depends on Ruby array semantics"]
1720    fn test_validates_with_array_condition_does_not_mutate_the_array() {}
1721
1722    #[test]
1723    #[ignore = "Rails-specific: validator instance introspection depends on Ruby reflection"]
1724    fn test_accessing_instance_of_validator_on_an_attribute() {}
1725
1726    #[test]
1727    #[ignore = "Rails-specific: validators_for_model multi-attribute introspection not implemented"]
1728    fn test_list_of_validators_for_model_exposes_all_attributes_at_once() {}
1729
1730    #[test]
1731    #[ignore = "Rails-specific: validators_on varargs interface not implemented"]
1732    fn test_list_of_validators_on_multiple_attributes_accepts_varargs() {}
1733}