Skip to main content

fraiseql_core/validation/
composite.rs

1//! Composite validation rules combining multiple validators.
2//!
3//! This module provides combinators for composing validators:
4//! - `All`: All validators must pass
5//! - `Any`: At least one validator must pass
6//! - `Not`: Validator must fail (negation)
7//! - `Optional`: Validator only applies if field is present
8//!
9//! # Examples
10//!
11//! ```ignore
12//! // All validators must pass: required AND pattern
13//! ValidationRule::All(vec![
14//!     ValidationRule::Required,
15//!     ValidationRule::Pattern { pattern: "^[a-z]+$".to_string(), message: None }
16//! ])
17//!
18//! // At least one must pass: strong password OR long password
19//! ValidationRule::Any(vec![
20//!     ValidationRule::Pattern { pattern: strong_password.to_string(), message: None },
21//!     ValidationRule::Length { min: Some(20), max: None }
22//! ])
23//! ```
24
25use std::fmt;
26
27use crate::{
28    error::{FraiseQLError, Result},
29    validation::rules::ValidationRule,
30};
31
32/// Composite validation error that aggregates multiple validation errors.
33#[derive(Debug, Clone)]
34pub struct CompositeError {
35    /// The operator being applied (all, any, not, optional)
36    pub operator: CompositeOperator,
37    /// Individual validation errors
38    pub errors:   Vec<String>,
39    /// The field being validated
40    pub field:    String,
41}
42
43impl fmt::Display for CompositeError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}", self.operator)?;
46        if !self.errors.is_empty() {
47            write!(f, ": {}", self.errors.join("; "))?;
48        }
49        Ok(())
50    }
51}
52
53/// Composite validation operators.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum CompositeOperator {
56    /// All validators must pass
57    All,
58    /// At least one validator must pass
59    Any,
60    /// Validator must fail (negation)
61    Not,
62    /// Validator only applies if field is present
63    Optional,
64}
65
66impl fmt::Display for CompositeOperator {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::All => write!(f, "All validators must pass"),
70            Self::Any => write!(f, "At least one validator must pass"),
71            Self::Not => write!(f, "Validator must fail"),
72            Self::Optional => write!(f, "Optional validation"),
73        }
74    }
75}
76
77/// Validates that all rules pass (logical AND).
78///
79/// All validators in the list must pass successfully. If any validator fails,
80/// the entire validation fails with aggregated error messages.
81///
82/// # Arguments
83/// * `rules` - List of validation rules to apply (all must pass)
84/// * `field_value` - The value being validated
85/// * `field_name` - Name of the field for error reporting
86/// * `is_present` - Whether the field is present/non-null
87///
88/// # Returns
89/// - `Ok(())` if all rules pass
90/// - `Err` if any rule fails, containing all error messages
91pub fn validate_all(
92    rules: &[ValidationRule],
93    field_value: &str,
94    field_name: &str,
95    is_present: bool,
96) -> Result<()> {
97    let mut errors = Vec::new();
98
99    for rule in rules {
100        if let Err(e) = validate_single_rule(rule, field_value, field_name, is_present) {
101            errors.push(format!("{}", e));
102        }
103    }
104
105    if !errors.is_empty() {
106        return Err(FraiseQLError::Validation {
107            message: format!(
108                "All validators must pass for '{}': {}",
109                field_name,
110                errors.join("; ")
111            ),
112            path:    Some(field_name.to_string()),
113        });
114    }
115
116    Ok(())
117}
118
119/// Validates that at least one rule passes (logical OR).
120///
121/// At least one validator in the list must pass. Only if all validators fail
122/// is the entire validation considered failed.
123///
124/// # Arguments
125/// * `rules` - List of validation rules (at least one must pass)
126/// * `field_value` - The value being validated
127/// * `field_name` - Name of the field for error reporting
128/// * `is_present` - Whether the field is present/non-null
129///
130/// # Returns
131/// - `Ok(())` if at least one rule passes
132/// - `Err` if all rules fail, containing all error messages
133pub fn validate_any(
134    rules: &[ValidationRule],
135    field_value: &str,
136    field_name: &str,
137    is_present: bool,
138) -> Result<()> {
139    let mut errors = Vec::new();
140    let mut passed_count = 0;
141
142    for rule in rules {
143        match validate_single_rule(rule, field_value, field_name, is_present) {
144            Ok(()) => {
145                passed_count += 1;
146            },
147            Err(e) => {
148                errors.push(format!("{}", e));
149            },
150        }
151    }
152
153    if passed_count == 0 {
154        return Err(FraiseQLError::Validation {
155            message: format!(
156                "At least one validator must pass for '{}': {}",
157                field_name,
158                errors.join("; ")
159            ),
160            path:    Some(field_name.to_string()),
161        });
162    }
163
164    Ok(())
165}
166
167/// Validates that a rule fails (logical NOT/negation).
168///
169/// The validator is inverted - it passes if the rule would normally fail,
170/// and fails if the rule would normally pass.
171///
172/// # Arguments
173/// * `rule` - The validation rule to negate
174/// * `field_value` - The value being validated
175/// * `field_name` - Name of the field for error reporting
176/// * `is_present` - Whether the field is present/non-null
177///
178/// # Returns
179/// - `Ok(())` if the rule fails (as expected)
180/// - `Err` if the rule passes (when it should fail)
181pub fn validate_not(
182    rule: &ValidationRule,
183    field_value: &str,
184    field_name: &str,
185    is_present: bool,
186) -> Result<()> {
187    match validate_single_rule(rule, field_value, field_name, is_present) {
188        Ok(()) => Err(FraiseQLError::Validation {
189            message: format!("Validator for '{}' must fail but passed", field_name),
190            path:    Some(field_name.to_string()),
191        }),
192        Err(_) => Ok(()), // Validator failed as expected
193    }
194}
195
196/// Validates a rule only if the field is present.
197///
198/// If the field is absent/null, validation is skipped (passes).
199/// If the field is present, the rule is applied normally.
200///
201/// # Arguments
202/// * `rule` - The validation rule to conditionally apply
203/// * `field_value` - The value being validated
204/// * `field_name` - Name of the field for error reporting
205/// * `is_present` - Whether the field is present/non-null
206///
207/// # Returns
208/// - `Ok(())` if field is absent or if rule passes
209/// - `Err` if field is present and rule fails
210pub fn validate_optional(
211    rule: &ValidationRule,
212    field_value: &str,
213    field_name: &str,
214    is_present: bool,
215) -> Result<()> {
216    if !is_present {
217        return Ok(());
218    }
219
220    validate_single_rule(rule, field_value, field_name, is_present)
221}
222
223/// Validates a single rule against a field value.
224///
225/// This is a helper function that applies a basic validation rule.
226/// For complex rules, this would dispatch to specialized validators.
227fn validate_single_rule(
228    rule: &ValidationRule,
229    field_value: &str,
230    field_name: &str,
231    _is_present: bool,
232) -> Result<()> {
233    match rule {
234        ValidationRule::Required => {
235            if field_value.is_empty() {
236                return Err(FraiseQLError::Validation {
237                    message: format!("Field '{}' is required", field_name),
238                    path:    Some(field_name.to_string()),
239                });
240            }
241            Ok(())
242        },
243        ValidationRule::Pattern { pattern, message } => {
244            if let Ok(regex) = regex::Regex::new(pattern) {
245                if !regex.is_match(field_value) {
246                    return Err(FraiseQLError::Validation {
247                        message: message.clone().unwrap_or_else(|| {
248                            format!("'{}' must match pattern: {}", field_name, pattern)
249                        }),
250                        path:    Some(field_name.to_string()),
251                    });
252                }
253            }
254            Ok(())
255        },
256        ValidationRule::Length { min, max } => {
257            let len = field_value.len();
258            if let Some(min_len) = min {
259                if len < *min_len {
260                    return Err(FraiseQLError::Validation {
261                        message: format!(
262                            "'{}' must be at least {} characters",
263                            field_name, min_len
264                        ),
265                        path:    Some(field_name.to_string()),
266                    });
267                }
268            }
269            if let Some(max_len) = max {
270                if len > *max_len {
271                    return Err(FraiseQLError::Validation {
272                        message: format!("'{}' must be at most {} characters", field_name, max_len),
273                        path:    Some(field_name.to_string()),
274                    });
275                }
276            }
277            Ok(())
278        },
279        ValidationRule::Enum { values } => {
280            if !values.contains(&field_value.to_string()) {
281                return Err(FraiseQLError::Validation {
282                    message: format!("'{}' must be one of: {}", field_name, values.join(", ")),
283                    path:    Some(field_name.to_string()),
284                });
285            }
286            Ok(())
287        },
288        // For other rule types, we skip validation in this basic implementation
289        _ => Ok(()),
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::validation::rules::ValidationRule;
297
298    #[test]
299    fn test_validate_all_passes() {
300        let rules = vec![
301            ValidationRule::Required,
302            ValidationRule::Length {
303                min: Some(5),
304                max: None,
305            },
306        ];
307        let result = validate_all(&rules, "hello123", "password", true);
308        assert!(result.is_ok());
309    }
310
311    #[test]
312    fn test_validate_all_fails_first() {
313        let rules = vec![
314            ValidationRule::Required,
315            ValidationRule::Length {
316                min: Some(10),
317                max: None,
318            },
319        ];
320        let result = validate_all(&rules, "short", "password", true);
321        assert!(result.is_err());
322        if let Err(FraiseQLError::Validation { message, .. }) = result {
323            assert!(message.contains("All validators must pass"));
324        }
325    }
326
327    #[test]
328    fn test_validate_all_fails_second() {
329        let rules = vec![
330            ValidationRule::Required,
331            ValidationRule::Pattern {
332                pattern: "^[a-z]+$".to_string(),
333                message: None,
334            },
335        ];
336        let result = validate_all(&rules, "Hello123", "username", true);
337        assert!(result.is_err());
338    }
339
340    #[test]
341    fn test_validate_all_multiple_failures() {
342        let rules = vec![
343            ValidationRule::Required,
344            ValidationRule::Length {
345                min: Some(10),
346                max: None,
347            },
348            ValidationRule::Pattern {
349                pattern: "^[a-z]+$".to_string(),
350                message: None,
351            },
352        ];
353        let result = validate_all(&rules, "Hi", "field", true);
354        assert!(result.is_err());
355        if let Err(FraiseQLError::Validation { message, .. }) = result {
356            assert!(message.contains("All validators must pass"));
357        }
358    }
359
360    #[test]
361    fn test_validate_any_passes_first() {
362        let rules = vec![
363            ValidationRule::Pattern {
364                pattern: "^[a-z]+$".to_string(),
365                message: None,
366            },
367            ValidationRule::Length {
368                min: Some(20),
369                max: None,
370            },
371        ];
372        let result = validate_any(&rules, "abc", "field", true);
373        assert!(result.is_ok());
374    }
375
376    #[test]
377    fn test_validate_any_passes_second() {
378        let rules = vec![
379            ValidationRule::Pattern {
380                pattern: "^[a-z]+$".to_string(),
381                message: None,
382            },
383            ValidationRule::Length {
384                min: Some(2),
385                max: None,
386            },
387        ];
388        let result = validate_any(&rules, "Hi", "field", true);
389        assert!(result.is_ok());
390    }
391
392    #[test]
393    fn test_validate_any_fails_all() {
394        let rules = vec![
395            ValidationRule::Pattern {
396                pattern: "^[a-z]+$".to_string(),
397                message: None,
398            },
399            ValidationRule::Length {
400                min: Some(20),
401                max: None,
402            },
403        ];
404        let result = validate_any(&rules, "Hi", "field", true);
405        assert!(result.is_err());
406        if let Err(FraiseQLError::Validation { message, .. }) = result {
407            assert!(message.contains("At least one validator must pass"));
408        }
409    }
410
411    #[test]
412    fn test_validate_any_multiple_passes() {
413        let rules = vec![
414            ValidationRule::Pattern {
415                pattern: "^[a-z]+$".to_string(),
416                message: None,
417            },
418            ValidationRule::Length {
419                min: Some(2),
420                max: None,
421            },
422            ValidationRule::Enum {
423                values: vec!["hello".to_string(), "world".to_string()],
424            },
425        ];
426        let result = validate_any(&rules, "hello", "field", true);
427        assert!(result.is_ok());
428    }
429
430    #[test]
431    fn test_validate_not_passes_when_rule_fails() {
432        let rule = ValidationRule::Pattern {
433            pattern: "^[0-9]+$".to_string(),
434            message: None,
435        };
436        let result = validate_not(&rule, "abc", "field", true);
437        assert!(result.is_ok());
438    }
439
440    #[test]
441    fn test_validate_not_fails_when_rule_passes() {
442        let rule = ValidationRule::Pattern {
443            pattern: "^[a-z]+$".to_string(),
444            message: None,
445        };
446        let result = validate_not(&rule, "abc", "field", true);
447        assert!(result.is_err());
448    }
449
450    #[test]
451    fn test_validate_optional_skips_absent() {
452        let rule = ValidationRule::Length {
453            min: Some(100),
454            max: None,
455        };
456        let result = validate_optional(&rule, "", "field", false);
457        assert!(result.is_ok());
458    }
459
460    #[test]
461    fn test_validate_optional_applies_present() {
462        let rule = ValidationRule::Length {
463            min: Some(5),
464            max: None,
465        };
466        let result = validate_optional(&rule, "hello", "field", true);
467        assert!(result.is_ok());
468    }
469
470    #[test]
471    fn test_validate_optional_fails_present() {
472        let rule = ValidationRule::Length {
473            min: Some(10),
474            max: None,
475        };
476        let result = validate_optional(&rule, "hi", "field", true);
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn test_composite_operator_display() {
482        assert_eq!(CompositeOperator::All.to_string(), "All validators must pass");
483        assert_eq!(CompositeOperator::Any.to_string(), "At least one validator must pass");
484        assert_eq!(CompositeOperator::Not.to_string(), "Validator must fail");
485        assert_eq!(CompositeOperator::Optional.to_string(), "Optional validation");
486    }
487
488    #[test]
489    fn test_nested_all_and_pattern() {
490        let rules = vec![
491            ValidationRule::Required,
492            ValidationRule::Length {
493                min: Some(8),
494                max: Some(20),
495            },
496            ValidationRule::Pattern {
497                pattern: "^[A-Za-z0-9]+$".to_string(),
498                message: Some("Username must be alphanumeric".to_string()),
499            },
500        ];
501        let result = validate_all(&rules, "User1234", "username", true);
502        assert!(result.is_ok());
503    }
504
505    #[test]
506    fn test_nested_all_fails_on_length() {
507        let rules = vec![
508            ValidationRule::Required,
509            ValidationRule::Length {
510                min: Some(8),
511                max: Some(20),
512            },
513            ValidationRule::Pattern {
514                pattern: "^[A-Za-z0-9]+$".to_string(),
515                message: Some("Username must be alphanumeric".to_string()),
516            },
517        ];
518        let result = validate_all(&rules, "Hi", "username", true);
519        assert!(result.is_err());
520    }
521
522    #[test]
523    fn test_strong_password_pattern_all() {
524        // Strong password: at least 1 uppercase, 1 lowercase, 1 digit
525        let rules = vec![
526            ValidationRule::Required,
527            ValidationRule::Length {
528                min: Some(8),
529                max: None,
530            },
531            ValidationRule::Pattern {
532                pattern: "^(?=.*[A-Z])".to_string(), // Lookahead for uppercase
533                message: Some("Must contain at least one uppercase letter".to_string()),
534            },
535        ];
536        let result = validate_all(&rules, "Password123", "password", true);
537        assert!(result.is_ok());
538    }
539
540    #[test]
541    fn test_enum_or_pattern_any() {
542        let rules = vec![
543            ValidationRule::Enum {
544                values: vec!["admin".to_string(), "user".to_string()],
545            },
546            ValidationRule::Pattern {
547                pattern: "^guest_[0-9]+$".to_string(),
548                message: None,
549            },
550        ];
551        let result = validate_any(&rules, "guest_123", "role", true);
552        assert!(result.is_ok());
553    }
554
555    #[test]
556    fn test_not_numeric_for_string_field() {
557        let rule = ValidationRule::Pattern {
558            pattern: "^[0-9]+$".to_string(),
559            message: None,
560        };
561        let result = validate_not(&rule, "abc123", "code", true);
562        // Should pass because the regex doesn't match the whole string
563        assert!(result.is_ok());
564    }
565
566    #[test]
567    fn test_composite_error_display() {
568        let error = CompositeError {
569            operator: CompositeOperator::All,
570            errors:   vec!["error1".to_string(), "error2".to_string()],
571            field:    "field".to_string(),
572        };
573        let display_str = error.to_string();
574        assert!(display_str.contains("All validators must pass"));
575        assert!(display_str.contains("error1"));
576        assert!(display_str.contains("error2"));
577    }
578
579    #[test]
580    fn test_multiple_validators_with_required() {
581        let rules = vec![ValidationRule::Required];
582        let result = validate_all(&rules, "test", "field", true);
583        assert!(result.is_ok());
584    }
585
586    #[test]
587    fn test_empty_rules_all() {
588        let rules: Vec<ValidationRule> = vec![];
589        let result = validate_all(&rules, "test", "field", true);
590        assert!(result.is_ok());
591    }
592
593    #[test]
594    fn test_empty_rules_any() {
595        let rules: Vec<ValidationRule> = vec![];
596        let result = validate_any(&rules, "test", "field", true);
597        // Any with no rules vacuously fails (nothing passed)
598        assert!(result.is_err());
599    }
600
601    #[test]
602    fn test_length_min_max() {
603        let rule = ValidationRule::Length {
604            min: Some(5),
605            max: Some(10),
606        };
607        let result = validate_single_rule(&rule, "hello", "password", true);
608        assert!(result.is_ok());
609
610        let result = validate_single_rule(&rule, "hi", "password", true);
611        assert!(result.is_err());
612
613        let result = validate_single_rule(&rule, "this_is_too_long", "password", true);
614        assert!(result.is_err());
615    }
616}