leptos_forms_rs/validation/
engine.rs1use super::errors::ValidationErrors;
8use super::rules::{FieldValidator, Validator};
9use crate::core::types::FieldValue;
10use crate::core::Form;
11use std::collections::HashMap;
12
13pub 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 pub fn new() -> Self {
27 let mut engine = Self {
28 validators: HashMap::new(),
29 };
30
31 engine.register_builtin_validators();
33 engine
34 }
35
36 pub fn register_validator(&mut self, name: &str, validator: FieldValidator) {
38 self.validators.insert(name.to_string(), validator);
39 }
40
41 pub fn add_validator(&mut self, _validator: Validator) {
43 }
47
48 pub fn validate_value(&self, value: FieldValue) -> Result<(), ValidationErrors> {
50 let mut errors = ValidationErrors::new();
51
52 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 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 fn register_builtin_validators(&mut self) {
160 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 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 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 self.validators.insert(
210 "min_length".to_string(),
211 Box::new(|value| {
212 if let FieldValue::String(s) = value {
213 let min_len = 8; 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 self.validators.insert(
227 "pattern".to_string(),
228 Box::new(|value| {
229 if let FieldValue::String(s) = value {
230 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 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 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 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
390pub fn validate_form<T: Form>(form: &T) -> Result<(), ValidationErrors> {
392 let engine = ValidationRuleEngine::new();
393 let mut errors = ValidationErrors::new();
394
395 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 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}