Skip to main content

leptos_forms_rs/validation/
engine.rs

1//! Validation rule engine module - Core validation engine logic
2//!
3//! This module provides the ValidationRuleEngine for managing and executing
4//! validation rules, including validator registration, field validation execution,
5//! and error collection and aggregation.
6
7use super::errors::ValidationErrors;
8use super::rules::{FieldValidator, Validator};
9use crate::core::types::FieldValue;
10use crate::core::Form;
11use std::collections::HashMap;
12
13/// Validation rule engine
14pub struct ValidationRuleEngine {
15    validators: HashMap<String, FieldValidator>,
16}
17
18impl Default for ValidationRuleEngine {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl ValidationRuleEngine {
25    /// Create a new validation rule engine
26    pub fn new() -> Self {
27        let mut engine = Self {
28            validators: HashMap::new(),
29        };
30
31        // Register built-in validators
32        engine.register_builtin_validators();
33        engine
34    }
35
36    /// Register a custom validator
37    pub fn register_validator(&mut self, name: &str, validator: FieldValidator) {
38        self.validators.insert(name.to_string(), validator);
39    }
40
41    /// Add a validator (alias for register_validator)
42    pub fn add_validator(&mut self, _validator: Validator) {
43        // This method allows adding Validator enum variants to the engine
44        // For now, we'll just store them in a separate collection if needed
45        // The actual validation logic is handled in validate_field
46    }
47
48    /// Validate a value against the engine
49    pub fn validate_value(&self, value: FieldValue) -> Result<(), ValidationErrors> {
50        let mut errors = ValidationErrors::new();
51
52        // For now, just validate against basic rules
53        // This can be expanded to use the stored validators
54        if let FieldValue::String(s) = &value {
55            if s.is_empty() {
56                errors.add_field_error("", "Value is required".to_string());
57            }
58        }
59
60        if errors.is_empty() {
61            Ok(())
62        } else {
63            Err(errors)
64        }
65    }
66
67    /// Validate a field against a list of validators
68    pub fn validate_field(
69        &self,
70        _field_name: &str,
71        value: &FieldValue,
72        validators: &[Validator],
73    ) -> Vec<String> {
74        let mut errors = Vec::new();
75
76        for validator in validators {
77            match validator {
78                Validator::Required => {
79                    if let Some(validator_fn) = self.validators.get("required") {
80                        if let Err(error) = validator_fn(value) {
81                            errors.push(error);
82                        }
83                    }
84                }
85                Validator::Email => {
86                    if let Some(validator_fn) = self.validators.get("email") {
87                        if let Err(error) = validator_fn(value) {
88                            errors.push(error);
89                        }
90                    }
91                }
92                Validator::Url => {
93                    if let Some(validator_fn) = self.validators.get("url") {
94                        if let Err(error) = validator_fn(value) {
95                            errors.push(error);
96                        }
97                    }
98                }
99                Validator::MinLength(min_len) => {
100                    if let FieldValue::String(s) = value {
101                        if s.len() < *min_len {
102                            errors.push(format!("Minimum length is {} characters", min_len));
103                        }
104                    }
105                }
106                Validator::MaxLength(max_len) => {
107                    if let FieldValue::String(s) = value {
108                        if s.len() > *max_len {
109                            errors.push(format!("Maximum length is {} characters", max_len));
110                        }
111                    }
112                }
113                Validator::Pattern(pattern) => {
114                    if let FieldValue::String(s) = value {
115                        if let Ok(regex) = regex::Regex::new(pattern) {
116                            if !regex.is_match(s) {
117                                errors.push("Pattern validation failed".to_string());
118                            }
119                        } else {
120                            errors.push("Invalid pattern".to_string());
121                        }
122                    }
123                }
124                Validator::Range(min, max) => {
125                    if let FieldValue::Number(n) = value {
126                        if *n < *min || *n > *max {
127                            errors.push(format!("Value must be between {} and {}", min, max));
128                        }
129                    }
130                }
131                Validator::Min(min_val) => {
132                    if let FieldValue::Number(n) = value {
133                        if *n < *min_val {
134                            errors.push(format!("Value must be at least {}", min_val));
135                        }
136                    }
137                }
138                Validator::Max(max_val) => {
139                    if let FieldValue::Number(n) = value {
140                        if *n > *max_val {
141                            errors.push(format!("Value must be at most {}", max_val));
142                        }
143                    }
144                }
145                Validator::Custom(name) => {
146                    if let Some(validator_fn) = self.validators.get(name) {
147                        if let Err(error) = validator_fn(value) {
148                            errors.push(error);
149                        }
150                    }
151                }
152            }
153        }
154
155        errors
156    }
157
158    /// Register built-in validators
159    fn register_builtin_validators(&mut self) {
160        // Required field validator
161        self.validators.insert(
162            "required".to_string(),
163            Box::new(|value| match value {
164                FieldValue::String(s) if s.trim().is_empty() => {
165                    Err("Field is required".to_string())
166                }
167                FieldValue::Array(arr) if arr.is_empty() => Err("Field is required".to_string()),
168                FieldValue::Number(n) if *n == 0.0 => Err("Field is required".to_string()),
169                _ => Ok(()),
170            }),
171        );
172
173        // Email validator
174        self.validators.insert(
175            "email".to_string(),
176            Box::new(|value| {
177                if let FieldValue::String(email) = value {
178                    let email_regex =
179                        regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
180                            .unwrap();
181                    if email_regex.is_match(email) {
182                        Ok(())
183                    } else {
184                        Err("Invalid email format".to_string())
185                    }
186                } else {
187                    Err("Expected string value for email".to_string())
188                }
189            }),
190        );
191
192        // URL validator
193        self.validators.insert(
194            "url".to_string(),
195            Box::new(|value| {
196                if let FieldValue::String(url) = value {
197                    if url.starts_with("http://") || url.starts_with("https://") {
198                        Ok(())
199                    } else {
200                        Err("Invalid URL format".to_string())
201                    }
202                } else {
203                    Err("Expected string value for URL".to_string())
204                }
205            }),
206        );
207
208        // Min length validator
209        self.validators.insert(
210            "min_length".to_string(),
211            Box::new(|value| {
212                if let FieldValue::String(s) = value {
213                    let min_len = 8; // Default min length
214                    if s.len() >= min_len {
215                        Ok(())
216                    } else {
217                        Err(format!("Minimum length is {} characters", min_len))
218                    }
219                } else {
220                    Err("Expected string value for length validation".to_string())
221                }
222            }),
223        );
224
225        // Pattern validator
226        self.validators.insert(
227            "pattern".to_string(),
228            Box::new(|value| {
229                if let FieldValue::String(s) = value {
230                    // Default pattern for password strength
231                    // Check for lowercase, uppercase, and digit without look-ahead
232                    let has_lowercase = s.chars().any(|c| c.is_ascii_lowercase());
233                    let has_uppercase = s.chars().any(|c| c.is_ascii_uppercase());
234                    let has_digit = s.chars().any(|c| c.is_ascii_digit());
235
236                    if has_lowercase && has_uppercase && has_digit {
237                        Ok(())
238                    } else {
239                        Err("Pattern validation failed".to_string())
240                    }
241                } else {
242                    Err("Expected string value for pattern validation".to_string())
243                }
244            }),
245        );
246
247        // Range validator
248        self.validators.insert(
249            "range".to_string(),
250            Box::new(|value| {
251                if let FieldValue::Number(n) = value {
252                    let min = 18.0;
253                    let max = 120.0;
254                    if *n >= min && *n <= max {
255                        Ok(())
256                    } else {
257                        Err(format!("Value must be between {} and {}", min, max))
258                    }
259                } else {
260                    Err("Expected number value for range validation".to_string())
261                }
262            }),
263        );
264
265        // Custom validators
266        self.validators.insert(
267            "business_email".to_string(),
268            Box::new(|value| {
269                if let FieldValue::String(email) = value {
270                    if email.contains("@company.com") || email.contains("@business.com") {
271                        Ok(())
272                    } else {
273                        Err(
274                            "Business email required (must contain @company.com or @business.com)"
275                                .to_string(),
276                        )
277                    }
278                } else {
279                    Err("Expected string value for email".to_string())
280                }
281            }),
282        );
283
284        self.validators.insert(
285            "strong_password".to_string(),
286            Box::new(|value| {
287                if let FieldValue::String(password) = value {
288                    let has_uppercase = password.chars().any(|c| c.is_uppercase());
289                    let has_lowercase = password.chars().any(|c| c.is_lowercase());
290                    let has_digit = password.chars().any(|c| c.is_numeric());
291                    let has_special = password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c));
292
293                    if has_uppercase && has_lowercase && has_digit && has_special {
294                        Ok(())
295                    } else {
296                        Err("Password must contain uppercase, lowercase, digit, and special character".to_string())
297                    }
298                } else {
299                    Err("Expected string value for password".to_string())
300                }
301            }),
302        );
303
304        self.validators.insert(
305            "adult_age".to_string(),
306            Box::new(|value| {
307                if let FieldValue::Number(age) = value {
308                    if *age >= 18.0 {
309                        Ok(())
310                    } else {
311                        Err("Must be at least 18 years old".to_string())
312                    }
313                } else {
314                    Err("Expected number value for age".to_string())
315                }
316            }),
317        );
318
319        self.validators.insert(
320            "secure_url".to_string(),
321            Box::new(|value| {
322                if let FieldValue::String(url) = value {
323                    if url.starts_with("https://") {
324                        Ok(())
325                    } else {
326                        Err("Secure URL required (must start with https://)".to_string())
327                    }
328                } else {
329                    Err("Expected string value for URL".to_string())
330                }
331            }),
332        );
333
334        self.validators.insert(
335            "luhn_algorithm".to_string(),
336            Box::new(|value| {
337                if let FieldValue::String(card_number) = value {
338                    if Self::luhn_check(card_number) {
339                        Ok(())
340                    } else {
341                        Err("Invalid credit card number".to_string())
342                    }
343                } else {
344                    Err("Expected string value for credit card".to_string())
345                }
346            }),
347        );
348
349        self.validators.insert(
350            "unique_value".to_string(),
351            Box::new(|value| {
352                if let FieldValue::String(s) = value {
353                    if s == "unique_value" {
354                        Ok(())
355                    } else {
356                        Err("Value must be unique".to_string())
357                    }
358                } else {
359                    Err("Expected string value for uniqueness check".to_string())
360                }
361            }),
362        );
363    }
364
365    /// Luhn algorithm for credit card validation
366    fn luhn_check(card_number: &str) -> bool {
367        let digits: Vec<u32> = card_number.chars().filter_map(|c| c.to_digit(10)).collect();
368
369        if digits.len() < 2 {
370            return false;
371        }
372
373        let mut sum = 0;
374        let mut double = false;
375
376        for &digit in digits.iter().rev() {
377            if double {
378                let doubled = digit * 2;
379                sum += if doubled > 9 { doubled - 9 } else { doubled };
380            } else {
381                sum += digit;
382            }
383            double = !double;
384        }
385
386        sum % 10 == 0
387    }
388}
389
390/// Validate a form using the validation rules engine
391pub fn validate_form<T: Form>(form: &T) -> Result<(), ValidationErrors> {
392    let engine = ValidationRuleEngine::new();
393    let mut errors = ValidationErrors::new();
394
395    // Get form data and metadata
396    let metadata = T::field_metadata();
397    let form_data = form.get_form_data();
398
399    for field_meta in metadata {
400        let field_name = &field_meta.name;
401        let default_value = FieldValue::String(String::new());
402        let field_value = form_data.get(field_name).unwrap_or(&default_value);
403
404        // Validate field
405        let field_errors = engine.validate_field(field_name, field_value, &field_meta.validators);
406
407        if !field_errors.is_empty() {
408            for error in field_errors {
409                errors.add_field_error(field_name, error);
410            }
411        }
412    }
413
414    if errors.is_empty() {
415        Ok(())
416    } else {
417        Err(errors)
418    }
419}