1use std::fmt;
2use std::sync::Arc;
3
4use serde_json::Value;
5
6use crate::errors::Errors;
7
8pub 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 #[must_use]
17 pub fn allow_nil(mut self) -> Self {
18 self.options.allow_nil = true;
19 self
20 }
21
22 #[must_use]
24 pub fn allow_blank(mut self) -> Self {
25 self.options.allow_blank = true;
26 self
27 }
28
29 #[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 #[must_use]
38 pub fn strict(mut self) -> Self {
39 self.options.strict = true;
40 self
41 }
42
43 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum ValidationContext {
92 Create,
94 Update,
96 Save,
98 Custom(String),
100}
101
102#[derive(Clone, Default)]
104pub struct ValidatorOptions {
105 pub allow_nil: bool,
107 pub allow_blank: bool,
109 pub on: Option<Vec<ValidationContext>>,
111 pub strict: bool,
113 pub if_cond: Option<ValidationPredicate>,
115 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
136pub trait Validator: Send + Sync {
138 fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors);
140
141 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 fn name(&self) -> &str;
154
155 fn options(&self) -> &ValidatorOptions;
157}
158
159pub struct ValidationRule {
161 pub attribute: String,
163 pub validator: Box<dyn Validator>,
165}
166
167#[derive(Default)]
169pub struct ValidationSet {
170 rules: Vec<ValidationRule>,
171}
172
173impl ValidationSet {
174 #[must_use]
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 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 #[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 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 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#[must_use]
450pub fn presence() -> PresenceValidator {
451 PresenceValidator::new()
452}
453
454#[must_use]
456pub fn length() -> LengthValidator {
457 LengthValidator::new()
458}
459
460#[must_use]
462pub fn numericality() -> NumericalityValidator {
463 NumericalityValidator::new()
464}
465
466#[must_use]
468pub fn format_with(pattern: &str) -> FormatValidator {
469 FormatValidator::with_pattern(pattern)
470}
471
472#[must_use]
474pub fn inclusion<T>(values: T) -> InclusionValidator
475where
476 T: Into<Vec<Value>>,
477{
478 InclusionValidator::new(values)
479}
480
481#[must_use]
483pub fn exclusion<T>(values: T) -> ExclusionValidator
484where
485 T: Into<Vec<Value>>,
486{
487 ExclusionValidator::new(values)
488}
489
490#[must_use]
492pub fn acceptance() -> AcceptanceValidator {
493 AcceptanceValidator::new()
494}
495
496#[must_use]
498pub fn confirmation(confirmation_field: &str) -> ConfirmationValidator {
499 ConfirmationValidator::new(confirmation_field)
500}
501
502#[must_use]
504pub fn uniqueness() -> UniquenessValidator {
505 UniquenessValidator::new()
506}
507
508#[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}