fraiseql_core/validation/
mutual_exclusivity.rs1use serde_json::Value;
10
11use crate::error::{FraiseQLError, Result};
12
13pub struct OneOfValidator;
21
22impl OneOfValidator {
23 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
58pub struct AnyOfValidator;
66
67impl AnyOfValidator {
68 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
95pub struct ConditionalRequiredValidator;
107
108impl ConditionalRequiredValidator {
109 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 let condition_met =
121 obj.get(if_field_present).map(|v| !matches!(v, Value::Null)).unwrap_or(false);
122
123 if condition_met {
124 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
151pub struct RequiredIfAbsentValidator;
164
165impl RequiredIfAbsentValidator {
166 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 let field_absent =
178 obj.get(absent_field).map(|v| matches!(v, Value::Null)).unwrap_or(true);
179
180 if field_absent {
181 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}