Skip to main content

ferro_rs/validation/
rules.rs

1//! Built-in validation rules.
2
3use crate::validation::translate_validation;
4use crate::validation::Rule;
5use regex::Regex;
6use serde_json::Value;
7use std::sync::LazyLock;
8
9// ============================================================================
10// Required Rules
11// ============================================================================
12
13/// Field must be present and not empty.
14pub struct Required;
15
16pub const fn required() -> Required {
17    Required
18}
19
20impl Rule for Required {
21    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
22        let is_empty = match value {
23            Value::Null => true,
24            Value::String(s) => s.trim().is_empty(),
25            Value::Array(a) => a.is_empty(),
26            Value::Object(o) => o.is_empty(),
27            _ => false,
28        };
29
30        if is_empty {
31            Err(
32                translate_validation("validation.required", &[("attribute", field)])
33                    .unwrap_or_else(|| format!("The {field} field is required.")),
34            )
35        } else {
36            Ok(())
37        }
38    }
39
40    fn name(&self) -> &'static str {
41        "required"
42    }
43}
44
45/// Field is required if another field equals a value.
46pub struct RequiredIf {
47    other: String,
48    value: Value,
49}
50
51pub fn required_if(other: impl Into<String>, value: impl Into<Value>) -> RequiredIf {
52    RequiredIf {
53        other: other.into(),
54        value: value.into(),
55    }
56}
57
58impl Rule for RequiredIf {
59    fn validate(&self, field: &str, value: &Value, data: &Value) -> Result<(), String> {
60        let other_value = data.get(&self.other);
61        if other_value == Some(&self.value) {
62            Required.validate(field, value, data)
63        } else {
64            Ok(())
65        }
66    }
67
68    fn name(&self) -> &'static str {
69        "required_if"
70    }
71}
72
73// ============================================================================
74// Type Rules
75// ============================================================================
76
77/// Field must be a string.
78pub struct IsString;
79
80pub const fn string() -> IsString {
81    IsString
82}
83
84impl Rule for IsString {
85    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
86        if value.is_null() || value.is_string() {
87            Ok(())
88        } else {
89            Err(
90                translate_validation("validation.string", &[("attribute", field)])
91                    .unwrap_or_else(|| format!("The {field} field must be a string.")),
92            )
93        }
94    }
95
96    fn name(&self) -> &'static str {
97        "string"
98    }
99}
100
101/// Field must be an integer.
102pub struct IsInteger;
103
104pub const fn integer() -> IsInteger {
105    IsInteger
106}
107
108impl Rule for IsInteger {
109    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
110        if value.is_null() || value.is_i64() || value.is_u64() {
111            Ok(())
112        } else if let Some(s) = value.as_str() {
113            if s.parse::<i64>().is_ok() {
114                Ok(())
115            } else {
116                Err(
117                    translate_validation("validation.integer", &[("attribute", field)])
118                        .unwrap_or_else(|| format!("The {field} field must be an integer.")),
119                )
120            }
121        } else {
122            Err(
123                translate_validation("validation.integer", &[("attribute", field)])
124                    .unwrap_or_else(|| format!("The {field} field must be an integer.")),
125            )
126        }
127    }
128
129    fn name(&self) -> &'static str {
130        "integer"
131    }
132}
133
134/// Field must be numeric.
135pub struct Numeric;
136
137pub const fn numeric() -> Numeric {
138    Numeric
139}
140
141impl Rule for Numeric {
142    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
143        if value.is_null() || value.is_number() {
144            Ok(())
145        } else if let Some(s) = value.as_str() {
146            if s.parse::<f64>().is_ok() {
147                Ok(())
148            } else {
149                Err(
150                    translate_validation("validation.numeric", &[("attribute", field)])
151                        .unwrap_or_else(|| format!("The {field} field must be a number.")),
152                )
153            }
154        } else {
155            Err(
156                translate_validation("validation.numeric", &[("attribute", field)])
157                    .unwrap_or_else(|| format!("The {field} field must be a number.")),
158            )
159        }
160    }
161
162    fn name(&self) -> &'static str {
163        "numeric"
164    }
165}
166
167/// Field must be a boolean.
168pub struct IsBoolean;
169
170pub const fn boolean() -> IsBoolean {
171    IsBoolean
172}
173
174impl Rule for IsBoolean {
175    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
176        if value.is_null() || value.is_boolean() {
177            Ok(())
178        } else if let Some(s) = value.as_str() {
179            match s.to_lowercase().as_str() {
180                "true" | "false" | "1" | "0" | "yes" | "no" => Ok(()),
181                _ => Err(
182                    translate_validation("validation.boolean", &[("attribute", field)])
183                        .unwrap_or_else(|| format!("The {field} field must be true or false.")),
184                ),
185            }
186        } else if let Some(n) = value.as_i64() {
187            if n == 0 || n == 1 {
188                Ok(())
189            } else {
190                Err(
191                    translate_validation("validation.boolean", &[("attribute", field)])
192                        .unwrap_or_else(|| format!("The {field} field must be true or false.")),
193                )
194            }
195        } else {
196            Err(
197                translate_validation("validation.boolean", &[("attribute", field)])
198                    .unwrap_or_else(|| format!("The {field} field must be true or false.")),
199            )
200        }
201    }
202
203    fn name(&self) -> &'static str {
204        "boolean"
205    }
206}
207
208/// Field must be an array.
209pub struct IsArray;
210
211pub const fn array() -> IsArray {
212    IsArray
213}
214
215impl Rule for IsArray {
216    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
217        if value.is_null() || value.is_array() {
218            Ok(())
219        } else {
220            Err(
221                translate_validation("validation.array", &[("attribute", field)])
222                    .unwrap_or_else(|| format!("The {field} field must be an array.")),
223            )
224        }
225    }
226
227    fn name(&self) -> &'static str {
228        "array"
229    }
230}
231
232// ============================================================================
233// Size Rules
234// ============================================================================
235
236/// Field must have a minimum size/length/value.
237pub struct Min {
238    min: f64,
239}
240
241pub fn min(min: impl Into<f64>) -> Min {
242    Min { min: min.into() }
243}
244
245impl Rule for Min {
246    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
247        if value.is_null() {
248            return Ok(());
249        }
250
251        let size = get_size(value);
252        if size < self.min {
253            let unit = get_size_unit(value);
254            let type_key = get_size_type_key("min", value);
255            let min_str = format!("{}", self.min as i64);
256            Err(
257                translate_validation(&type_key, &[("attribute", field), ("min", &min_str)])
258                    .unwrap_or_else(|| {
259                        format!(
260                            "The {} field must be at least {} {}.",
261                            field, self.min as i64, unit
262                        )
263                    }),
264            )
265        } else {
266            Ok(())
267        }
268    }
269
270    fn name(&self) -> &'static str {
271        "min"
272    }
273}
274
275/// Field must have a maximum size/length/value.
276pub struct Max {
277    max: f64,
278}
279
280pub fn max(max: impl Into<f64>) -> Max {
281    Max { max: max.into() }
282}
283
284impl Rule for Max {
285    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
286        if value.is_null() {
287            return Ok(());
288        }
289
290        let size = get_size(value);
291        if size > self.max {
292            let unit = get_size_unit(value);
293            let type_key = get_size_type_key("max", value);
294            let max_str = format!("{}", self.max as i64);
295            Err(
296                translate_validation(&type_key, &[("attribute", field), ("max", &max_str)])
297                    .unwrap_or_else(|| {
298                        format!(
299                            "The {} field must not be greater than {} {}.",
300                            field, self.max as i64, unit
301                        )
302                    }),
303            )
304        } else {
305            Ok(())
306        }
307    }
308
309    fn name(&self) -> &'static str {
310        "max"
311    }
312}
313
314/// Field must be between min and max size.
315pub struct Between {
316    min: f64,
317    max: f64,
318}
319
320pub fn between(min: impl Into<f64>, max: impl Into<f64>) -> Between {
321    Between {
322        min: min.into(),
323        max: max.into(),
324    }
325}
326
327impl Rule for Between {
328    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
329        if value.is_null() {
330            return Ok(());
331        }
332
333        let size = get_size(value);
334        if size < self.min || size > self.max {
335            let unit = get_size_unit(value);
336            let type_key = get_size_type_key("between", value);
337            let min_str = format!("{}", self.min as i64);
338            let max_str = format!("{}", self.max as i64);
339            Err(translate_validation(
340                &type_key,
341                &[("attribute", field), ("min", &min_str), ("max", &max_str)],
342            )
343            .unwrap_or_else(|| {
344                format!(
345                    "The {} field must be between {} and {} {}.",
346                    field, self.min as i64, self.max as i64, unit
347                )
348            }))
349        } else {
350            Ok(())
351        }
352    }
353
354    fn name(&self) -> &'static str {
355        "between"
356    }
357}
358
359// ============================================================================
360// Format Rules
361// ============================================================================
362
363static EMAIL_REGEX: LazyLock<Regex> =
364    LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap());
365
366/// Field must be a valid email address.
367pub struct Email;
368
369pub const fn email() -> Email {
370    Email
371}
372
373impl Rule for Email {
374    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
375        if value.is_null() {
376            return Ok(());
377        }
378
379        match value.as_str() {
380            Some(s) if EMAIL_REGEX.is_match(s) => Ok(()),
381            _ => Err(
382                translate_validation("validation.email", &[("attribute", field)])
383                    .unwrap_or_else(|| format!("The {field} field must be a valid email address.")),
384            ),
385        }
386    }
387
388    fn name(&self) -> &'static str {
389        "email"
390    }
391}
392
393static URL_REGEX: LazyLock<Regex> =
394    LazyLock::new(|| Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap());
395
396/// Field must be a valid URL.
397pub struct Url;
398
399pub const fn url() -> Url {
400    Url
401}
402
403impl Rule for Url {
404    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
405        if value.is_null() {
406            return Ok(());
407        }
408
409        match value.as_str() {
410            Some(s) if URL_REGEX.is_match(s) => Ok(()),
411            _ => Err(
412                translate_validation("validation.url", &[("attribute", field)])
413                    .unwrap_or_else(|| format!("The {field} field must be a valid URL.")),
414            ),
415        }
416    }
417
418    fn name(&self) -> &'static str {
419        "url"
420    }
421}
422
423/// Field must match a regex pattern.
424pub struct Regex_ {
425    pattern: Regex,
426}
427
428pub fn regex(pattern: &str) -> Regex_ {
429    Regex_ {
430        pattern: Regex::new(pattern).expect("Invalid regex pattern"),
431    }
432}
433
434impl Rule for Regex_ {
435    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
436        if value.is_null() {
437            return Ok(());
438        }
439
440        match value.as_str() {
441            Some(s) if self.pattern.is_match(s) => Ok(()),
442            _ => Err(
443                translate_validation("validation.regex", &[("attribute", field)])
444                    .unwrap_or_else(|| format!("The {field} field format is invalid.")),
445            ),
446        }
447    }
448
449    fn name(&self) -> &'static str {
450        "regex"
451    }
452}
453
454static ALPHA_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z]+$").unwrap());
455
456/// Field must contain only alphabetic characters.
457pub struct Alpha;
458
459pub const fn alpha() -> Alpha {
460    Alpha
461}
462
463impl Rule for Alpha {
464    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
465        if value.is_null() {
466            return Ok(());
467        }
468
469        match value.as_str() {
470            Some(s) if ALPHA_REGEX.is_match(s) => Ok(()),
471            _ => Err(
472                translate_validation("validation.alpha", &[("attribute", field)])
473                    .unwrap_or_else(|| format!("The {field} field must only contain letters.")),
474            ),
475        }
476    }
477
478    fn name(&self) -> &'static str {
479        "alpha"
480    }
481}
482
483static ALPHA_NUM_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]+$").unwrap());
484
485/// Field must contain only alphanumeric characters.
486pub struct AlphaNum;
487
488pub const fn alpha_num() -> AlphaNum {
489    AlphaNum
490}
491
492impl Rule for AlphaNum {
493    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
494        if value.is_null() {
495            return Ok(());
496        }
497
498        match value.as_str() {
499            Some(s) if ALPHA_NUM_REGEX.is_match(s) => Ok(()),
500            _ => Err(
501                translate_validation("validation.alpha_num", &[("attribute", field)])
502                    .unwrap_or_else(|| {
503                        format!("The {field} field must only contain letters and numbers.")
504                    }),
505            ),
506        }
507    }
508
509    fn name(&self) -> &'static str {
510        "alpha_num"
511    }
512}
513
514static ALPHA_DASH_REGEX: LazyLock<Regex> =
515    LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap());
516
517/// Field must contain only alphanumeric characters, dashes, and underscores.
518pub struct AlphaDash;
519
520pub const fn alpha_dash() -> AlphaDash {
521    AlphaDash
522}
523
524impl Rule for AlphaDash {
525    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
526        if value.is_null() {
527            return Ok(());
528        }
529
530        match value.as_str() {
531            Some(s) if ALPHA_DASH_REGEX.is_match(s) => Ok(()),
532            _ => Err(
533                translate_validation("validation.alpha_dash", &[("attribute", field)])
534                    .unwrap_or_else(|| {
535                        format!(
536                    "The {field} field must only contain letters, numbers, dashes, and underscores."
537                )
538                    }),
539            ),
540        }
541    }
542
543    fn name(&self) -> &'static str {
544        "alpha_dash"
545    }
546}
547
548// ============================================================================
549// Comparison Rules
550// ============================================================================
551
552/// Field must match another field.
553pub struct Confirmed {
554    confirmation_field: String,
555}
556
557pub fn confirmed() -> Confirmed {
558    Confirmed {
559        confirmation_field: String::new(), // Will be set based on field name
560    }
561}
562
563impl Rule for Confirmed {
564    fn validate(&self, field: &str, value: &Value, data: &Value) -> Result<(), String> {
565        if value.is_null() {
566            return Ok(());
567        }
568
569        let confirmation_field = if self.confirmation_field.is_empty() {
570            format!("{field}_confirmation")
571        } else {
572            self.confirmation_field.clone()
573        };
574
575        let confirmation_value = data.get(&confirmation_field);
576        if confirmation_value == Some(value) {
577            Ok(())
578        } else {
579            Err(
580                translate_validation("validation.confirmed", &[("attribute", field)])
581                    .unwrap_or_else(|| format!("The {field} confirmation does not match.")),
582            )
583        }
584    }
585
586    fn name(&self) -> &'static str {
587        "confirmed"
588    }
589}
590
591/// Field must be in a list of values.
592pub struct In {
593    values: Vec<Value>,
594}
595
596pub fn in_array<I, V>(values: I) -> In
597where
598    I: IntoIterator<Item = V>,
599    V: Into<Value>,
600{
601    In {
602        values: values.into_iter().map(|v| v.into()).collect(),
603    }
604}
605
606impl Rule for In {
607    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
608        if value.is_null() {
609            return Ok(());
610        }
611
612        if self.values.contains(value) {
613            Ok(())
614        } else {
615            Err(
616                translate_validation("validation.in", &[("attribute", field)])
617                    .unwrap_or_else(|| format!("The selected {field} is invalid.")),
618            )
619        }
620    }
621
622    fn name(&self) -> &'static str {
623        "in"
624    }
625}
626
627/// Field must not be in a list of values.
628pub struct NotIn {
629    values: Vec<Value>,
630}
631
632pub fn not_in<I, V>(values: I) -> NotIn
633where
634    I: IntoIterator<Item = V>,
635    V: Into<Value>,
636{
637    NotIn {
638        values: values.into_iter().map(|v| v.into()).collect(),
639    }
640}
641
642impl Rule for NotIn {
643    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
644        if value.is_null() {
645            return Ok(());
646        }
647
648        if self.values.contains(value) {
649            Err(
650                translate_validation("validation.not_in", &[("attribute", field)])
651                    .unwrap_or_else(|| format!("The selected {field} is invalid.")),
652            )
653        } else {
654            Ok(())
655        }
656    }
657
658    fn name(&self) -> &'static str {
659        "not_in"
660    }
661}
662
663/// Field must be different from another field.
664pub struct Different {
665    other: String,
666}
667
668pub fn different(other: impl Into<String>) -> Different {
669    Different {
670        other: other.into(),
671    }
672}
673
674impl Rule for Different {
675    fn validate(&self, field: &str, value: &Value, data: &Value) -> Result<(), String> {
676        if value.is_null() {
677            return Ok(());
678        }
679
680        let other_value = data.get(&self.other);
681        if other_value == Some(value) {
682            Err(translate_validation(
683                "validation.different",
684                &[("attribute", field), ("other", &self.other)],
685            )
686            .unwrap_or_else(|| {
687                format!("The {} and {} fields must be different.", field, self.other)
688            }))
689        } else {
690            Ok(())
691        }
692    }
693
694    fn name(&self) -> &'static str {
695        "different"
696    }
697}
698
699/// Field must be the same as another field.
700pub struct Same {
701    other: String,
702}
703
704pub fn same(other: impl Into<String>) -> Same {
705    Same {
706        other: other.into(),
707    }
708}
709
710impl Rule for Same {
711    fn validate(&self, field: &str, value: &Value, data: &Value) -> Result<(), String> {
712        if value.is_null() {
713            return Ok(());
714        }
715
716        let other_value = data.get(&self.other);
717        if other_value != Some(value) {
718            Err(translate_validation(
719                "validation.same",
720                &[("attribute", field), ("other", &self.other)],
721            )
722            .unwrap_or_else(|| format!("The {} and {} fields must match.", field, self.other)))
723        } else {
724            Ok(())
725        }
726    }
727
728    fn name(&self) -> &'static str {
729        "same"
730    }
731}
732
733// ============================================================================
734// Date Rules
735// ============================================================================
736
737/// Field must be a valid date.
738pub struct Date;
739
740pub const fn date() -> Date {
741    Date
742}
743
744impl Rule for Date {
745    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
746        if value.is_null() {
747            return Ok(());
748        }
749
750        if let Some(s) = value.as_str() {
751            // Try common date formats
752            if chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok()
753                || chrono::NaiveDate::parse_from_str(s, "%d/%m/%Y").is_ok()
754                || chrono::NaiveDate::parse_from_str(s, "%m/%d/%Y").is_ok()
755                || chrono::DateTime::parse_from_rfc3339(s).is_ok()
756            {
757                return Ok(());
758            }
759        }
760
761        Err(
762            translate_validation("validation.date", &[("attribute", field)])
763                .unwrap_or_else(|| format!("The {field} field must be a valid date.")),
764        )
765    }
766
767    fn name(&self) -> &'static str {
768        "date"
769    }
770}
771
772// ============================================================================
773// Special Rules
774// ============================================================================
775
776/// Field is optional - only validate if present.
777pub struct Nullable;
778
779pub const fn nullable() -> Nullable {
780    Nullable
781}
782
783impl Rule for Nullable {
784    fn validate(&self, _field: &str, _value: &Value, _data: &Value) -> Result<(), String> {
785        // This is a marker rule - validator handles it specially
786        Ok(())
787    }
788
789    fn name(&self) -> &'static str {
790        "nullable"
791    }
792}
793
794/// Field must be accepted (yes, on, 1, true).
795pub struct Accepted;
796
797pub const fn accepted() -> Accepted {
798    Accepted
799}
800
801impl Rule for Accepted {
802    fn validate(&self, field: &str, value: &Value, _data: &Value) -> Result<(), String> {
803        let accepted = match value {
804            Value::Bool(true) => true,
805            Value::Number(n) => n.as_i64() == Some(1),
806            Value::String(s) => {
807                matches!(s.to_lowercase().as_str(), "yes" | "on" | "1" | "true")
808            }
809            _ => false,
810        };
811
812        if accepted {
813            Ok(())
814        } else {
815            Err(
816                translate_validation("validation.accepted", &[("attribute", field)])
817                    .unwrap_or_else(|| format!("The {field} field must be accepted.")),
818            )
819        }
820    }
821
822    fn name(&self) -> &'static str {
823        "accepted"
824    }
825}
826
827// ============================================================================
828// Helper Functions
829// ============================================================================
830
831/// Get the size of a value (length for strings/arrays, value for numbers).
832fn get_size(value: &Value) -> f64 {
833    match value {
834        Value::String(s) => s.chars().count() as f64,
835        Value::Array(a) => a.len() as f64,
836        Value::Object(o) => o.len() as f64,
837        Value::Number(n) => n.as_f64().unwrap_or(0.0),
838        _ => 0.0,
839    }
840}
841
842/// Get the appropriate unit for size validation messages.
843fn get_size_unit(value: &Value) -> &'static str {
844    match value {
845        Value::String(_) => "characters",
846        Value::Array(_) => "items",
847        Value::Object(_) => "items",
848        _ => "",
849    }
850}
851
852/// Get the type-specific translation key for size rules (min, max, between).
853fn get_size_type_key(rule: &str, value: &Value) -> String {
854    let suffix = match value {
855        Value::String(_) => "string",
856        Value::Array(_) | Value::Object(_) => "array",
857        _ => "numeric",
858    };
859    format!("validation.{rule}.{suffix}")
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865    use serde_json::json;
866
867    #[test]
868    fn test_required() {
869        let rule = required();
870        let data = json!({});
871
872        assert!(rule.validate("name", &json!("John"), &data).is_ok());
873        assert!(rule.validate("name", &json!(null), &data).is_err());
874        assert!(rule.validate("name", &json!(""), &data).is_err());
875        assert!(rule.validate("name", &json!("  "), &data).is_err());
876    }
877
878    #[test]
879    fn test_email() {
880        let rule = email();
881        let data = json!({});
882
883        assert!(rule
884            .validate("email", &json!("test@example.com"), &data)
885            .is_ok());
886        assert!(rule.validate("email", &json!("invalid"), &data).is_err());
887        assert!(rule.validate("email", &json!(null), &data).is_ok());
888    }
889
890    #[test]
891    fn test_min_max() {
892        let data = json!({});
893
894        // String length
895        assert!(min(3).validate("name", &json!("John"), &data).is_ok());
896        assert!(min(5).validate("name", &json!("John"), &data).is_err());
897
898        assert!(max(5).validate("name", &json!("John"), &data).is_ok());
899        assert!(max(2).validate("name", &json!("John"), &data).is_err());
900
901        // Numeric value
902        assert!(min(18).validate("age", &json!(25), &data).is_ok());
903        assert!(min(18).validate("age", &json!(15), &data).is_err());
904    }
905
906    #[test]
907    fn test_between() {
908        let rule = between(1, 10);
909        let data = json!({});
910
911        assert!(rule.validate("count", &json!(5), &data).is_ok());
912        assert!(rule.validate("count", &json!(0), &data).is_err());
913        assert!(rule.validate("count", &json!(11), &data).is_err());
914    }
915
916    #[test]
917    fn test_in_array() {
918        let rule = in_array(["active", "inactive", "pending"]);
919        let data = json!({});
920
921        assert!(rule.validate("status", &json!("active"), &data).is_ok());
922        assert!(rule.validate("status", &json!("unknown"), &data).is_err());
923    }
924
925    #[test]
926    fn test_confirmed() {
927        let rule = confirmed();
928        let data = json!({
929            "password": "secret123",
930            "password_confirmation": "secret123"
931        });
932
933        assert!(rule
934            .validate("password", &json!("secret123"), &data)
935            .is_ok());
936
937        let bad_data = json!({
938            "password": "secret123",
939            "password_confirmation": "different"
940        });
941        assert!(rule
942            .validate("password", &json!("secret123"), &bad_data)
943            .is_err());
944    }
945
946    #[test]
947    fn test_url() {
948        let rule = url();
949        let data = json!({});
950
951        assert!(rule
952            .validate("website", &json!("https://example.com"), &data)
953            .is_ok());
954        assert!(rule
955            .validate("website", &json!("http://example.com/path"), &data)
956            .is_ok());
957        assert!(rule
958            .validate("website", &json!("not-a-url"), &data)
959            .is_err());
960    }
961
962    #[test]
963    fn test_rules_call_translate_validation() {
964        fn mock(key: &str, params: &[(&str, &str)]) -> Option<String> {
965            let attr = params
966                .iter()
967                .find(|(k, _)| *k == "attribute")
968                .map(|(_, v)| *v)
969                .unwrap_or("?");
970            Some(format!("[translated] {key}: attr={attr}"))
971        }
972
973        // OnceLock: if already set by another test, skip gracefully
974        let was_set = crate::validation::bridge::VALIDATION_TRANSLATOR
975            .set(mock as crate::validation::TranslatorFn)
976            .is_ok();
977
978        let result = required().validate("email", &json!(null), &json!({}));
979        assert!(result.is_err());
980
981        if was_set {
982            let msg = result.unwrap_err();
983            assert!(
984                msg.contains("[translated]"),
985                "Expected translated message, got: {msg}"
986            );
987            assert!(
988                msg.contains("validation.required"),
989                "Expected key in message, got: {msg}"
990            );
991        }
992    }
993
994    #[test]
995    fn test_string() {
996        let rule = string();
997        let data = json!({});
998
999        assert!(rule.validate("name", &json!("hello"), &data).is_ok());
1000        assert!(rule.validate("name", &json!(""), &data).is_ok());
1001        assert!(rule.validate("name", &json!(42), &data).is_err());
1002        assert!(rule.validate("name", &json!(true), &data).is_err());
1003        assert!(rule.validate("name", &json!([1, 2]), &data).is_err());
1004        // Null passthrough
1005        assert!(rule.validate("name", &json!(null), &data).is_ok());
1006    }
1007
1008    #[test]
1009    fn test_integer() {
1010        let rule = integer();
1011        let data = json!({});
1012
1013        assert!(rule.validate("age", &json!(42), &data).is_ok());
1014        assert!(rule.validate("age", &json!(0), &data).is_ok());
1015        assert!(rule.validate("age", &json!(-5), &data).is_ok());
1016        // String integers are accepted
1017        assert!(rule.validate("age", &json!("123"), &data).is_ok());
1018        // Floats are not integers
1019        assert!(rule.validate("age", &json!(3.17), &data).is_err());
1020        assert!(rule.validate("age", &json!("hello"), &data).is_err());
1021        assert!(rule.validate("age", &json!(true), &data).is_err());
1022        // Null passthrough
1023        assert!(rule.validate("age", &json!(null), &data).is_ok());
1024    }
1025
1026    #[test]
1027    fn test_numeric() {
1028        let rule = numeric();
1029        let data = json!({});
1030
1031        assert!(rule.validate("price", &json!(42), &data).is_ok());
1032        assert!(rule.validate("price", &json!(3.17), &data).is_ok());
1033        assert!(rule.validate("price", &json!(-10), &data).is_ok());
1034        // String numbers are accepted
1035        assert!(rule.validate("price", &json!("42.5"), &data).is_ok());
1036        assert!(rule.validate("price", &json!("hello"), &data).is_err());
1037        assert!(rule.validate("price", &json!(true), &data).is_err());
1038        // Null passthrough
1039        assert!(rule.validate("price", &json!(null), &data).is_ok());
1040    }
1041
1042    #[test]
1043    fn test_boolean() {
1044        let rule = boolean();
1045        let data = json!({});
1046
1047        assert!(rule.validate("active", &json!(true), &data).is_ok());
1048        assert!(rule.validate("active", &json!(false), &data).is_ok());
1049        // String booleans are accepted
1050        assert!(rule.validate("active", &json!("true"), &data).is_ok());
1051        assert!(rule.validate("active", &json!("false"), &data).is_ok());
1052        assert!(rule.validate("active", &json!("yes"), &data).is_ok());
1053        assert!(rule.validate("active", &json!("no"), &data).is_ok());
1054        assert!(rule.validate("active", &json!("1"), &data).is_ok());
1055        assert!(rule.validate("active", &json!("0"), &data).is_ok());
1056        // Integer 0 and 1 are accepted
1057        assert!(rule.validate("active", &json!(1), &data).is_ok());
1058        assert!(rule.validate("active", &json!(0), &data).is_ok());
1059        // Non-boolean strings rejected
1060        assert!(rule.validate("active", &json!("maybe"), &data).is_err());
1061        // Other integers rejected
1062        assert!(rule.validate("active", &json!(42), &data).is_err());
1063        // Null passthrough
1064        assert!(rule.validate("active", &json!(null), &data).is_ok());
1065    }
1066
1067    #[test]
1068    fn test_array() {
1069        let rule = array();
1070        let data = json!({});
1071
1072        assert!(rule.validate("items", &json!([1, 2, 3]), &data).is_ok());
1073        assert!(rule.validate("items", &json!([]), &data).is_ok());
1074        assert!(rule.validate("items", &json!(["a", "b"]), &data).is_ok());
1075        assert!(rule.validate("items", &json!("not array"), &data).is_err());
1076        assert!(rule.validate("items", &json!(42), &data).is_err());
1077        assert!(rule.validate("items", &json!(true), &data).is_err());
1078        // Null passthrough
1079        assert!(rule.validate("items", &json!(null), &data).is_ok());
1080    }
1081
1082    #[test]
1083    fn test_required_if() {
1084        let data = json!({"role": "admin"});
1085
1086        // role == admin, so name is required
1087        assert!(required_if("role", "admin")
1088            .validate("name", &json!(null), &data)
1089            .is_err());
1090        assert!(required_if("role", "admin")
1091            .validate("name", &json!(""), &data)
1092            .is_err());
1093        assert!(required_if("role", "admin")
1094            .validate("name", &json!("Alice"), &data)
1095            .is_ok());
1096
1097        // role != "user", so name is not required
1098        assert!(required_if("role", "user")
1099            .validate("name", &json!(null), &data)
1100            .is_ok());
1101        assert!(required_if("role", "user")
1102            .validate("name", &json!(""), &data)
1103            .is_ok());
1104    }
1105
1106    #[test]
1107    fn test_different() {
1108        let data = json!({"other_field": "b"});
1109
1110        // Different values pass
1111        assert!(different("other_field")
1112            .validate("field", &json!("a"), &data)
1113            .is_ok());
1114        // Same values fail
1115        assert!(different("other_field")
1116            .validate("field", &json!("b"), &data)
1117            .is_err());
1118        // Null passthrough
1119        assert!(different("other_field")
1120            .validate("field", &json!(null), &data)
1121            .is_ok());
1122    }
1123
1124    #[test]
1125    fn test_same() {
1126        let data = json!({"other_field": "a"});
1127
1128        // Same values pass
1129        assert!(same("other_field")
1130            .validate("field", &json!("a"), &data)
1131            .is_ok());
1132        // Different values fail
1133        assert!(same("other_field")
1134            .validate("field", &json!("b"), &data)
1135            .is_err());
1136        // Null passthrough
1137        assert!(same("other_field")
1138            .validate("field", &json!(null), &data)
1139            .is_ok());
1140    }
1141
1142    #[test]
1143    fn test_regex() {
1144        let rule = regex(r"^\d{3}-\d{4}$");
1145        let data = json!({});
1146
1147        assert!(rule.validate("phone", &json!("123-4567"), &data).is_ok());
1148        assert!(rule.validate("phone", &json!("abc"), &data).is_err());
1149        assert!(rule.validate("phone", &json!("12-345"), &data).is_err());
1150        // Null passthrough
1151        assert!(rule.validate("phone", &json!(null), &data).is_ok());
1152    }
1153
1154    #[test]
1155    fn test_alpha() {
1156        let rule = alpha();
1157        let data = json!({});
1158
1159        assert!(rule.validate("name", &json!("Hello"), &data).is_ok());
1160        assert!(rule.validate("name", &json!("abc"), &data).is_ok());
1161        assert!(rule.validate("name", &json!("Hello123"), &data).is_err());
1162        assert!(rule.validate("name", &json!("hello world"), &data).is_err());
1163        // Empty string does not match alpha regex
1164        assert!(rule.validate("name", &json!(""), &data).is_err());
1165        // Null passthrough
1166        assert!(rule.validate("name", &json!(null), &data).is_ok());
1167    }
1168
1169    #[test]
1170    fn test_alpha_num() {
1171        let rule = alpha_num();
1172        let data = json!({});
1173
1174        assert!(rule.validate("code", &json!("Hello123"), &data).is_ok());
1175        assert!(rule.validate("code", &json!("abc"), &data).is_ok());
1176        assert!(rule.validate("code", &json!("123"), &data).is_ok());
1177        assert!(rule.validate("code", &json!("Hello!@#"), &data).is_err());
1178        assert!(rule.validate("code", &json!("hello world"), &data).is_err());
1179        // Null passthrough
1180        assert!(rule.validate("code", &json!(null), &data).is_ok());
1181    }
1182
1183    #[test]
1184    fn test_alpha_dash() {
1185        let rule = alpha_dash();
1186        let data = json!({});
1187
1188        assert!(rule
1189            .validate("slug", &json!("hello-world_123"), &data)
1190            .is_ok());
1191        assert!(rule.validate("slug", &json!("abc"), &data).is_ok());
1192        assert!(rule.validate("slug", &json!("a-b_c"), &data).is_ok());
1193        assert!(rule.validate("slug", &json!("hello world"), &data).is_err());
1194        assert!(rule.validate("slug", &json!("hello!@#"), &data).is_err());
1195        // Null passthrough
1196        assert!(rule.validate("slug", &json!(null), &data).is_ok());
1197    }
1198
1199    #[test]
1200    fn test_not_in() {
1201        let rule = not_in(["banned", "blocked"]);
1202        let data = json!({});
1203
1204        assert!(rule.validate("status", &json!("active"), &data).is_ok());
1205        assert!(rule.validate("status", &json!("approved"), &data).is_ok());
1206        assert!(rule.validate("status", &json!("banned"), &data).is_err());
1207        assert!(rule.validate("status", &json!("blocked"), &data).is_err());
1208        // Null passthrough
1209        assert!(rule.validate("status", &json!(null), &data).is_ok());
1210    }
1211
1212    #[test]
1213    fn test_date() {
1214        let rule = date();
1215        let data = json!({});
1216
1217        // ISO date format
1218        assert!(rule
1219            .validate("birthday", &json!("2024-01-15"), &data)
1220            .is_ok());
1221        // RFC3339 datetime
1222        assert!(rule
1223            .validate("created", &json!("2024-01-15T10:30:00Z"), &data)
1224            .is_ok());
1225        // Invalid strings
1226        assert!(rule
1227            .validate("birthday", &json!("not-a-date"), &data)
1228            .is_err());
1229        assert!(rule
1230            .validate("birthday", &json!("2024-13-01"), &data)
1231            .is_err());
1232        // Non-string
1233        assert!(rule.validate("birthday", &json!(42), &data).is_err());
1234        // Null passthrough
1235        assert!(rule.validate("birthday", &json!(null), &data).is_ok());
1236    }
1237
1238    #[test]
1239    fn test_nullable() {
1240        let rule = nullable();
1241        let data = json!({});
1242
1243        // Nullable always passes - it's a marker rule
1244        assert!(rule.validate("field", &json!(null), &data).is_ok());
1245        assert!(rule.validate("field", &json!("value"), &data).is_ok());
1246        assert!(rule.validate("field", &json!(42), &data).is_ok());
1247        assert!(rule.validate("field", &json!(true), &data).is_ok());
1248    }
1249
1250    #[test]
1251    fn test_accepted() {
1252        let rule = accepted();
1253        let data = json!({});
1254
1255        // Accepted values
1256        assert!(rule.validate("terms", &json!(true), &data).is_ok());
1257        assert!(rule.validate("terms", &json!("yes"), &data).is_ok());
1258        assert!(rule.validate("terms", &json!("on"), &data).is_ok());
1259        assert!(rule.validate("terms", &json!("1"), &data).is_ok());
1260        assert!(rule.validate("terms", &json!("true"), &data).is_ok());
1261        assert!(rule.validate("terms", &json!(1), &data).is_ok());
1262
1263        // Rejected values
1264        assert!(rule.validate("terms", &json!(false), &data).is_err());
1265        assert!(rule.validate("terms", &json!("no"), &data).is_err());
1266        assert!(rule.validate("terms", &json!("off"), &data).is_err());
1267        assert!(rule.validate("terms", &json!(0), &data).is_err());
1268        assert!(rule.validate("terms", &json!(null), &data).is_err());
1269        assert!(rule.validate("terms", &json!("false"), &data).is_err());
1270    }
1271
1272    #[test]
1273    fn test_size_type_key_selection() {
1274        // String values use "string" subkey
1275        assert_eq!(
1276            get_size_type_key("min", &json!("hello")),
1277            "validation.min.string"
1278        );
1279        assert_eq!(
1280            get_size_type_key("max", &json!("hello")),
1281            "validation.max.string"
1282        );
1283        assert_eq!(
1284            get_size_type_key("between", &json!("hello")),
1285            "validation.between.string"
1286        );
1287
1288        // Numeric values use "numeric" subkey
1289        assert_eq!(
1290            get_size_type_key("min", &json!(42)),
1291            "validation.min.numeric"
1292        );
1293        assert_eq!(
1294            get_size_type_key("max", &json!(42)),
1295            "validation.max.numeric"
1296        );
1297        assert_eq!(
1298            get_size_type_key("between", &json!(42)),
1299            "validation.between.numeric"
1300        );
1301
1302        // Array values use "array" subkey
1303        assert_eq!(
1304            get_size_type_key("min", &json!([1, 2, 3])),
1305            "validation.min.array"
1306        );
1307        assert_eq!(
1308            get_size_type_key("max", &json!([1, 2, 3])),
1309            "validation.max.array"
1310        );
1311        assert_eq!(
1312            get_size_type_key("between", &json!([1, 2, 3])),
1313            "validation.between.array"
1314        );
1315    }
1316}