1use serde_json::Value;
31
32use crate::error::{FraiseQLError, Result};
33
34#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub enum InputObjectRule {
38 AnyOf {
40 fields: Vec<String>,
42 },
43 OneOf {
45 fields: Vec<String>,
47 },
48 ConditionalRequired {
50 if_field: String,
52 then_fields: Vec<String>,
54 },
55 RequiredIfAbsent {
57 absent_field: String,
59 then_fields: Vec<String>,
61 },
62 Custom {
64 name: String,
66 },
67}
68
69#[derive(Debug, Clone, Default)]
71pub struct InputObjectValidationResult {
72 pub errors: Vec<String>,
74 pub error_count: usize,
76}
77
78impl InputObjectValidationResult {
79 pub const fn new() -> Self {
81 Self {
82 errors: Vec::new(),
83 error_count: 0,
84 }
85 }
86
87 pub fn add_error(&mut self, error: String) {
89 self.errors.push(error);
90 self.error_count += 1;
91 }
92
93 pub fn add_errors(&mut self, errors: Vec<String>) {
95 self.error_count += errors.len();
96 self.errors.extend(errors);
97 }
98
99 pub const fn has_errors(&self) -> bool {
101 !self.errors.is_empty()
102 }
103
104 pub fn into_result(self) -> Result<()> {
111 self.into_result_with_path("input")
112 }
113
114 pub fn into_result_with_path(self, path: &str) -> Result<()> {
121 if self.has_errors() {
122 Err(FraiseQLError::Validation {
123 message: format!("Input object validation failed: {}", self.errors.join("; ")),
124 path: Some(path.to_string()),
125 })
126 } else {
127 Ok(())
128 }
129 }
130}
131
132pub fn validate_input_object(
145 input: &Value,
146 rules: &[InputObjectRule],
147 object_path: Option<&str>,
148) -> Result<()> {
149 let mut result = InputObjectValidationResult::new();
150 let path = object_path.unwrap_or("input");
151
152 if !matches!(input, Value::Object(_)) {
153 return Err(FraiseQLError::Validation {
154 message: "Input must be an object".to_string(),
155 path: Some(path.to_string()),
156 });
157 }
158
159 for rule in rules {
160 if let Err(FraiseQLError::Validation { message, .. }) = validate_rule(input, rule, path) {
161 result.add_error(message);
162 }
163 }
164
165 result.into_result_with_path(path)
166}
167
168fn validate_rule(input: &Value, rule: &InputObjectRule, path: &str) -> Result<()> {
170 match rule {
171 InputObjectRule::AnyOf { fields } => validate_any_of(input, fields, path),
172 InputObjectRule::OneOf { fields } => validate_one_of(input, fields, path),
173 InputObjectRule::ConditionalRequired {
174 if_field,
175 then_fields,
176 } => validate_conditional_required(input, if_field, then_fields, path),
177 InputObjectRule::RequiredIfAbsent {
178 absent_field,
179 then_fields,
180 } => validate_required_if_absent(input, absent_field, then_fields, path),
181 InputObjectRule::Custom { name } => Err(FraiseQLError::Validation {
182 message: format!(
183 "Custom validator '{name}' is not registered. \
184 Register validators via InputValidatorRegistry before executing queries."
185 ),
186 path: Some(path.to_string()),
187 }),
188 }
189}
190
191fn validate_any_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
193 if let Value::Object(obj) = input {
194 let has_any = fields
195 .iter()
196 .any(|name| obj.get(name).is_some_and(|v| !matches!(v, Value::Null)));
197
198 if !has_any {
199 return Err(FraiseQLError::Validation {
200 message: format!("At least one of [{}] must be provided", fields.join(", ")),
201 path: Some(path.to_string()),
202 });
203 }
204 }
205
206 Ok(())
207}
208
209fn validate_one_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
211 if let Value::Object(obj) = input {
212 let present_count = fields
213 .iter()
214 .filter(|name| obj.get(*name).is_some_and(|v| !matches!(v, Value::Null)))
215 .count();
216
217 if present_count != 1 {
218 return Err(FraiseQLError::Validation {
219 message: format!(
220 "Exactly one of [{}] must be provided, but {} {} provided",
221 fields.join(", "),
222 present_count,
223 if present_count == 1 { "was" } else { "were" }
224 ),
225 path: Some(path.to_string()),
226 });
227 }
228 }
229
230 Ok(())
231}
232
233fn validate_conditional_required(
235 input: &Value,
236 if_field: &str,
237 then_fields: &[String],
238 path: &str,
239) -> Result<()> {
240 if let Value::Object(obj) = input {
241 let condition_met = obj.get(if_field).is_some_and(|v| !matches!(v, Value::Null));
242
243 if condition_met {
244 let missing_fields: Vec<&String> = then_fields
245 .iter()
246 .filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
247 .collect();
248
249 if !missing_fields.is_empty() {
250 return Err(FraiseQLError::Validation {
251 message: format!(
252 "Since '{}' is provided, {} must also be provided",
253 if_field,
254 missing_fields
255 .iter()
256 .map(|s| format!("'{}'", s))
257 .collect::<Vec<_>>()
258 .join(", ")
259 ),
260 path: Some(path.to_string()),
261 });
262 }
263 }
264 }
265
266 Ok(())
267}
268
269fn validate_required_if_absent(
271 input: &Value,
272 absent_field: &str,
273 then_fields: &[String],
274 path: &str,
275) -> Result<()> {
276 if let Value::Object(obj) = input {
277 let field_absent = obj.get(absent_field).is_none_or(|v| matches!(v, Value::Null));
278
279 if field_absent {
280 let missing_fields: Vec<&String> = then_fields
281 .iter()
282 .filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
283 .collect();
284
285 if !missing_fields.is_empty() {
286 return Err(FraiseQLError::Validation {
287 message: format!(
288 "Since '{}' is not provided, {} must be provided",
289 absent_field,
290 missing_fields
291 .iter()
292 .map(|s| format!("'{}'", s))
293 .collect::<Vec<_>>()
294 .join(", ")
295 ),
296 path: Some(path.to_string()),
297 });
298 }
299 }
300 }
301
302 Ok(())
303}
304
305#[cfg(test)]
306mod tests {
307 #![allow(clippy::unwrap_used)] use serde_json::json;
310
311 use super::*;
312
313 #[test]
314 fn test_any_of_passes() {
315 let input = json!({
316 "email": "user@example.com",
317 "phone": null,
318 "address": null
319 });
320 let rules = vec![InputObjectRule::AnyOf {
321 fields: vec![
322 "email".to_string(),
323 "phone".to_string(),
324 "address".to_string(),
325 ],
326 }];
327 let result = validate_input_object(&input, &rules, None);
328 result.unwrap_or_else(|e| panic!("any_of should pass when email is present: {e}"));
329 }
330
331 #[test]
332 fn test_any_of_fails() {
333 let input = json!({
334 "email": null,
335 "phone": null,
336 "address": null
337 });
338 let rules = vec![InputObjectRule::AnyOf {
339 fields: vec![
340 "email".to_string(),
341 "phone".to_string(),
342 "address".to_string(),
343 ],
344 }];
345 let result = validate_input_object(&input, &rules, None);
346 assert!(
347 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("At least one of")),
348 "expected Validation error about missing fields, got: {result:?}"
349 );
350 }
351
352 #[test]
353 fn test_one_of_passes() {
354 let input = json!({
355 "entityId": "123",
356 "entityPayload": null
357 });
358 let rules = vec![InputObjectRule::OneOf {
359 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
360 }];
361 let result = validate_input_object(&input, &rules, None);
362 result.unwrap_or_else(|e| {
363 panic!("one_of should pass when exactly one field is present: {e}")
364 });
365 }
366
367 #[test]
368 fn test_one_of_fails_both_present() {
369 let input = json!({
370 "entityId": "123",
371 "entityPayload": { "name": "test" }
372 });
373 let rules = vec![InputObjectRule::OneOf {
374 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
375 }];
376 let result = validate_input_object(&input, &rules, None);
377 assert!(
378 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Exactly one of")),
379 "expected Validation error about exactly one field, got: {result:?}"
380 );
381 }
382
383 #[test]
384 fn test_one_of_fails_neither_present() {
385 let input = json!({
386 "entityId": null,
387 "entityPayload": null
388 });
389 let rules = vec![InputObjectRule::OneOf {
390 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
391 }];
392 let result = validate_input_object(&input, &rules, None);
393 assert!(
394 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Exactly one of")),
395 "expected Validation error about exactly one field, got: {result:?}"
396 );
397 }
398
399 #[test]
400 fn test_conditional_required_passes() {
401 let input = json!({
402 "isPremium": true,
403 "paymentMethod": "credit_card"
404 });
405 let rules = vec![InputObjectRule::ConditionalRequired {
406 if_field: "isPremium".to_string(),
407 then_fields: vec!["paymentMethod".to_string()],
408 }];
409 let result = validate_input_object(&input, &rules, None);
410 result.unwrap_or_else(|e| {
411 panic!("conditional_required should pass when condition is met: {e}")
412 });
413 }
414
415 #[test]
416 fn test_conditional_required_fails() {
417 let input = json!({
418 "isPremium": true,
419 "paymentMethod": null
420 });
421 let rules = vec![InputObjectRule::ConditionalRequired {
422 if_field: "isPremium".to_string(),
423 then_fields: vec!["paymentMethod".to_string()],
424 }];
425 let result = validate_input_object(&input, &rules, None);
426 assert!(
427 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("must also be provided")),
428 "expected Validation error about missing conditional fields, got: {result:?}"
429 );
430 }
431
432 #[test]
433 fn test_conditional_required_skips_when_condition_false() {
434 let input = json!({
435 "isPremium": null,
436 "paymentMethod": null
437 });
438 let rules = vec![InputObjectRule::ConditionalRequired {
439 if_field: "isPremium".to_string(),
440 then_fields: vec!["paymentMethod".to_string()],
441 }];
442 let result = validate_input_object(&input, &rules, None);
443 result.unwrap_or_else(|e| {
444 panic!("conditional_required should skip when condition field is null: {e}")
445 });
446 }
447
448 #[test]
449 fn test_required_if_absent_passes() {
450 let input = json!({
451 "addressId": null,
452 "street": "123 Main St",
453 "city": "Springfield",
454 "zip": "12345"
455 });
456 let rules = vec![InputObjectRule::RequiredIfAbsent {
457 absent_field: "addressId".to_string(),
458 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
459 }];
460 let result = validate_input_object(&input, &rules, None);
461 result.unwrap_or_else(|e| {
462 panic!("required_if_absent should pass when all then_fields are provided: {e}")
463 });
464 }
465
466 #[test]
467 fn test_required_if_absent_fails() {
468 let input = json!({
469 "addressId": null,
470 "street": "123 Main St",
471 "city": null,
472 "zip": "12345"
473 });
474 let rules = vec![InputObjectRule::RequiredIfAbsent {
475 absent_field: "addressId".to_string(),
476 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
477 }];
478 let result = validate_input_object(&input, &rules, None);
479 assert!(
480 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("must be provided")),
481 "expected Validation error about missing required fields, got: {result:?}"
482 );
483 }
484
485 #[test]
486 fn test_required_if_absent_skips_when_field_present() {
487 let input = json!({
488 "addressId": "addr_123",
489 "street": null,
490 "city": null,
491 "zip": null
492 });
493 let rules = vec![InputObjectRule::RequiredIfAbsent {
494 absent_field: "addressId".to_string(),
495 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
496 }];
497 let result = validate_input_object(&input, &rules, None);
498 result.unwrap_or_else(|e| {
499 panic!("required_if_absent should skip when absent_field is present: {e}")
500 });
501 }
502
503 #[test]
504 fn test_multiple_rules_all_pass() {
505 let input = json!({
506 "entityId": "123",
507 "entityPayload": null,
508 "isPremium": true,
509 "paymentMethod": "credit_card"
510 });
511 let rules = vec![
512 InputObjectRule::OneOf {
513 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
514 },
515 InputObjectRule::ConditionalRequired {
516 if_field: "isPremium".to_string(),
517 then_fields: vec!["paymentMethod".to_string()],
518 },
519 ];
520 let result = validate_input_object(&input, &rules, None);
521 result.unwrap_or_else(|e| panic!("multiple rules should all pass: {e}"));
522 }
523
524 #[test]
525 fn test_multiple_rules_one_fails() {
526 let input = json!({
527 "entityId": "123",
528 "entityPayload": null,
529 "isPremium": true,
530 "paymentMethod": null
531 });
532 let rules = vec![
533 InputObjectRule::OneOf {
534 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
535 },
536 InputObjectRule::ConditionalRequired {
537 if_field: "isPremium".to_string(),
538 then_fields: vec!["paymentMethod".to_string()],
539 },
540 ];
541 let result = validate_input_object(&input, &rules, None);
542 assert!(
543 matches!(result, Err(FraiseQLError::Validation { .. })),
544 "expected Validation error when one rule fails, got: {result:?}"
545 );
546 }
547
548 #[test]
549 fn test_multiple_rules_both_fail() {
550 let input = json!({
551 "entityId": "123",
552 "entityPayload": { "name": "test" },
553 "isPremium": true,
554 "paymentMethod": null
555 });
556 let rules = vec![
557 InputObjectRule::OneOf {
558 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
559 },
560 InputObjectRule::ConditionalRequired {
561 if_field: "isPremium".to_string(),
562 then_fields: vec!["paymentMethod".to_string()],
563 },
564 ];
565 let result = validate_input_object(&input, &rules, None);
566 assert!(
567 matches!(result, Err(FraiseQLError::Validation { ref message, .. })
568 if message.contains("Exactly one") || message.contains("must also be provided")),
569 "expected aggregated Validation error with both failures, got: {result:?}"
570 );
571 }
572
573 #[test]
574 fn test_error_aggregation() {
575 let input = json!({
576 "entityId": "123",
577 "entityPayload": { "name": "test" },
578 "isPremium": true,
579 "paymentMethod": null
580 });
581 let rules = vec![
582 InputObjectRule::OneOf {
583 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
584 },
585 InputObjectRule::ConditionalRequired {
586 if_field: "isPremium".to_string(),
587 then_fields: vec!["paymentMethod".to_string()],
588 },
589 ];
590
591 let result = validate_input_object(&input, &rules, Some("createInput"));
592 match result {
593 Err(FraiseQLError::Validation {
594 ref message,
595 ref path,
596 }) => {
597 assert_eq!(*path, Some("createInput".to_string()));
598 assert!(message.contains("failed"), "expected 'failed' in message, got: {message}");
599 },
600 other => panic!("expected Validation error with custom path, got: {other:?}"),
601 }
602 }
603
604 #[test]
605 fn test_conditional_required_multiple_fields() {
606 let input = json!({
607 "isInternational": true,
608 "customsCode": "ABC123",
609 "importDuties": "50.00"
610 });
611 let rules = vec![InputObjectRule::ConditionalRequired {
612 if_field: "isInternational".to_string(),
613 then_fields: vec!["customsCode".to_string(), "importDuties".to_string()],
614 }];
615 let result = validate_input_object(&input, &rules, None);
616 result.unwrap_or_else(|e| {
617 panic!("conditional_required with multiple fields should pass: {e}")
618 });
619 }
620
621 #[test]
622 fn test_conditional_required_multiple_fields_one_missing() {
623 let input = json!({
624 "isInternational": true,
625 "customsCode": "ABC123",
626 "importDuties": null
627 });
628 let rules = vec![InputObjectRule::ConditionalRequired {
629 if_field: "isInternational".to_string(),
630 then_fields: vec!["customsCode".to_string(), "importDuties".to_string()],
631 }];
632 let result = validate_input_object(&input, &rules, None);
633 assert!(
634 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("must also be provided")),
635 "expected Validation error about missing conditional field, got: {result:?}"
636 );
637 }
638
639 #[test]
640 fn test_validation_result_aggregation() {
641 let mut result = InputObjectValidationResult::new();
642 assert!(!result.has_errors());
643 assert_eq!(result.error_count, 0);
644
645 result.add_error("Error 1".to_string());
646 assert!(result.has_errors());
647 assert_eq!(result.error_count, 1);
648
649 result.add_errors(vec!["Error 2".to_string(), "Error 3".to_string()]);
650 assert_eq!(result.error_count, 3);
651 }
652
653 #[test]
654 fn test_validation_result_into_result_success() {
655 let result = InputObjectValidationResult::new();
656 result
657 .into_result()
658 .unwrap_or_else(|e| panic!("empty result should be Ok: {e}"));
659 }
660
661 #[test]
662 fn test_validation_result_into_result_failure() {
663 let mut result = InputObjectValidationResult::new();
664 result.add_error("Test error".to_string());
665 let outcome = result.into_result();
666 assert!(
667 matches!(outcome, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("Test error")),
668 "expected Validation error containing 'Test error', got: {outcome:?}"
669 );
670 }
671
672 #[test]
673 fn test_non_object_input() {
674 let input = json!([1, 2, 3]);
675 let rules = vec![InputObjectRule::AnyOf {
676 fields: vec!["field".to_string()],
677 }];
678 let result = validate_input_object(&input, &rules, None);
679 assert!(
680 matches!(result, Err(FraiseQLError::Validation { ref message, .. }) if message.contains("must be an object")),
681 "expected Validation error about non-object input, got: {result:?}"
682 );
683 }
684
685 #[test]
686 fn test_empty_rules() {
687 let input = json!({"field": "value"});
688 let rules: Vec<InputObjectRule> = vec![];
689 let result = validate_input_object(&input, &rules, None);
690 result.unwrap_or_else(|e| panic!("empty rules should always pass: {e}"));
691 }
692
693 #[test]
694 fn test_custom_validator_not_implemented() {
695 let input = json!({"field": "value"});
696 let rules = vec![InputObjectRule::Custom {
697 name: "myValidator".to_string(),
698 }];
699 let result = validate_input_object(&input, &rules, None);
700 match result {
701 Err(FraiseQLError::Validation { ref message, .. }) => {
702 assert!(
703 message.contains("myValidator"),
704 "expected 'myValidator' in message, got: {message}"
705 );
706 assert!(
707 message.contains("InputValidatorRegistry"),
708 "expected 'InputValidatorRegistry' in message, got: {message}"
709 );
710 },
711 other => {
712 panic!("expected Validation error about unregistered validator, got: {other:?}")
713 },
714 }
715 }
716
717 #[test]
718 fn test_complex_create_or_reference_pattern() {
719 let input = json!({
721 "entityId": "123",
722 "name": null,
723 "description": null
724 });
725 let rules = vec![InputObjectRule::OneOf {
726 fields: vec!["entityId".to_string(), "name".to_string()],
727 }];
728 let result = validate_input_object(&input, &rules, None);
729 result.unwrap_or_else(|e| {
730 panic!("create_or_reference pattern should pass with entityId: {e}")
731 });
732 }
733
734 #[test]
735 fn test_complex_address_pattern() {
736 let input = json!({
738 "addressId": null,
739 "street": "123 Main St",
740 "city": "Springfield",
741 "zip": "12345"
742 });
743 let rules = vec![InputObjectRule::RequiredIfAbsent {
744 absent_field: "addressId".to_string(),
745 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
746 }];
747 let result = validate_input_object(&input, &rules, None);
748 result.unwrap_or_else(|e| {
749 panic!("address pattern should pass with all fields provided: {e}")
750 });
751 }
752}