1use std::fmt;
28
29use crate::{
30 error::{FraiseQLError, Result},
31 validation::rules::ValidationRule,
32};
33
34#[derive(Debug, Clone)]
36pub struct CompositeError {
37 pub operator: CompositeOperator,
39 pub errors: Vec<String>,
41 pub field: String,
43}
44
45impl fmt::Display for CompositeError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 write!(f, "{}", self.operator)?;
48 if !self.errors.is_empty() {
49 write!(f, ": {}", self.errors.join("; "))?;
50 }
51 Ok(())
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57#[non_exhaustive]
58pub enum CompositeOperator {
59 All,
61 Any,
63 Not,
65 Optional,
67}
68
69impl fmt::Display for CompositeOperator {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 Self::All => write!(f, "All validators must pass"),
73 Self::Any => write!(f, "At least one validator must pass"),
74 Self::Not => write!(f, "Validator must fail"),
75 Self::Optional => write!(f, "Optional validation"),
76 }
77 }
78}
79
80pub fn validate_all(
95 rules: &[ValidationRule],
96 field_value: &str,
97 field_name: &str,
98 is_present: bool,
99) -> Result<()> {
100 let mut errors = Vec::new();
101
102 for rule in rules {
103 if let Err(e) = validate_single_rule(rule, field_value, field_name, is_present) {
104 errors.push(format!("{}", e));
105 }
106 }
107
108 if !errors.is_empty() {
109 return Err(FraiseQLError::Validation {
110 message: format!(
111 "All validators must pass for '{}': {}",
112 field_name,
113 errors.join("; ")
114 ),
115 path: Some(field_name.to_string()),
116 });
117 }
118
119 Ok(())
120}
121
122pub fn validate_any(
137 rules: &[ValidationRule],
138 field_value: &str,
139 field_name: &str,
140 is_present: bool,
141) -> Result<()> {
142 let mut errors = Vec::new();
143 let mut passed_count = 0;
144
145 for rule in rules {
146 match validate_single_rule(rule, field_value, field_name, is_present) {
147 Ok(()) => {
148 passed_count += 1;
149 },
150 Err(e) => {
151 errors.push(format!("{}", e));
152 },
153 }
154 }
155
156 if passed_count == 0 {
157 return Err(FraiseQLError::Validation {
158 message: format!(
159 "At least one validator must pass for '{}': {}",
160 field_name,
161 errors.join("; ")
162 ),
163 path: Some(field_name.to_string()),
164 });
165 }
166
167 Ok(())
168}
169
170pub fn validate_not(
185 rule: &ValidationRule,
186 field_value: &str,
187 field_name: &str,
188 is_present: bool,
189) -> Result<()> {
190 match validate_single_rule(rule, field_value, field_name, is_present) {
191 Ok(()) => Err(FraiseQLError::Validation {
192 message: format!("Validator for '{}' must fail but passed", field_name),
193 path: Some(field_name.to_string()),
194 }),
195 Err(_) => Ok(()), }
197}
198
199pub fn validate_optional(
214 rule: &ValidationRule,
215 field_value: &str,
216 field_name: &str,
217 is_present: bool,
218) -> Result<()> {
219 if !is_present {
220 return Ok(());
221 }
222
223 validate_single_rule(rule, field_value, field_name, is_present)
224}
225
226fn validate_single_rule(
231 rule: &ValidationRule,
232 field_value: &str,
233 field_name: &str,
234 _is_present: bool,
235) -> Result<()> {
236 match rule {
237 ValidationRule::Required => {
238 if field_value.is_empty() {
239 return Err(FraiseQLError::Validation {
240 message: format!("Field '{}' is required", field_name),
241 path: Some(field_name.to_string()),
242 });
243 }
244 Ok(())
245 },
246 ValidationRule::Pattern { pattern, message } => {
247 if let Ok(regex) = regex::Regex::new(pattern) {
248 if !regex.is_match(field_value) {
249 return Err(FraiseQLError::Validation {
250 message: message.clone().unwrap_or_else(|| {
251 format!("'{}' must match pattern: {}", field_name, pattern)
252 }),
253 path: Some(field_name.to_string()),
254 });
255 }
256 }
257 Ok(())
258 },
259 ValidationRule::Length { min, max } => {
260 let len = field_value.len();
261 if let Some(min_len) = min {
262 if len < *min_len {
263 return Err(FraiseQLError::Validation {
264 message: format!(
265 "'{}' must be at least {} characters",
266 field_name, min_len
267 ),
268 path: Some(field_name.to_string()),
269 });
270 }
271 }
272 if let Some(max_len) = max {
273 if len > *max_len {
274 return Err(FraiseQLError::Validation {
275 message: format!("'{}' must be at most {} characters", field_name, max_len),
276 path: Some(field_name.to_string()),
277 });
278 }
279 }
280 Ok(())
281 },
282 ValidationRule::Enum { values } => {
283 if !values.contains(&field_value.to_string()) {
284 return Err(FraiseQLError::Validation {
285 message: format!("'{}' must be one of: {}", field_name, values.join(", ")),
286 path: Some(field_name.to_string()),
287 });
288 }
289 Ok(())
290 },
291 _ => Ok(()),
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::validation::rules::ValidationRule;
300
301 #[test]
302 fn test_validate_all_passes() {
303 let rules = vec![
304 ValidationRule::Required,
305 ValidationRule::Length {
306 min: Some(5),
307 max: None,
308 },
309 ];
310 let result = validate_all(&rules, "hello123", "password", true);
311 result.unwrap_or_else(|e| panic!("expected all validators to pass: {e}"));
312 }
313
314 #[test]
315 fn test_validate_all_fails_first() {
316 let rules = vec![
317 ValidationRule::Required,
318 ValidationRule::Length {
319 min: Some(10),
320 max: None,
321 },
322 ];
323 let result = validate_all(&rules, "short", "password", true);
324 assert!(
325 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("All validators must pass")),
326 "expected Validation error with 'All validators must pass', got: {result:?}"
327 );
328 }
329
330 #[test]
331 fn test_validate_all_fails_second() {
332 let rules = vec![
333 ValidationRule::Required,
334 ValidationRule::Pattern {
335 pattern: "^[a-z]+$".to_string(),
336 message: None,
337 },
338 ];
339 let result = validate_all(&rules, "Hello123", "username", true);
340 assert!(
341 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("All validators must pass")),
342 "expected Validation error with 'All validators must pass', got: {result:?}"
343 );
344 }
345
346 #[test]
347 fn test_validate_all_multiple_failures() {
348 let rules = vec![
349 ValidationRule::Required,
350 ValidationRule::Length {
351 min: Some(10),
352 max: None,
353 },
354 ValidationRule::Pattern {
355 pattern: "^[a-z]+$".to_string(),
356 message: None,
357 },
358 ];
359 let result = validate_all(&rules, "Hi", "field", true);
360 assert!(
361 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("All validators must pass")),
362 "expected Validation error with 'All validators must pass', got: {result:?}"
363 );
364 }
365
366 #[test]
367 fn test_validate_any_passes_first() {
368 let rules = vec![
369 ValidationRule::Pattern {
370 pattern: "^[a-z]+$".to_string(),
371 message: None,
372 },
373 ValidationRule::Length {
374 min: Some(20),
375 max: None,
376 },
377 ];
378 let result = validate_any(&rules, "abc", "field", true);
379 result.unwrap_or_else(|e| panic!("expected any validator to pass: {e}"));
380 }
381
382 #[test]
383 fn test_validate_any_passes_second() {
384 let rules = vec![
385 ValidationRule::Pattern {
386 pattern: "^[a-z]+$".to_string(),
387 message: None,
388 },
389 ValidationRule::Length {
390 min: Some(2),
391 max: None,
392 },
393 ];
394 let result = validate_any(&rules, "Hi", "field", true);
395 result.unwrap_or_else(|e| panic!("expected any validator to pass: {e}"));
396 }
397
398 #[test]
399 fn test_validate_any_fails_all() {
400 let rules = vec![
401 ValidationRule::Pattern {
402 pattern: "^[a-z]+$".to_string(),
403 message: None,
404 },
405 ValidationRule::Length {
406 min: Some(20),
407 max: None,
408 },
409 ];
410 let result = validate_any(&rules, "Hi", "field", true);
411 assert!(
412 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("At least one validator must pass")),
413 "expected Validation error with 'At least one validator must pass', got: {result:?}"
414 );
415 }
416
417 #[test]
418 fn test_validate_any_multiple_passes() {
419 let rules = vec![
420 ValidationRule::Pattern {
421 pattern: "^[a-z]+$".to_string(),
422 message: None,
423 },
424 ValidationRule::Length {
425 min: Some(2),
426 max: None,
427 },
428 ValidationRule::Enum {
429 values: vec!["hello".to_string(), "world".to_string()],
430 },
431 ];
432 let result = validate_any(&rules, "hello", "field", true);
433 result.unwrap_or_else(|e| panic!("expected any validator to pass: {e}"));
434 }
435
436 #[test]
437 fn test_validate_not_passes_when_rule_fails() {
438 let rule = ValidationRule::Pattern {
439 pattern: "^[0-9]+$".to_string(),
440 message: None,
441 };
442 let result = validate_not(&rule, "abc", "field", true);
443 result.unwrap_or_else(|e| panic!("expected not-validator to pass when rule fails: {e}"));
444 }
445
446 #[test]
447 fn test_validate_not_fails_when_rule_passes() {
448 let rule = ValidationRule::Pattern {
449 pattern: "^[a-z]+$".to_string(),
450 message: None,
451 };
452 let result = validate_not(&rule, "abc", "field", true);
453 assert!(
454 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("must fail but passed")),
455 "expected Validation error with 'must fail but passed', got: {result:?}"
456 );
457 }
458
459 #[test]
460 fn test_validate_optional_skips_absent() {
461 let rule = ValidationRule::Length {
462 min: Some(100),
463 max: None,
464 };
465 let result = validate_optional(&rule, "", "field", false);
466 result.unwrap_or_else(|e| panic!("expected optional to skip absent field: {e}"));
467 }
468
469 #[test]
470 fn test_validate_optional_applies_present() {
471 let rule = ValidationRule::Length {
472 min: Some(5),
473 max: None,
474 };
475 let result = validate_optional(&rule, "hello", "field", true);
476 result.unwrap_or_else(|e| panic!("expected optional to pass for valid present field: {e}"));
477 }
478
479 #[test]
480 fn test_validate_optional_fails_present() {
481 let rule = ValidationRule::Length {
482 min: Some(10),
483 max: None,
484 };
485 let result = validate_optional(&rule, "hi", "field", true);
486 assert!(
487 matches!(result, Err(FraiseQLError::Validation { .. })),
488 "expected Validation error for present field failing rule, got: {result:?}"
489 );
490 }
491
492 #[test]
493 fn test_composite_operator_display() {
494 assert_eq!(CompositeOperator::All.to_string(), "All validators must pass");
495 assert_eq!(CompositeOperator::Any.to_string(), "At least one validator must pass");
496 assert_eq!(CompositeOperator::Not.to_string(), "Validator must fail");
497 assert_eq!(CompositeOperator::Optional.to_string(), "Optional validation");
498 }
499
500 #[test]
501 fn test_nested_all_and_pattern() {
502 let rules = vec![
503 ValidationRule::Required,
504 ValidationRule::Length {
505 min: Some(8),
506 max: Some(20),
507 },
508 ValidationRule::Pattern {
509 pattern: "^[A-Za-z0-9]+$".to_string(),
510 message: Some("Username must be alphanumeric".to_string()),
511 },
512 ];
513 let result = validate_all(&rules, "User1234", "username", true);
514 result.unwrap_or_else(|e| panic!("expected nested all+pattern to pass: {e}"));
515 }
516
517 #[test]
518 fn test_nested_all_fails_on_length() {
519 let rules = vec![
520 ValidationRule::Required,
521 ValidationRule::Length {
522 min: Some(8),
523 max: Some(20),
524 },
525 ValidationRule::Pattern {
526 pattern: "^[A-Za-z0-9]+$".to_string(),
527 message: Some("Username must be alphanumeric".to_string()),
528 },
529 ];
530 let result = validate_all(&rules, "Hi", "username", true);
531 assert!(
532 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("All validators must pass")),
533 "expected Validation error for length failure, got: {result:?}"
534 );
535 }
536
537 #[test]
538 fn test_strong_password_pattern_all() {
539 let rules = vec![
541 ValidationRule::Required,
542 ValidationRule::Length {
543 min: Some(8),
544 max: None,
545 },
546 ValidationRule::Pattern {
547 pattern: "^(?=.*[A-Z])".to_string(), message: Some("Must contain at least one uppercase letter".to_string()),
549 },
550 ];
551 let result = validate_all(&rules, "Password123", "password", true);
552 result.unwrap_or_else(|e| panic!("expected strong password to pass: {e}"));
553 }
554
555 #[test]
556 fn test_enum_or_pattern_any() {
557 let rules = vec![
558 ValidationRule::Enum {
559 values: vec!["admin".to_string(), "user".to_string()],
560 },
561 ValidationRule::Pattern {
562 pattern: "^guest_[0-9]+$".to_string(),
563 message: None,
564 },
565 ];
566 let result = validate_any(&rules, "guest_123", "role", true);
567 result.unwrap_or_else(|e| panic!("expected enum-or-pattern any to pass: {e}"));
568 }
569
570 #[test]
571 fn test_not_numeric_for_string_field() {
572 let rule = ValidationRule::Pattern {
573 pattern: "^[0-9]+$".to_string(),
574 message: None,
575 };
576 let result = validate_not(&rule, "abc123", "code", true);
577 result.unwrap_or_else(|e| panic!("expected not-numeric to pass for mixed string: {e}"));
579 }
580
581 #[test]
582 fn test_composite_error_display() {
583 let error = CompositeError {
584 operator: CompositeOperator::All,
585 errors: vec!["error1".to_string(), "error2".to_string()],
586 field: "field".to_string(),
587 };
588 let display_str = error.to_string();
589 assert!(display_str.contains("All validators must pass"));
590 assert!(display_str.contains("error1"));
591 assert!(display_str.contains("error2"));
592 }
593
594 #[test]
595 fn test_multiple_validators_with_required() {
596 let rules = vec![ValidationRule::Required];
597 let result = validate_all(&rules, "test", "field", true);
598 result.unwrap_or_else(|e| panic!("expected required validator to pass: {e}"));
599 }
600
601 #[test]
602 fn test_empty_rules_all() {
603 let rules: Vec<ValidationRule> = vec![];
604 let result = validate_all(&rules, "test", "field", true);
605 result.unwrap_or_else(|e| panic!("expected empty all-rules to pass vacuously: {e}"));
606 }
607
608 #[test]
609 fn test_empty_rules_any() {
610 let rules: Vec<ValidationRule> = vec![];
611 let result = validate_any(&rules, "test", "field", true);
612 assert!(
614 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("At least one validator must pass")),
615 "expected Validation error for empty any-rules, got: {result:?}"
616 );
617 }
618
619 #[test]
620 fn test_length_min_max() {
621 let rule = ValidationRule::Length {
622 min: Some(5),
623 max: Some(10),
624 };
625 let result = validate_single_rule(&rule, "hello", "password", true);
626 result.unwrap_or_else(|e| panic!("expected length check to pass for 'hello': {e}"));
627
628 let result = validate_single_rule(&rule, "hi", "password", true);
629 assert!(
630 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("at least")),
631 "expected Validation error for too-short value, got: {result:?}"
632 );
633
634 let result = validate_single_rule(&rule, "this_is_too_long", "password", true);
635 assert!(
636 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("at most")),
637 "expected Validation error for too-long value, got: {result:?}"
638 );
639 }
640}