Skip to main content

fraiseql_core/validation/
mutual_exclusivity.rs

1//! Mutual exclusivity and conditional requirement validators.
2//!
3//! This module provides validators for complex field-relationship rules:
4//! - `OneOf`: Exactly one field from a set must be provided
5//! - `AnyOf`: At least one field from a set must be provided
6//! - `ConditionalRequired`: If one field is present, others must be too
7//! - `RequiredIfAbsent`: If one field is missing, others must be provided
8
9use serde_json::Value;
10
11use crate::error::{FraiseQLError, Result};
12
13/// Validates that exactly one field from the specified set is provided.
14///
15/// # Example
16/// ```
17/// use fraiseql_core::validation::mutual_exclusivity::OneOfValidator;
18/// use serde_json::json;
19/// // Either entityId OR entityPayload, but not both
20/// let input = json!({ "entityId": "123", "entityPayload": null });
21/// assert!(
22///     OneOfValidator::validate(&input, &["entityId".to_string(), "entityPayload".to_string()], None).is_ok(),
23///     "one-of constraint satisfied when only entityId is present"
24/// );
25/// ```
26pub struct OneOfValidator;
27
28impl OneOfValidator {
29    /// Validate that exactly one field from the set is present and non-null.
30    ///
31    /// # Errors
32    ///
33    /// Returns `FraiseQLError::Validation` if zero or more than one field is present.
34    pub fn validate(
35        input: &Value,
36        field_names: &[String],
37        context_path: Option<&str>,
38    ) -> Result<()> {
39        let field_path = context_path.unwrap_or("input");
40
41        let present_count = field_names
42            .iter()
43            .filter(|name| {
44                if let Value::Object(obj) = input {
45                    obj.get(*name).is_some_and(|v| !matches!(v, Value::Null))
46                } else {
47                    false
48                }
49            })
50            .count();
51
52        if present_count != 1 {
53            return Err(FraiseQLError::Validation {
54                message: format!(
55                    "Exactly one of [{}] must be provided, but {} {} provided",
56                    field_names.join(", "),
57                    present_count,
58                    if present_count == 1 { "was" } else { "were" }
59                ),
60                path:    Some(field_path.to_string()),
61            });
62        }
63
64        Ok(())
65    }
66}
67
68/// Validates that at least one field from the specified set is provided.
69///
70/// # Example
71/// ```
72/// use fraiseql_core::validation::mutual_exclusivity::AnyOfValidator;
73/// use serde_json::json;
74/// // At least one of: email, phone, address must be present
75/// let input = json!({ "email": "user@example.com", "phone": null, "address": null });
76/// assert!(
77///     AnyOfValidator::validate(&input, &["email".to_string(), "phone".to_string(), "address".to_string()], None).is_ok(),
78///     "any-of constraint satisfied when at least one field is present"
79/// );
80/// ```
81pub struct AnyOfValidator;
82
83impl AnyOfValidator {
84    /// Validate that at least one field from the set is present and non-null.
85    ///
86    /// # Errors
87    ///
88    /// Returns `FraiseQLError::Validation` if none of the specified fields are present.
89    pub fn validate(
90        input: &Value,
91        field_names: &[String],
92        context_path: Option<&str>,
93    ) -> Result<()> {
94        let field_path = context_path.unwrap_or("input");
95
96        let has_any = field_names.iter().any(|name| {
97            if let Value::Object(obj) = input {
98                obj.get(name).is_some_and(|v| !matches!(v, Value::Null))
99            } else {
100                false
101            }
102        });
103
104        if !has_any {
105            return Err(FraiseQLError::Validation {
106                message: format!("At least one of [{}] must be provided", field_names.join(", ")),
107                path:    Some(field_path.to_string()),
108            });
109        }
110
111        Ok(())
112    }
113}
114
115/// Validates conditional requirement: if one field is present, others must be too.
116///
117/// # Example
118/// ```
119/// use fraiseql_core::validation::mutual_exclusivity::ConditionalRequiredValidator;
120/// use serde_json::json;
121/// // If isPremium is true, then paymentMethod is required
122/// let input = json!({ "isPremium": true, "paymentMethod": "credit_card" });
123/// assert!(
124///     ConditionalRequiredValidator::validate(&input, "isPremium", &["paymentMethod".to_string()], None).is_ok(),
125///     "conditional requirement satisfied when condition field is true and required field present"
126/// );
127/// ```
128pub struct ConditionalRequiredValidator;
129
130impl ConditionalRequiredValidator {
131    /// Validate that if `if_field_present` is present, all `then_required` fields must be too.
132    ///
133    /// # Errors
134    ///
135    /// Returns `FraiseQLError::Validation` if the condition field is present but any
136    /// required field is missing.
137    pub fn validate(
138        input: &Value,
139        if_field_present: &str,
140        then_required: &[String],
141        context_path: Option<&str>,
142    ) -> Result<()> {
143        let field_path = context_path.unwrap_or("input");
144
145        if let Value::Object(obj) = input {
146            // Check if the condition field is present and non-null
147            let condition_met =
148                obj.get(if_field_present).is_some_and(|v| !matches!(v, Value::Null));
149
150            if condition_met {
151                // If condition is met, check that all required fields are present
152                let missing_fields: Vec<&String> = then_required
153                    .iter()
154                    .filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
155                    .collect();
156
157                if !missing_fields.is_empty() {
158                    return Err(FraiseQLError::Validation {
159                        message: format!(
160                            "Since '{}' is provided, {} must also be provided",
161                            if_field_present,
162                            missing_fields
163                                .iter()
164                                .map(|s| format!("'{}'", s))
165                                .collect::<Vec<_>>()
166                                .join(", ")
167                        ),
168                        path:    Some(field_path.to_string()),
169                    });
170                }
171            }
172        }
173
174        Ok(())
175    }
176}
177
178/// Validates conditional requirement based on absence: if one field is missing, others must be
179/// provided.
180///
181/// # Example
182/// ```
183/// use fraiseql_core::validation::mutual_exclusivity::RequiredIfAbsentValidator;
184/// use serde_json::json;
185/// // If addressId is not provided, then street, city, zip must all be provided
186/// let input = json!({ "addressId": null, "street": "123 Main St", "city": "Springfield", "zip": "12345" });
187/// assert!(
188///     RequiredIfAbsentValidator::validate(&input, "addressId", &["street".to_string(), "city".to_string(), "zip".to_string()], None).is_ok(),
189///     "required-if-absent constraint satisfied when absent field is null and all required fields present"
190/// );
191/// ```
192pub struct RequiredIfAbsentValidator;
193
194impl RequiredIfAbsentValidator {
195    /// Validate that if `absent_field` is absent/null, all `then_required` fields must be provided.
196    ///
197    /// # Errors
198    ///
199    /// Returns `FraiseQLError::Validation` if the condition field is absent and any
200    /// required field is also missing.
201    pub fn validate(
202        input: &Value,
203        absent_field: &str,
204        then_required: &[String],
205        context_path: Option<&str>,
206    ) -> Result<()> {
207        let field_path = context_path.unwrap_or("input");
208
209        if let Value::Object(obj) = input {
210            // Check if the condition field is absent or null
211            let field_absent = obj.get(absent_field).is_none_or(|v| matches!(v, Value::Null));
212
213            if field_absent {
214                // If field is absent, check that all required fields are present
215                let missing_fields: Vec<&String> = then_required
216                    .iter()
217                    .filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
218                    .collect();
219
220                if !missing_fields.is_empty() {
221                    return Err(FraiseQLError::Validation {
222                        message: format!(
223                            "Since '{}' is not provided, {} must be provided",
224                            absent_field,
225                            missing_fields
226                                .iter()
227                                .map(|s| format!("'{}'", s))
228                                .collect::<Vec<_>>()
229                                .join(", ")
230                        ),
231                        path:    Some(field_path.to_string()),
232                    });
233                }
234            }
235        }
236
237        Ok(())
238    }
239}