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