1use serde_json::Value;
10
11use crate::error::{FraiseQLError, Result};
12
13pub struct OneOfValidator;
27
28impl OneOfValidator {
29 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
68pub struct AnyOfValidator;
82
83impl AnyOfValidator {
84 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
115pub struct ConditionalRequiredValidator;
129
130impl ConditionalRequiredValidator {
131 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 let condition_met =
148 obj.get(if_field_present).is_some_and(|v| !matches!(v, Value::Null));
149
150 if condition_met {
151 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
178pub struct RequiredIfAbsentValidator;
193
194impl RequiredIfAbsentValidator {
195 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 let field_absent = obj.get(absent_field).is_none_or(|v| matches!(v, Value::Null));
212
213 if field_absent {
214 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}
240
241#[cfg(test)]
242mod tests {
243 use serde_json::json;
244
245 use super::*;
246
247 #[test]
248 fn test_one_of_validator_exactly_one_present() {
249 let input = json!({
250 "entityId": "123",
251 "entityPayload": null
252 });
253 let result = OneOfValidator::validate(
254 &input,
255 &["entityId".to_string(), "entityPayload".to_string()],
256 None,
257 );
258 result.unwrap_or_else(|e| panic!("expected exactly-one to pass with one present: {e}"));
259 }
260
261 #[test]
262 fn test_one_of_validator_both_present() {
263 let input = json!({
264 "entityId": "123",
265 "entityPayload": { "name": "test" }
266 });
267 let result = OneOfValidator::validate(
268 &input,
269 &["entityId".to_string(), "entityPayload".to_string()],
270 None,
271 );
272 assert!(
273 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Exactly one of")),
274 "expected Validation error for both fields present, got: {result:?}"
275 );
276 }
277
278 #[test]
279 fn test_one_of_validator_neither_present() {
280 let input = json!({
281 "entityId": null,
282 "entityPayload": null
283 });
284 let result = OneOfValidator::validate(
285 &input,
286 &["entityId".to_string(), "entityPayload".to_string()],
287 None,
288 );
289 assert!(
290 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Exactly one of")),
291 "expected Validation error for neither field present, got: {result:?}"
292 );
293 }
294
295 #[test]
296 fn test_one_of_validator_missing_field() {
297 let input = json!({
298 "entityId": "123"
299 });
300 let result = OneOfValidator::validate(
301 &input,
302 &["entityId".to_string(), "entityPayload".to_string()],
303 None,
304 );
305 result.unwrap_or_else(|e| {
306 panic!("expected exactly-one to pass with one field missing from object: {e}")
307 });
308 }
309
310 #[test]
311 fn test_any_of_validator_one_present() {
312 let input = json!({
313 "email": "user@example.com",
314 "phone": null,
315 "address": null
316 });
317 let result = AnyOfValidator::validate(
318 &input,
319 &[
320 "email".to_string(),
321 "phone".to_string(),
322 "address".to_string(),
323 ],
324 None,
325 );
326 result.unwrap_or_else(|e| panic!("expected any-of to pass with one present: {e}"));
327 }
328
329 #[test]
330 fn test_any_of_validator_multiple_present() {
331 let input = json!({
332 "email": "user@example.com",
333 "phone": "+1234567890",
334 "address": null
335 });
336 let result = AnyOfValidator::validate(
337 &input,
338 &[
339 "email".to_string(),
340 "phone".to_string(),
341 "address".to_string(),
342 ],
343 None,
344 );
345 result.unwrap_or_else(|e| panic!("expected any-of to pass with multiple present: {e}"));
346 }
347
348 #[test]
349 fn test_any_of_validator_none_present() {
350 let input = json!({
351 "email": null,
352 "phone": null,
353 "address": null
354 });
355 let result = AnyOfValidator::validate(
356 &input,
357 &[
358 "email".to_string(),
359 "phone".to_string(),
360 "address".to_string(),
361 ],
362 None,
363 );
364 assert!(
365 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("At least one of")),
366 "expected Validation error for no fields present, got: {result:?}"
367 );
368 }
369
370 #[test]
371 fn test_conditional_required_validator_condition_met_requirement_met() {
372 let input = json!({
373 "isPremium": true,
374 "paymentMethod": "credit_card"
375 });
376 let result = ConditionalRequiredValidator::validate(
377 &input,
378 "isPremium",
379 &["paymentMethod".to_string()],
380 None,
381 );
382 result.unwrap_or_else(|e| {
383 panic!("expected conditional-required to pass when requirement met: {e}")
384 });
385 }
386
387 #[test]
388 fn test_conditional_required_validator_condition_met_requirement_missing() {
389 let input = json!({
390 "isPremium": true,
391 "paymentMethod": null
392 });
393 let result = ConditionalRequiredValidator::validate(
394 &input,
395 "isPremium",
396 &["paymentMethod".to_string()],
397 None,
398 );
399 assert!(
400 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Since") && message.contains("must also be provided")),
401 "expected Validation error for missing conditional requirement, got: {result:?}"
402 );
403 }
404
405 #[test]
406 fn test_conditional_required_validator_condition_not_met() {
407 let input = json!({
408 "isPremium": null,
409 "paymentMethod": null
410 });
411 let result = ConditionalRequiredValidator::validate(
412 &input,
413 "isPremium",
414 &["paymentMethod".to_string()],
415 None,
416 );
417 result.unwrap_or_else(|e| {
418 panic!("expected conditional-required to pass when condition not met: {e}")
419 });
420 }
421
422 #[test]
423 fn test_conditional_required_validator_multiple_requirements() {
424 let input = json!({
425 "isInternational": true,
426 "customsCode": "ABC123",
427 "importDuties": "50.00"
428 });
429 let result = ConditionalRequiredValidator::validate(
430 &input,
431 "isInternational",
432 &["customsCode".to_string(), "importDuties".to_string()],
433 None,
434 );
435 result.unwrap_or_else(|e| {
436 panic!("expected conditional-required to pass with all requirements met: {e}")
437 });
438 }
439
440 #[test]
441 fn test_conditional_required_validator_one_requirement_missing() {
442 let input = json!({
443 "isInternational": true,
444 "customsCode": "ABC123",
445 "importDuties": null
446 });
447 let result = ConditionalRequiredValidator::validate(
448 &input,
449 "isInternational",
450 &["customsCode".to_string(), "importDuties".to_string()],
451 None,
452 );
453 assert!(
454 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Since") && message.contains("must also be provided")),
455 "expected Validation error for one missing requirement, got: {result:?}"
456 );
457 }
458
459 #[test]
460 fn test_required_if_absent_validator_field_absent_requirements_met() {
461 let input = json!({
462 "addressId": null,
463 "street": "123 Main St",
464 "city": "Springfield",
465 "zip": "12345"
466 });
467 let result = RequiredIfAbsentValidator::validate(
468 &input,
469 "addressId",
470 &["street".to_string(), "city".to_string(), "zip".to_string()],
471 None,
472 );
473 result.unwrap_or_else(|e| {
474 panic!("expected required-if-absent to pass when requirements met: {e}")
475 });
476 }
477
478 #[test]
479 fn test_required_if_absent_validator_field_absent_requirements_missing() {
480 let input = json!({
481 "addressId": null,
482 "street": "123 Main St",
483 "city": null,
484 "zip": "12345"
485 });
486 let result = RequiredIfAbsentValidator::validate(
487 &input,
488 "addressId",
489 &["street".to_string(), "city".to_string(), "zip".to_string()],
490 None,
491 );
492 assert!(
493 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Since") && message.contains("must be provided")),
494 "expected Validation error for missing requirements when field absent, got: {result:?}"
495 );
496 }
497
498 #[test]
499 fn test_required_if_absent_validator_field_present() {
500 let input = json!({
501 "addressId": "addr_123",
502 "street": null,
503 "city": null,
504 "zip": null
505 });
506 let result = RequiredIfAbsentValidator::validate(
507 &input,
508 "addressId",
509 &["street".to_string(), "city".to_string(), "zip".to_string()],
510 None,
511 );
512 result.unwrap_or_else(|e| {
513 panic!("expected required-if-absent to pass when field present: {e}")
514 });
515 }
516
517 #[test]
518 fn test_required_if_absent_validator_all_missing_from_object() {
519 let input = json!({});
520 let result = RequiredIfAbsentValidator::validate(
521 &input,
522 "addressId",
523 &["street".to_string(), "city".to_string()],
524 None,
525 );
526 assert!(
527 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Since") && message.contains("must be provided")),
528 "expected Validation error for all fields missing from empty object, got: {result:?}"
529 );
530 }
531
532 #[test]
533 fn test_error_messages_include_context() {
534 let input = json!({
535 "entityId": "123",
536 "entityPayload": { "name": "test" }
537 });
538 let result = OneOfValidator::validate(
539 &input,
540 &["entityId".to_string(), "entityPayload".to_string()],
541 Some("createInput"),
542 );
543 assert!(
544 matches!(result, Err(FraiseQLError::Validation { ref path, .. }) if *path == Some("createInput".to_string())),
545 "expected Validation error with path 'createInput', got: {result:?}"
546 );
547 }
548}