radix_leptos_primitives/components/form_validation/
validation.rs

1use std::collections::HashMap;
2use regex::Regex;
3
4/// Validation Mode enum
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum ValidationMode {
7    OnChange,
8    OnBlur,
9    OnSubmit,
10    Manual,
11}
12
13impl ValidationMode {
14    pub fn as_str(&self) -> &'static str {
15        match self {
16            ValidationMode::OnChange => "on-change",
17            ValidationMode::OnBlur => "on-blur",
18            ValidationMode::OnSubmit => "on-submit",
19            ValidationMode::Manual => "manual",
20        }
21    }
22}
23
24/// Validation Rule struct
25#[derive(Debug, Clone, PartialEq)]
26pub struct ValidationRule {
27    pub rule_type: ValidationRuleType,
28    pub message: String,
29    pub value: Option<String>,
30}
31
32impl Default for ValidationRule {
33    fn default() -> Self {
34        Self {
35            rule_type: ValidationRuleType::Required,
36            message: "This field is required".to_string(),
37            value: None,
38        }
39    }
40}
41
42/// Validation Rule Type enum
43#[derive(Debug, Clone, PartialEq)]
44pub enum ValidationRuleType {
45    Required,
46    MinLength(usize),
47    MaxLength(usize),
48    Min(f64),
49    Max(f64),
50    Pattern(String),
51    Email,
52    Url,
53    Phone,
54    Date,
55    Time,
56    Number,
57    Integer,
58    Custom(String),
59}
60
61/// Custom Validator function type
62pub type CustomValidator = Box<dyn Fn(&str) -> ValidationResult + Send + Sync>;
63
64/// Validation Result struct
65#[derive(Debug, Clone, PartialEq)]
66pub struct ValidationResult {
67    pub is_valid: bool,
68    pub message: Option<String>,
69}
70
71impl Default for ValidationResult {
72    fn default() -> Self {
73        Self {
74            is_valid: true,
75            message: None,
76        }
77    }
78}
79
80/// Field Validation Result struct
81#[derive(Debug, Clone, PartialEq)]
82pub struct FieldValidationResult {
83    pub field_name: String,
84    pub is_valid: bool,
85    pub errors: Vec<String>,
86    pub warnings: Vec<String>,
87}
88
89impl Default for FieldValidationResult {
90    fn default() -> Self {
91        Self {
92            field_name: String::new(),
93            is_valid: true,
94            errors: Vec::new(),
95            warnings: Vec::new(),
96        }
97    }
98}
99
100/// Form Validation State struct
101#[derive(Debug, Clone, PartialEq)]
102pub struct FormValidationState {
103    pub is_valid: bool,
104    pub is_submitting: bool,
105    pub is_dirty: bool,
106    pub is_touched: bool,
107    pub field_errors: HashMap<String, FieldError>,
108    pub form_errors: Vec<FormError>,
109}
110
111impl Default for FormValidationState {
112    fn default() -> Self {
113        Self {
114            is_valid: true,
115            is_submitting: false,
116            is_dirty: false,
117            is_touched: false,
118            field_errors: HashMap::new(),
119            form_errors: Vec::new(),
120        }
121    }
122}
123
124/// Field Error struct
125#[derive(Debug, Clone, PartialEq)]
126pub struct FieldError {
127    pub field_name: String,
128    pub message: String,
129    pub error_type: ErrorType,
130    pub timestamp: u64,
131}
132
133impl Default for FieldError {
134    fn default() -> Self {
135        Self {
136            field_name: String::new(),
137            message: String::new(),
138            error_type: ErrorType::Validation,
139            timestamp: 0,
140        }
141    }
142}
143
144/// Form Error struct
145#[derive(Debug, Clone, PartialEq)]
146pub struct FormError {
147    pub field: String,
148    pub message: String,
149    pub error_type: ErrorType,
150}
151
152impl Default for FormError {
153    fn default() -> Self {
154        Self {
155            field: String::new(),
156            message: String::new(),
157            error_type: ErrorType::Validation,
158        }
159    }
160}
161
162/// Error Type enum
163#[derive(Debug, Clone, PartialEq)]
164pub enum ErrorType {
165    Validation,
166    Network,
167    Server,
168    Custom,
169}
170
171/// Validation Engine
172pub struct ValidationEngine {
173    rules: HashMap<String, Vec<ValidationRule>>,
174    custom_validators: HashMap<String, CustomValidator>,
175}
176
177impl Default for ValidationEngine {
178    fn default() -> Self {
179        Self {
180            rules: HashMap::new(),
181            custom_validators: HashMap::new(),
182        }
183    }
184}
185
186impl ValidationEngine {
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    pub fn add_rule(&mut self, field_name: String, rule: ValidationRule) {
192        self.rules.entry(field_name).or_insert_with(Vec::new).push(rule);
193    }
194
195    pub fn add_custom_validator(&mut self, name: String, validator: CustomValidator) {
196        self.custom_validators.insert(name, validator);
197    }
198
199    pub fn has_rules(&self) -> bool {
200        !self.rules.is_empty()
201    }
202
203    pub fn has_rule_for_field(&self, field_name: &str) -> bool {
204        self.rules.contains_key(field_name)
205    }
206
207    pub fn has_custom_validators(&self) -> bool {
208        !self.custom_validators.is_empty()
209    }
210
211    pub fn validate_field(&self, field_name: &str, value: &str) -> FieldValidationResult {
212        let mut result = FieldValidationResult {
213            field_name: field_name.to_string(),
214            is_valid: true,
215            errors: Vec::new(),
216            warnings: Vec::new(),
217        };
218
219        if let Some(rules) = self.rules.get(field_name) {
220            for rule in rules {
221                let validation_result = self.validate_rule(rule, value);
222                if !validation_result.is_valid {
223                    result.is_valid = false;
224                    if let Some(message) = validation_result.message {
225                        result.errors.push(message);
226                    }
227                }
228            }
229        }
230
231        result
232    }
233
234    pub fn validate_form(&self, form_data: &HashMap<String, String>) -> FormValidationState {
235        let mut state = FormValidationState::default();
236        let mut all_valid = true;
237
238        for (field_name, value) in form_data {
239            let field_result = self.validate_field(field_name, value);
240            if !field_result.is_valid {
241                all_valid = false;
242                let field_error = FieldError {
243                    field_name: field_name.clone(),
244                    message: field_result.errors.join(", "),
245                    error_type: ErrorType::Validation,
246                    timestamp: std::time::SystemTime::now()
247                        .duration_since(std::time::UNIX_EPOCH)
248                        .unwrap()
249                        .as_secs(),
250                };
251                state.field_errors.insert(field_name.clone(), field_error);
252            }
253        }
254
255        state.is_valid = all_valid;
256        state
257    }
258
259    fn validate_rule(&self, rule: &ValidationRule, value: &str) -> ValidationResult {
260        match &rule.rule_type {
261            ValidationRuleType::Required => {
262                if value.trim().is_empty() {
263                    ValidationResult {
264                        is_valid: false,
265                        message: Some(rule.message.clone()),
266                    }
267                } else {
268                    ValidationResult::default()
269                }
270            }
271            ValidationRuleType::MinLength(min_len) => {
272                if value.len() < *min_len {
273                    ValidationResult {
274                        is_valid: false,
275                        message: Some(rule.message.clone()),
276                    }
277                } else {
278                    ValidationResult::default()
279                }
280            }
281            ValidationRuleType::MaxLength(max_len) => {
282                if value.len() > *max_len {
283                    ValidationResult {
284                        is_valid: false,
285                        message: Some(rule.message.clone()),
286                    }
287                } else {
288                    ValidationResult::default()
289                }
290            }
291            ValidationRuleType::Min(min_val) => {
292                if let Ok(num) = value.parse::<f64>() {
293                    if num < *min_val {
294                        ValidationResult {
295                            is_valid: false,
296                            message: Some(rule.message.clone()),
297                        }
298                    } else {
299                        ValidationResult::default()
300                    }
301                } else {
302                    ValidationResult {
303                        is_valid: false,
304                        message: Some(rule.message.clone()),
305                    }
306                }
307            }
308            ValidationRuleType::Max(max_val) => {
309                if let Ok(num) = value.parse::<f64>() {
310                    if num > *max_val {
311                        ValidationResult {
312                            is_valid: false,
313                            message: Some(rule.message.clone()),
314                        }
315                    } else {
316                        ValidationResult::default()
317                    }
318                } else {
319                    ValidationResult {
320                        is_valid: false,
321                        message: Some(rule.message.clone()),
322                    }
323                }
324            }
325            ValidationRuleType::Pattern(pattern) => {
326                if let Ok(regex) = Regex::new(pattern) {
327                    if !regex.is_match(value) {
328                        ValidationResult {
329                            is_valid: false,
330                            message: Some(rule.message.clone()),
331                        }
332                    } else {
333                        ValidationResult::default()
334                    }
335                } else {
336                    ValidationResult {
337                        is_valid: false,
338                        message: Some(rule.message.clone()),
339                    }
340                }
341            }
342            ValidationRuleType::Email => {
343                if !is_valid_email(value) {
344                    ValidationResult {
345                        is_valid: false,
346                        message: Some(rule.message.clone()),
347                    }
348                } else {
349                    ValidationResult::default()
350                }
351            }
352            ValidationRuleType::Url => {
353                if !is_valid_url(value) {
354                    ValidationResult {
355                        is_valid: false,
356                        message: Some(rule.message.clone()),
357                    }
358                } else {
359                    ValidationResult::default()
360                }
361            }
362            ValidationRuleType::Phone => {
363                if !is_valid_phone(value) {
364                    ValidationResult {
365                        is_valid: false,
366                        message: Some(rule.message.clone()),
367                    }
368                } else {
369                    ValidationResult::default()
370                }
371            }
372            ValidationRuleType::Date => {
373                if !is_valid_date(value) {
374                    ValidationResult {
375                        is_valid: false,
376                        message: Some(rule.message.clone()),
377                    }
378                } else {
379                    ValidationResult::default()
380                }
381            }
382            ValidationRuleType::Time => {
383                if !is_valid_time(value) {
384                    ValidationResult {
385                        is_valid: false,
386                        message: Some(rule.message.clone()),
387                    }
388                } else {
389                    ValidationResult::default()
390                }
391            }
392            ValidationRuleType::Number => {
393                if !is_valid_number(value) {
394                    ValidationResult {
395                        is_valid: false,
396                        message: Some(rule.message.clone()),
397                    }
398                } else {
399                    ValidationResult::default()
400                }
401            }
402            ValidationRuleType::Integer => {
403                if !is_valid_integer(value) {
404                    ValidationResult {
405                        is_valid: false,
406                        message: Some(rule.message.clone()),
407                    }
408                } else {
409                    ValidationResult::default()
410                }
411            }
412            ValidationRuleType::Custom(name) => {
413                if let Some(validator) = self.custom_validators.get(name) {
414                    validator(value)
415                } else {
416                    ValidationResult::default()
417                }
418            }
419        }
420    }
421}
422
423/// Email validation
424pub fn is_valid_email(email: &str) -> bool {
425    let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
426    email_regex.is_match(email)
427}
428
429/// URL validation
430pub fn is_valid_url(url: &str) -> bool {
431    let url_regex = Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap();
432    url_regex.is_match(url)
433}
434
435/// Phone validation
436pub fn is_valid_phone(phone: &str) -> bool {
437    let phone_regex = Regex::new(r"^\+?[\d\s\-\(\)]{10,}$").unwrap();
438    phone_regex.is_match(phone)
439}
440
441/// Date validation
442pub fn is_valid_date(date: &str) -> bool {
443    let date_regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
444    if !date_regex.is_match(date) {
445        return false;
446    }
447    
448    // Parse the date to validate actual values
449    let parts: Vec<&str> = date.split('-').collect();
450    if parts.len() != 3 {
451        return false;
452    }
453    
454    let year: i32 = parts[0].parse().unwrap_or(0);
455    let month: u32 = parts[1].parse().unwrap_or(0);
456    let day: u32 = parts[2].parse().unwrap_or(0);
457    
458    // Basic validation
459    if year < 1 || month < 1 || month > 12 || day < 1 || day > 31 {
460        return false;
461    }
462    
463    // More specific validation for days per month
464    let days_in_month = match month {
465        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
466        4 | 6 | 9 | 11 => 30,
467        2 => if is_leap_year(year) { 29 } else { 28 },
468        _ => return false,
469    };
470    
471    day <= days_in_month
472}
473
474/// Helper function to check if a year is a leap year
475fn is_leap_year(year: i32) -> bool {
476    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
477}
478
479/// Time validation
480pub fn is_valid_time(time: &str) -> bool {
481    let time_regex = Regex::new(r"^\d{2}:\d{2}(:\d{2})?$").unwrap();
482    if !time_regex.is_match(time) {
483        return false;
484    }
485    
486    // Parse the time to validate actual values
487    let parts: Vec<&str> = time.split(':').collect();
488    if parts.len() < 2 || parts.len() > 3 {
489        return false;
490    }
491    
492    let hour: u32 = parts[0].parse().unwrap_or(99);
493    let minute: u32 = parts[1].parse().unwrap_or(99);
494    let second: u32 = if parts.len() == 3 { parts[2].parse().unwrap_or(99) } else { 0 };
495    
496    // Validate ranges
497    hour < 24 && minute < 60 && second < 60
498}
499
500/// Number validation
501pub fn is_valid_number(number: &str) -> bool {
502    number.parse::<f64>().is_ok()
503}
504
505/// Integer validation
506pub fn is_valid_integer(integer: &str) -> bool {
507    integer.parse::<i64>().is_ok()
508}
509
510#[cfg(test)]
511mod validation_tests {
512    use super::*;
513    use proptest::prelude::*;
514use crate::utils::{merge_optional_classes, generate_id};
515
516    #[test]
517    fn test_validation_mode_enum() {
518        assert_eq!(ValidationMode::OnChange.as_str(), "on-change");
519        assert_eq!(ValidationMode::OnBlur.as_str(), "on-blur");
520        assert_eq!(ValidationMode::OnSubmit.as_str(), "on-submit");
521        assert_eq!(ValidationMode::Manual.as_str(), "manual");
522    }
523
524    #[test]
525    fn test_validation_rule_default() {
526        let rule = ValidationRule::default();
527        assert_eq!(rule.rule_type, ValidationRuleType::Required);
528        assert_eq!(rule.message, "This field is required");
529    }
530
531    #[test]
532    fn test_validation_result_default() {
533        let result = ValidationResult::default();
534        assert!(result.is_valid);
535        assert!(result.message.is_none());
536    }
537
538    #[test]
539    fn test_field_validation_result_default() {
540        let result = FieldValidationResult::default();
541        assert!(result.is_valid);
542        assert!(result.errors.is_empty());
543        assert!(result.warnings.is_empty());
544    }
545
546    #[test]
547    fn test_form_validation_state_default() {
548        let state = FormValidationState::default();
549        assert!(state.is_valid);
550        assert!(!state.is_submitting);
551        assert!(!state.is_dirty);
552        assert!(!state.is_touched);
553    }
554
555    #[test]
556    fn test_field_error_default() {
557        let error = FieldError::default();
558        assert_eq!(error.error_type, ErrorType::Validation);
559        assert_eq!(error.timestamp, 0);
560    }
561
562    #[test]
563    fn test_form_error_default() {
564        let error = FormError::default();
565        assert_eq!(error.error_type, ErrorType::Validation);
566    }
567
568    #[test]
569    fn test_validation_engine_new() {
570        let engine = ValidationEngine::new();
571        assert!(engine.rules.is_empty());
572        assert!(engine.custom_validators.is_empty());
573    }
574
575    #[test]
576    fn test_validation_engine_add_rule() {
577        let mut engine = ValidationEngine::new();
578        let rule = ValidationRule {
579            rule_type: ValidationRuleType::Required,
580            message: "Field is required".to_string(),
581            value: None,
582        };
583        engine.add_rule("email".to_string(), rule);
584        assert!(engine.rules.contains_key("email"));
585    }
586
587    #[test]
588    fn test_validation_engine_validate_field() {
589        let mut engine = ValidationEngine::new();
590        let rule = ValidationRule {
591            rule_type: ValidationRuleType::Required,
592            message: "Field is required".to_string(),
593            value: None,
594        };
595        engine.add_rule("email".to_string(), rule);
596        
597        let result = engine.validate_field("email", "");
598        assert!(!result.is_valid);
599        assert!(!result.errors.is_empty());
600        
601        let result = engine.validate_field("email", "test@example.com");
602        assert!(result.is_valid);
603        assert!(result.errors.is_empty());
604    }
605
606    #[test]
607    fn test_validation_engine_validate_form() {
608        let mut engine = ValidationEngine::new();
609        let rule = ValidationRule {
610            rule_type: ValidationRuleType::Required,
611            message: "Field is required".to_string(),
612            value: None,
613        };
614        engine.add_rule("email".to_string(), rule);
615        
616        let mut form_data = HashMap::new();
617        form_data.insert("email".to_string(), "".to_string());
618        
619        let state = engine.validate_form(&form_data);
620        assert!(!state.is_valid);
621        assert!(!state.field_errors.is_empty());
622    }
623
624    #[test]
625    fn test_email_validation() {
626        assert!(is_valid_email("test@example.com"));
627        assert!(is_valid_email("user.name+tag@domain.co.uk"));
628        assert!(!is_valid_email("invalid-email"));
629        assert!(!is_valid_email("@domain.com"));
630        assert!(!is_valid_email("user@"));
631    }
632
633    #[test]
634    fn test_url_validation() {
635        assert!(is_valid_url("https://example.com"));
636        assert!(is_valid_url("http://subdomain.example.com/path"));
637        assert!(!is_valid_url("invalid-url"));
638        assert!(!is_valid_url("ftp://example.com"));
639        assert!(!is_valid_url("example.com"));
640    }
641
642    #[test]
643    fn test_phone_validation() {
644        assert!(is_valid_phone("+1234567890"));
645        assert!(is_valid_phone("(123) 456-7890"));
646        assert!(is_valid_phone("123-456-7890"));
647        assert!(!is_valid_phone("123"));
648        assert!(!is_valid_phone("invalid-phone"));
649    }
650
651    #[test]
652    fn test_date_validation() {
653        assert!(is_valid_date("2023-12-25"));
654        assert!(is_valid_date("2000-01-01"));
655        assert!(!is_valid_date("12/25/2023"));
656        assert!(!is_valid_date("2023-13-01"));
657        assert!(!is_valid_date("invalid-date"));
658    }
659
660    #[test]
661    fn test_time_validation() {
662        assert!(is_valid_time("14:30"));
663        assert!(is_valid_time("09:15:45"));
664        assert!(!is_valid_time("25:00"));
665        assert!(!is_valid_time("12:60"));
666        assert!(!is_valid_time("invalid-time"));
667    }
668
669    #[test]
670    fn test_number_validation() {
671        assert!(is_valid_number("123.45"));
672        assert!(is_valid_number("-123.45"));
673        assert!(is_valid_number("0"));
674        assert!(!is_valid_number("abc"));
675        assert!(!is_valid_number("12.34.56"));
676    }
677
678    #[test]
679    fn test_integer_validation() {
680        assert!(is_valid_integer("123"));
681        assert!(is_valid_integer("-123"));
682        assert!(is_valid_integer("0"));
683        assert!(!is_valid_integer("123.45"));
684        assert!(!is_valid_integer("abc"));
685    }
686
687    // Property-based tests
688    #[test]
689    fn test_validation_rule_property_based() {
690        proptest!(|(message in ".*")| {
691            let rule = ValidationRule {
692                rule_type: ValidationRuleType::Required,
693                message: message.clone(),
694                value: None,
695            };
696            assert_eq!(rule.message, message);
697        });
698    }
699
700    #[test]
701    fn test_validation_result_property_based() {
702        proptest!(|(is_valid in any::<bool>())| {
703            let result = ValidationResult {
704                is_valid,
705                message: None,
706            };
707            assert_eq!(result.is_valid, is_valid);
708        });
709    }
710}