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}