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/// ```ignore
17/// // Either entityId OR entityPayload, but not both
18/// validator.validate_one_of(input, &["entityId", "entityPayload"])
19/// ```
20pub struct OneOfValidator;
21
22impl OneOfValidator {
23    /// Validate that exactly one field from the set is present and non-null.
24    pub fn validate(
25        input: &Value,
26        field_names: &[String],
27        context_path: Option<&str>,
28    ) -> Result<()> {
29        let field_path = context_path.unwrap_or("input");
30
31        let present_count = field_names
32            .iter()
33            .filter(|name| {
34                if let Value::Object(obj) = input {
35                    obj.get(*name).map(|v| !matches!(v, Value::Null)).unwrap_or(false)
36                } else {
37                    false
38                }
39            })
40            .count();
41
42        if present_count != 1 {
43            return Err(FraiseQLError::Validation {
44                message: format!(
45                    "Exactly one of [{}] must be provided, but {} {} provided",
46                    field_names.join(", "),
47                    present_count,
48                    if present_count == 1 { "was" } else { "were" }
49                ),
50                path:    Some(field_path.to_string()),
51            });
52        }
53
54        Ok(())
55    }
56}
57
58/// Validates that at least one field from the specified set is provided.
59///
60/// # Example
61/// ```ignore
62/// // At least one of: email, phone, address must be present
63/// validator.validate_any_of(input, &["email", "phone", "address"])
64/// ```
65pub struct AnyOfValidator;
66
67impl AnyOfValidator {
68    /// Validate that at least one field from the set is present and non-null.
69    pub fn validate(
70        input: &Value,
71        field_names: &[String],
72        context_path: Option<&str>,
73    ) -> Result<()> {
74        let field_path = context_path.unwrap_or("input");
75
76        let has_any = field_names.iter().any(|name| {
77            if let Value::Object(obj) = input {
78                obj.get(name).map(|v| !matches!(v, Value::Null)).unwrap_or(false)
79            } else {
80                false
81            }
82        });
83
84        if !has_any {
85            return Err(FraiseQLError::Validation {
86                message: format!("At least one of [{}] must be provided", field_names.join(", ")),
87                path:    Some(field_path.to_string()),
88            });
89        }
90
91        Ok(())
92    }
93}
94
95/// Validates conditional requirement: if one field is present, others must be too.
96///
97/// # Example
98/// ```ignore
99/// // If isPremium is true, then paymentMethod is required
100/// validator.validate_conditional_required(
101///     input,
102///     "isPremium",
103///     &["paymentMethod"]
104/// )
105/// ```
106pub struct ConditionalRequiredValidator;
107
108impl ConditionalRequiredValidator {
109    /// Validate that if `if_field_present` is present, all `then_required` fields must be too.
110    pub fn validate(
111        input: &Value,
112        if_field_present: &str,
113        then_required: &[String],
114        context_path: Option<&str>,
115    ) -> Result<()> {
116        let field_path = context_path.unwrap_or("input");
117
118        if let Value::Object(obj) = input {
119            // Check if the condition field is present and non-null
120            let condition_met =
121                obj.get(if_field_present).map(|v| !matches!(v, Value::Null)).unwrap_or(false);
122
123            if condition_met {
124                // If condition is met, check that all required fields are present
125                let missing_fields: Vec<&String> = then_required
126                    .iter()
127                    .filter(|name| obj.get(*name).map(|v| matches!(v, Value::Null)).unwrap_or(true))
128                    .collect();
129
130                if !missing_fields.is_empty() {
131                    return Err(FraiseQLError::Validation {
132                        message: format!(
133                            "Since '{}' is provided, {} must also be provided",
134                            if_field_present,
135                            missing_fields
136                                .iter()
137                                .map(|s| format!("'{}'", s))
138                                .collect::<Vec<_>>()
139                                .join(", ")
140                        ),
141                        path:    Some(field_path.to_string()),
142                    });
143                }
144            }
145        }
146
147        Ok(())
148    }
149}
150
151/// Validates conditional requirement based on absence: if one field is missing, others must be
152/// provided.
153///
154/// # Example
155/// ```ignore
156/// // If addressId is not provided, then street, city, zip must all be provided
157/// validator.validate_required_if_absent(
158///     input,
159///     "addressId",
160///     &["street", "city", "zip"]
161/// )
162/// ```
163pub struct RequiredIfAbsentValidator;
164
165impl RequiredIfAbsentValidator {
166    /// Validate that if `absent_field` is absent/null, all `then_required` fields must be provided.
167    pub fn validate(
168        input: &Value,
169        absent_field: &str,
170        then_required: &[String],
171        context_path: Option<&str>,
172    ) -> Result<()> {
173        let field_path = context_path.unwrap_or("input");
174
175        if let Value::Object(obj) = input {
176            // Check if the condition field is absent or null
177            let field_absent =
178                obj.get(absent_field).map(|v| matches!(v, Value::Null)).unwrap_or(true);
179
180            if field_absent {
181                // If field is absent, check that all required fields are present
182                let missing_fields: Vec<&String> = then_required
183                    .iter()
184                    .filter(|name| obj.get(*name).map(|v| matches!(v, Value::Null)).unwrap_or(true))
185                    .collect();
186
187                if !missing_fields.is_empty() {
188                    return Err(FraiseQLError::Validation {
189                        message: format!(
190                            "Since '{}' is not provided, {} must be provided",
191                            absent_field,
192                            missing_fields
193                                .iter()
194                                .map(|s| format!("'{}'", s))
195                                .collect::<Vec<_>>()
196                                .join(", ")
197                        ),
198                        path:    Some(field_path.to_string()),
199                    });
200                }
201            }
202        }
203
204        Ok(())
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use serde_json::json;
211
212    use super::*;
213
214    #[test]
215    fn test_one_of_validator_exactly_one_present() {
216        let input = json!({
217            "entityId": "123",
218            "entityPayload": null
219        });
220        let result = OneOfValidator::validate(
221            &input,
222            &["entityId".to_string(), "entityPayload".to_string()],
223            None,
224        );
225        assert!(result.is_ok());
226    }
227
228    #[test]
229    fn test_one_of_validator_both_present() {
230        let input = json!({
231            "entityId": "123",
232            "entityPayload": { "name": "test" }
233        });
234        let result = OneOfValidator::validate(
235            &input,
236            &["entityId".to_string(), "entityPayload".to_string()],
237            None,
238        );
239        assert!(result.is_err());
240    }
241
242    #[test]
243    fn test_one_of_validator_neither_present() {
244        let input = json!({
245            "entityId": null,
246            "entityPayload": null
247        });
248        let result = OneOfValidator::validate(
249            &input,
250            &["entityId".to_string(), "entityPayload".to_string()],
251            None,
252        );
253        assert!(result.is_err());
254    }
255
256    #[test]
257    fn test_one_of_validator_missing_field() {
258        let input = json!({
259            "entityId": "123"
260        });
261        let result = OneOfValidator::validate(
262            &input,
263            &["entityId".to_string(), "entityPayload".to_string()],
264            None,
265        );
266        assert!(result.is_ok());
267    }
268
269    #[test]
270    fn test_any_of_validator_one_present() {
271        let input = json!({
272            "email": "user@example.com",
273            "phone": null,
274            "address": null
275        });
276        let result = AnyOfValidator::validate(
277            &input,
278            &[
279                "email".to_string(),
280                "phone".to_string(),
281                "address".to_string(),
282            ],
283            None,
284        );
285        assert!(result.is_ok());
286    }
287
288    #[test]
289    fn test_any_of_validator_multiple_present() {
290        let input = json!({
291            "email": "user@example.com",
292            "phone": "+1234567890",
293            "address": null
294        });
295        let result = AnyOfValidator::validate(
296            &input,
297            &[
298                "email".to_string(),
299                "phone".to_string(),
300                "address".to_string(),
301            ],
302            None,
303        );
304        assert!(result.is_ok());
305    }
306
307    #[test]
308    fn test_any_of_validator_none_present() {
309        let input = json!({
310            "email": null,
311            "phone": null,
312            "address": null
313        });
314        let result = AnyOfValidator::validate(
315            &input,
316            &[
317                "email".to_string(),
318                "phone".to_string(),
319                "address".to_string(),
320            ],
321            None,
322        );
323        assert!(result.is_err());
324    }
325
326    #[test]
327    fn test_conditional_required_validator_condition_met_requirement_met() {
328        let input = json!({
329            "isPremium": true,
330            "paymentMethod": "credit_card"
331        });
332        let result = ConditionalRequiredValidator::validate(
333            &input,
334            "isPremium",
335            &["paymentMethod".to_string()],
336            None,
337        );
338        assert!(result.is_ok());
339    }
340
341    #[test]
342    fn test_conditional_required_validator_condition_met_requirement_missing() {
343        let input = json!({
344            "isPremium": true,
345            "paymentMethod": null
346        });
347        let result = ConditionalRequiredValidator::validate(
348            &input,
349            "isPremium",
350            &["paymentMethod".to_string()],
351            None,
352        );
353        assert!(result.is_err());
354    }
355
356    #[test]
357    fn test_conditional_required_validator_condition_not_met() {
358        let input = json!({
359            "isPremium": null,
360            "paymentMethod": null
361        });
362        let result = ConditionalRequiredValidator::validate(
363            &input,
364            "isPremium",
365            &["paymentMethod".to_string()],
366            None,
367        );
368        assert!(result.is_ok());
369    }
370
371    #[test]
372    fn test_conditional_required_validator_multiple_requirements() {
373        let input = json!({
374            "isInternational": true,
375            "customsCode": "ABC123",
376            "importDuties": "50.00"
377        });
378        let result = ConditionalRequiredValidator::validate(
379            &input,
380            "isInternational",
381            &["customsCode".to_string(), "importDuties".to_string()],
382            None,
383        );
384        assert!(result.is_ok());
385    }
386
387    #[test]
388    fn test_conditional_required_validator_one_requirement_missing() {
389        let input = json!({
390            "isInternational": true,
391            "customsCode": "ABC123",
392            "importDuties": null
393        });
394        let result = ConditionalRequiredValidator::validate(
395            &input,
396            "isInternational",
397            &["customsCode".to_string(), "importDuties".to_string()],
398            None,
399        );
400        assert!(result.is_err());
401    }
402
403    #[test]
404    fn test_required_if_absent_validator_field_absent_requirements_met() {
405        let input = json!({
406            "addressId": null,
407            "street": "123 Main St",
408            "city": "Springfield",
409            "zip": "12345"
410        });
411        let result = RequiredIfAbsentValidator::validate(
412            &input,
413            "addressId",
414            &["street".to_string(), "city".to_string(), "zip".to_string()],
415            None,
416        );
417        assert!(result.is_ok());
418    }
419
420    #[test]
421    fn test_required_if_absent_validator_field_absent_requirements_missing() {
422        let input = json!({
423            "addressId": null,
424            "street": "123 Main St",
425            "city": null,
426            "zip": "12345"
427        });
428        let result = RequiredIfAbsentValidator::validate(
429            &input,
430            "addressId",
431            &["street".to_string(), "city".to_string(), "zip".to_string()],
432            None,
433        );
434        assert!(result.is_err());
435    }
436
437    #[test]
438    fn test_required_if_absent_validator_field_present() {
439        let input = json!({
440            "addressId": "addr_123",
441            "street": null,
442            "city": null,
443            "zip": null
444        });
445        let result = RequiredIfAbsentValidator::validate(
446            &input,
447            "addressId",
448            &["street".to_string(), "city".to_string(), "zip".to_string()],
449            None,
450        );
451        assert!(result.is_ok());
452    }
453
454    #[test]
455    fn test_required_if_absent_validator_all_missing_from_object() {
456        let input = json!({});
457        let result = RequiredIfAbsentValidator::validate(
458            &input,
459            "addressId",
460            &["street".to_string(), "city".to_string()],
461            None,
462        );
463        assert!(result.is_err());
464    }
465
466    #[test]
467    fn test_error_messages_include_context() {
468        let input = json!({
469            "entityId": "123",
470            "entityPayload": { "name": "test" }
471        });
472        let result = OneOfValidator::validate(
473            &input,
474            &["entityId".to_string(), "entityPayload".to_string()],
475            Some("createInput"),
476        );
477        assert!(result.is_err());
478        if let Err(FraiseQLError::Validation { path, .. }) = result {
479            assert_eq!(path, Some("createInput".to_string()));
480        }
481    }
482}