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//! ```
12//! use fraiseql_core::validation::{CompiledPattern, ValidationRule};
13//!
14//! // All validators must pass: required AND pattern
15//! let _rule = ValidationRule::All(vec![
16//!     ValidationRule::Required,
17//!     ValidationRule::Pattern {
18//!         pattern: CompiledPattern::new("^[a-z]+$").expect("valid regex"),
19//!         message: None,
20//!     },
21//! ]);
22//!
23//! // At least one must pass: alphanumeric short word OR long password
24//! // (Note: the `regex` crate does not support look-ahead, so we use a plain
25//! // anchored pattern instead of the `(?=.*[A-Z])(?=.*[0-9])` form.)
26//! let _rule = ValidationRule::Any(vec![
27//!     ValidationRule::Pattern {
28//!         pattern: CompiledPattern::new(r"^[A-Za-z0-9]+$").expect("valid regex"),
29//!         message: None,
30//!     },
31//!     ValidationRule::Length { min: Some(20), max: None },
32//! ]);
33//! ```
34
35use std::fmt;
36
37use crate::{
38    error::{FraiseQLError, Result},
39    validation::rules::ValidationRule,
40};
41
42/// Composite validation error that aggregates multiple validation errors.
43#[derive(Debug, Clone)]
44pub struct CompositeError {
45    /// The operator being applied (all, any, not, optional)
46    pub operator: CompositeOperator,
47    /// Individual validation errors
48    pub errors:   Vec<String>,
49    /// The field being validated
50    pub field:    String,
51}
52
53impl fmt::Display for CompositeError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{}", self.operator)?;
56        if !self.errors.is_empty() {
57            write!(f, ": {}", self.errors.join("; "))?;
58        }
59        Ok(())
60    }
61}
62
63/// Composite validation operators.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum CompositeOperator {
67    /// All validators must pass
68    All,
69    /// At least one validator must pass
70    Any,
71    /// Validator must fail (negation)
72    Not,
73    /// Validator only applies if field is present
74    Optional,
75}
76
77impl fmt::Display for CompositeOperator {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            Self::All => write!(f, "All validators must pass"),
81            Self::Any => write!(f, "At least one validator must pass"),
82            Self::Not => write!(f, "Validator must fail"),
83            Self::Optional => write!(f, "Optional validation"),
84        }
85    }
86}
87
88/// Validates that all rules pass (logical AND).
89///
90/// All validators in the list must pass successfully. If any validator fails,
91/// the entire validation fails with aggregated error messages.
92///
93/// # Arguments
94/// * `rules` - List of validation rules to apply (all must pass)
95/// * `field_value` - The value being validated
96/// * `field_name` - Name of the field for error reporting
97/// * `is_present` - Whether the field is present/non-null
98///
99/// # Errors
100///
101/// Returns `FraiseQLError::Validation` if any rule fails, with all failure messages combined.
102pub fn validate_all(
103    rules: &[ValidationRule],
104    field_value: &str,
105    field_name: &str,
106    is_present: bool,
107) -> Result<()> {
108    let mut errors = Vec::new();
109
110    for rule in rules {
111        if let Err(e) = validate_single_rule(rule, field_value, field_name, is_present) {
112            errors.push(format!("{}", e));
113        }
114    }
115
116    if !errors.is_empty() {
117        return Err(FraiseQLError::Validation {
118            message: format!(
119                "All validators must pass for '{}': {}",
120                field_name,
121                errors.join("; ")
122            ),
123            path:    Some(field_name.to_string()),
124        });
125    }
126
127    Ok(())
128}
129
130/// Validates that at least one rule passes (logical OR).
131///
132/// At least one validator in the list must pass. Only if all validators fail
133/// is the entire validation considered failed.
134///
135/// # Arguments
136/// * `rules` - List of validation rules (at least one must pass)
137/// * `field_value` - The value being validated
138/// * `field_name` - Name of the field for error reporting
139/// * `is_present` - Whether the field is present/non-null
140///
141/// # Errors
142///
143/// Returns `FraiseQLError::Validation` if all rules fail, with all failure messages combined.
144pub fn validate_any(
145    rules: &[ValidationRule],
146    field_value: &str,
147    field_name: &str,
148    is_present: bool,
149) -> Result<()> {
150    let mut errors = Vec::new();
151    let mut passed_count = 0;
152
153    for rule in rules {
154        match validate_single_rule(rule, field_value, field_name, is_present) {
155            Ok(()) => {
156                passed_count += 1;
157            },
158            Err(e) => {
159                errors.push(format!("{}", e));
160            },
161        }
162    }
163
164    if passed_count == 0 {
165        return Err(FraiseQLError::Validation {
166            message: format!(
167                "At least one validator must pass for '{}': {}",
168                field_name,
169                errors.join("; ")
170            ),
171            path:    Some(field_name.to_string()),
172        });
173    }
174
175    Ok(())
176}
177
178/// Validates that a rule fails (logical NOT/negation).
179///
180/// The validator is inverted - it passes if the rule would normally fail,
181/// and fails if the rule would normally pass.
182///
183/// # Arguments
184/// * `rule` - The validation rule to negate
185/// * `field_value` - The value being validated
186/// * `field_name` - Name of the field for error reporting
187/// * `is_present` - Whether the field is present/non-null
188///
189/// # Errors
190///
191/// Returns `FraiseQLError::Validation` if the negated rule passes (when it should fail).
192pub fn validate_not(
193    rule: &ValidationRule,
194    field_value: &str,
195    field_name: &str,
196    is_present: bool,
197) -> Result<()> {
198    match validate_single_rule(rule, field_value, field_name, is_present) {
199        Ok(()) => Err(FraiseQLError::Validation {
200            message: format!("Validator for '{}' must fail but passed", field_name),
201            path:    Some(field_name.to_string()),
202        }),
203        Err(_) => Ok(()), // Validator failed as expected
204    }
205}
206
207/// Validates a rule only if the field is present.
208///
209/// If the field is absent/null, validation is skipped (passes).
210/// If the field is present, the rule is applied normally.
211///
212/// # Arguments
213/// * `rule` - The validation rule to conditionally apply
214/// * `field_value` - The value being validated
215/// * `field_name` - Name of the field for error reporting
216/// * `is_present` - Whether the field is present/non-null
217///
218/// # Errors
219///
220/// Returns `FraiseQLError::Validation` if the field is present and the rule fails.
221pub fn validate_optional(
222    rule: &ValidationRule,
223    field_value: &str,
224    field_name: &str,
225    is_present: bool,
226) -> Result<()> {
227    if !is_present {
228        return Ok(());
229    }
230
231    validate_single_rule(rule, field_value, field_name, is_present)
232}
233
234/// Validates a single rule against a field value.
235///
236/// This is a helper function that applies a basic validation rule.
237/// For complex rules, this would dispatch to specialized validators.
238pub(crate) fn validate_single_rule(
239    rule: &ValidationRule,
240    field_value: &str,
241    field_name: &str,
242    _is_present: bool,
243) -> Result<()> {
244    match rule {
245        ValidationRule::Required => {
246            if field_value.is_empty() {
247                return Err(FraiseQLError::Validation {
248                    message: format!("Field '{}' is required", field_name),
249                    path:    Some(field_name.to_string()),
250                });
251            }
252            Ok(())
253        },
254        ValidationRule::Pattern { pattern, message } => {
255            if !pattern.is_match(field_value) {
256                return Err(FraiseQLError::Validation {
257                    message: message.clone().unwrap_or_else(|| {
258                        format!("'{}' must match pattern: {}", field_name, pattern)
259                    }),
260                    path:    Some(field_name.to_string()),
261                });
262            }
263            Ok(())
264        },
265        ValidationRule::Length { min, max } => {
266            let len = field_value.len();
267            if let Some(min_len) = min {
268                if len < *min_len {
269                    return Err(FraiseQLError::Validation {
270                        message: format!(
271                            "'{}' must be at least {} characters",
272                            field_name, min_len
273                        ),
274                        path:    Some(field_name.to_string()),
275                    });
276                }
277            }
278            if let Some(max_len) = max {
279                if len > *max_len {
280                    return Err(FraiseQLError::Validation {
281                        message: format!("'{}' must be at most {} characters", field_name, max_len),
282                        path:    Some(field_name.to_string()),
283                    });
284                }
285            }
286            Ok(())
287        },
288        ValidationRule::Enum { values } => {
289            if !values.contains(&field_value.to_string()) {
290                return Err(FraiseQLError::Validation {
291                    message: format!("'{}' must be one of: {}", field_name, values.join(", ")),
292                    path:    Some(field_name.to_string()),
293                });
294            }
295            Ok(())
296        },
297        // For other rule types, we skip validation in this basic implementation
298        _ => Ok(()),
299    }
300}