1use std::collections::HashMap;
2use regex::Regex;
3
4#[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#[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#[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
61pub type CustomValidator = Box<dyn Fn(&str) -> ValidationResult + Send + Sync>;
63
64#[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#[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#[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#[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#[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#[derive(Debug, Clone, PartialEq)]
164pub enum ErrorType {
165 Validation,
166 Network,
167 Server,
168 Custom,
169}
170
171pub 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
423pub 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
429pub 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
435pub 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
441pub 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 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 if year < 1 || month < 1 || month > 12 || day < 1 || day > 31 {
460 return false;
461 }
462
463 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
474fn is_leap_year(year: i32) -> bool {
476 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
477}
478
479pub 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 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 hour < 24 && minute < 60 && second < 60
498}
499
500pub fn is_valid_number(number: &str) -> bool {
502 number.parse::<f64>().is_ok()
503}
504
505pub 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 #[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}