1use serde_json::Value;
28
29use crate::error::{FraiseQLError, Result};
30
31#[derive(Debug, Clone)]
33pub enum InputObjectRule {
34 AnyOf { fields: Vec<String> },
36 OneOf { fields: Vec<String> },
38 ConditionalRequired {
40 if_field: String,
41 then_fields: Vec<String>,
42 },
43 RequiredIfAbsent {
45 absent_field: String,
46 then_fields: Vec<String>,
47 },
48 Custom { name: String },
50}
51
52#[derive(Debug, Clone, Default)]
54pub struct InputObjectValidationResult {
55 pub errors: Vec<String>,
57 pub error_count: usize,
59}
60
61impl InputObjectValidationResult {
62 pub fn new() -> Self {
64 Self {
65 errors: Vec::new(),
66 error_count: 0,
67 }
68 }
69
70 pub fn add_error(&mut self, error: String) {
72 self.errors.push(error);
73 self.error_count += 1;
74 }
75
76 pub fn add_errors(&mut self, errors: Vec<String>) {
78 self.error_count += errors.len();
79 self.errors.extend(errors);
80 }
81
82 pub fn has_errors(&self) -> bool {
84 !self.errors.is_empty()
85 }
86
87 pub fn into_result(self) -> Result<()> {
89 self.into_result_with_path("input")
90 }
91
92 pub fn into_result_with_path(self, path: &str) -> Result<()> {
94 if self.has_errors() {
95 Err(FraiseQLError::Validation {
96 message: format!("Input object validation failed: {}", self.errors.join("; ")),
97 path: Some(path.to_string()),
98 })
99 } else {
100 Ok(())
101 }
102 }
103}
104
105pub fn validate_input_object(
118 input: &Value,
119 rules: &[InputObjectRule],
120 object_path: Option<&str>,
121) -> Result<()> {
122 let mut result = InputObjectValidationResult::new();
123 let path = object_path.unwrap_or("input");
124
125 if !matches!(input, Value::Object(_)) {
126 return Err(FraiseQLError::Validation {
127 message: "Input must be an object".to_string(),
128 path: Some(path.to_string()),
129 });
130 }
131
132 for rule in rules {
133 if let Err(FraiseQLError::Validation { message, .. }) = validate_rule(input, rule, path) {
134 result.add_error(message);
135 }
136 }
137
138 result.into_result_with_path(path)
139}
140
141fn validate_rule(input: &Value, rule: &InputObjectRule, path: &str) -> Result<()> {
143 match rule {
144 InputObjectRule::AnyOf { fields } => validate_any_of(input, fields, path),
145 InputObjectRule::OneOf { fields } => validate_one_of(input, fields, path),
146 InputObjectRule::ConditionalRequired {
147 if_field,
148 then_fields,
149 } => validate_conditional_required(input, if_field, then_fields, path),
150 InputObjectRule::RequiredIfAbsent {
151 absent_field,
152 then_fields,
153 } => validate_required_if_absent(input, absent_field, then_fields, path),
154 InputObjectRule::Custom { name } => Err(FraiseQLError::Validation {
155 message: format!("Custom validator '{}' not implemented", name),
156 path: Some(path.to_string()),
157 }),
158 }
159}
160
161fn validate_any_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
163 if let Value::Object(obj) = input {
164 let has_any = fields
165 .iter()
166 .any(|name| obj.get(name).map(|v| !matches!(v, Value::Null)).unwrap_or(false));
167
168 if !has_any {
169 return Err(FraiseQLError::Validation {
170 message: format!("At least one of [{}] must be provided", fields.join(", ")),
171 path: Some(path.to_string()),
172 });
173 }
174 }
175
176 Ok(())
177}
178
179fn validate_one_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
181 if let Value::Object(obj) = input {
182 let present_count = fields
183 .iter()
184 .filter(|name| obj.get(*name).map(|v| !matches!(v, Value::Null)).unwrap_or(false))
185 .count();
186
187 if present_count != 1 {
188 return Err(FraiseQLError::Validation {
189 message: format!(
190 "Exactly one of [{}] must be provided, but {} {} provided",
191 fields.join(", "),
192 present_count,
193 if present_count == 1 { "was" } else { "were" }
194 ),
195 path: Some(path.to_string()),
196 });
197 }
198 }
199
200 Ok(())
201}
202
203fn validate_conditional_required(
205 input: &Value,
206 if_field: &str,
207 then_fields: &[String],
208 path: &str,
209) -> Result<()> {
210 if let Value::Object(obj) = input {
211 let condition_met = obj.get(if_field).map(|v| !matches!(v, Value::Null)).unwrap_or(false);
212
213 if condition_met {
214 let missing_fields: Vec<&String> = then_fields
215 .iter()
216 .filter(|name| obj.get(*name).map(|v| matches!(v, Value::Null)).unwrap_or(true))
217 .collect();
218
219 if !missing_fields.is_empty() {
220 return Err(FraiseQLError::Validation {
221 message: format!(
222 "Since '{}' is provided, {} must also be provided",
223 if_field,
224 missing_fields
225 .iter()
226 .map(|s| format!("'{}'", s))
227 .collect::<Vec<_>>()
228 .join(", ")
229 ),
230 path: Some(path.to_string()),
231 });
232 }
233 }
234 }
235
236 Ok(())
237}
238
239fn validate_required_if_absent(
241 input: &Value,
242 absent_field: &str,
243 then_fields: &[String],
244 path: &str,
245) -> Result<()> {
246 if let Value::Object(obj) = input {
247 let field_absent = obj.get(absent_field).map(|v| matches!(v, Value::Null)).unwrap_or(true);
248
249 if field_absent {
250 let missing_fields: Vec<&String> = then_fields
251 .iter()
252 .filter(|name| obj.get(*name).map(|v| matches!(v, Value::Null)).unwrap_or(true))
253 .collect();
254
255 if !missing_fields.is_empty() {
256 return Err(FraiseQLError::Validation {
257 message: format!(
258 "Since '{}' is not provided, {} must be provided",
259 absent_field,
260 missing_fields
261 .iter()
262 .map(|s| format!("'{}'", s))
263 .collect::<Vec<_>>()
264 .join(", ")
265 ),
266 path: Some(path.to_string()),
267 });
268 }
269 }
270 }
271
272 Ok(())
273}
274
275#[cfg(test)]
276mod tests {
277 use serde_json::json;
278
279 use super::*;
280
281 #[test]
282 fn test_any_of_passes() {
283 let input = json!({
284 "email": "user@example.com",
285 "phone": null,
286 "address": null
287 });
288 let rules = vec![InputObjectRule::AnyOf {
289 fields: vec![
290 "email".to_string(),
291 "phone".to_string(),
292 "address".to_string(),
293 ],
294 }];
295 let result = validate_input_object(&input, &rules, None);
296 assert!(result.is_ok());
297 }
298
299 #[test]
300 fn test_any_of_fails() {
301 let input = json!({
302 "email": null,
303 "phone": null,
304 "address": null
305 });
306 let rules = vec![InputObjectRule::AnyOf {
307 fields: vec![
308 "email".to_string(),
309 "phone".to_string(),
310 "address".to_string(),
311 ],
312 }];
313 let result = validate_input_object(&input, &rules, None);
314 assert!(result.is_err());
315 }
316
317 #[test]
318 fn test_one_of_passes() {
319 let input = json!({
320 "entityId": "123",
321 "entityPayload": null
322 });
323 let rules = vec![InputObjectRule::OneOf {
324 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
325 }];
326 let result = validate_input_object(&input, &rules, None);
327 assert!(result.is_ok());
328 }
329
330 #[test]
331 fn test_one_of_fails_both_present() {
332 let input = json!({
333 "entityId": "123",
334 "entityPayload": { "name": "test" }
335 });
336 let rules = vec![InputObjectRule::OneOf {
337 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
338 }];
339 let result = validate_input_object(&input, &rules, None);
340 assert!(result.is_err());
341 }
342
343 #[test]
344 fn test_one_of_fails_neither_present() {
345 let input = json!({
346 "entityId": null,
347 "entityPayload": null
348 });
349 let rules = vec![InputObjectRule::OneOf {
350 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
351 }];
352 let result = validate_input_object(&input, &rules, None);
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_conditional_required_passes() {
358 let input = json!({
359 "isPremium": true,
360 "paymentMethod": "credit_card"
361 });
362 let rules = vec![InputObjectRule::ConditionalRequired {
363 if_field: "isPremium".to_string(),
364 then_fields: vec!["paymentMethod".to_string()],
365 }];
366 let result = validate_input_object(&input, &rules, None);
367 assert!(result.is_ok());
368 }
369
370 #[test]
371 fn test_conditional_required_fails() {
372 let input = json!({
373 "isPremium": true,
374 "paymentMethod": null
375 });
376 let rules = vec![InputObjectRule::ConditionalRequired {
377 if_field: "isPremium".to_string(),
378 then_fields: vec!["paymentMethod".to_string()],
379 }];
380 let result = validate_input_object(&input, &rules, None);
381 assert!(result.is_err());
382 }
383
384 #[test]
385 fn test_conditional_required_skips_when_condition_false() {
386 let input = json!({
387 "isPremium": null,
388 "paymentMethod": null
389 });
390 let rules = vec![InputObjectRule::ConditionalRequired {
391 if_field: "isPremium".to_string(),
392 then_fields: vec!["paymentMethod".to_string()],
393 }];
394 let result = validate_input_object(&input, &rules, None);
395 assert!(result.is_ok());
396 }
397
398 #[test]
399 fn test_required_if_absent_passes() {
400 let input = json!({
401 "addressId": null,
402 "street": "123 Main St",
403 "city": "Springfield",
404 "zip": "12345"
405 });
406 let rules = vec![InputObjectRule::RequiredIfAbsent {
407 absent_field: "addressId".to_string(),
408 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
409 }];
410 let result = validate_input_object(&input, &rules, None);
411 assert!(result.is_ok());
412 }
413
414 #[test]
415 fn test_required_if_absent_fails() {
416 let input = json!({
417 "addressId": null,
418 "street": "123 Main St",
419 "city": null,
420 "zip": "12345"
421 });
422 let rules = vec![InputObjectRule::RequiredIfAbsent {
423 absent_field: "addressId".to_string(),
424 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
425 }];
426 let result = validate_input_object(&input, &rules, None);
427 assert!(result.is_err());
428 }
429
430 #[test]
431 fn test_required_if_absent_skips_when_field_present() {
432 let input = json!({
433 "addressId": "addr_123",
434 "street": null,
435 "city": null,
436 "zip": null
437 });
438 let rules = vec![InputObjectRule::RequiredIfAbsent {
439 absent_field: "addressId".to_string(),
440 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
441 }];
442 let result = validate_input_object(&input, &rules, None);
443 assert!(result.is_ok());
444 }
445
446 #[test]
447 fn test_multiple_rules_all_pass() {
448 let input = json!({
449 "entityId": "123",
450 "entityPayload": null,
451 "isPremium": true,
452 "paymentMethod": "credit_card"
453 });
454 let rules = vec![
455 InputObjectRule::OneOf {
456 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
457 },
458 InputObjectRule::ConditionalRequired {
459 if_field: "isPremium".to_string(),
460 then_fields: vec!["paymentMethod".to_string()],
461 },
462 ];
463 let result = validate_input_object(&input, &rules, None);
464 assert!(result.is_ok());
465 }
466
467 #[test]
468 fn test_multiple_rules_one_fails() {
469 let input = json!({
470 "entityId": "123",
471 "entityPayload": null,
472 "isPremium": true,
473 "paymentMethod": null
474 });
475 let rules = vec![
476 InputObjectRule::OneOf {
477 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
478 },
479 InputObjectRule::ConditionalRequired {
480 if_field: "isPremium".to_string(),
481 then_fields: vec!["paymentMethod".to_string()],
482 },
483 ];
484 let result = validate_input_object(&input, &rules, None);
485 assert!(result.is_err());
486 }
487
488 #[test]
489 fn test_multiple_rules_both_fail() {
490 let input = json!({
491 "entityId": "123",
492 "entityPayload": { "name": "test" },
493 "isPremium": true,
494 "paymentMethod": null
495 });
496 let rules = vec![
497 InputObjectRule::OneOf {
498 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
499 },
500 InputObjectRule::ConditionalRequired {
501 if_field: "isPremium".to_string(),
502 then_fields: vec!["paymentMethod".to_string()],
503 },
504 ];
505 let result = validate_input_object(&input, &rules, None);
506 assert!(result.is_err());
507 if let Err(FraiseQLError::Validation { message, .. }) = result {
508 assert!(message.contains("Exactly one") || message.contains("must also be provided"));
510 }
511 }
512
513 #[test]
514 fn test_error_aggregation() {
515 let input = json!({
516 "entityId": "123",
517 "entityPayload": { "name": "test" },
518 "isPremium": true,
519 "paymentMethod": null
520 });
521 let rules = vec![
522 InputObjectRule::OneOf {
523 fields: vec!["entityId".to_string(), "entityPayload".to_string()],
524 },
525 InputObjectRule::ConditionalRequired {
526 if_field: "isPremium".to_string(),
527 then_fields: vec!["paymentMethod".to_string()],
528 },
529 ];
530
531 let result = validate_input_object(&input, &rules, Some("createInput"));
532 assert!(result.is_err());
533 if let Err(FraiseQLError::Validation { message, path }) = result {
534 assert_eq!(path, Some("createInput".to_string()));
535 assert!(message.contains("failed"));
536 }
537 }
538
539 #[test]
540 fn test_conditional_required_multiple_fields() {
541 let input = json!({
542 "isInternational": true,
543 "customsCode": "ABC123",
544 "importDuties": "50.00"
545 });
546 let rules = vec![InputObjectRule::ConditionalRequired {
547 if_field: "isInternational".to_string(),
548 then_fields: vec!["customsCode".to_string(), "importDuties".to_string()],
549 }];
550 let result = validate_input_object(&input, &rules, None);
551 assert!(result.is_ok());
552 }
553
554 #[test]
555 fn test_conditional_required_multiple_fields_one_missing() {
556 let input = json!({
557 "isInternational": true,
558 "customsCode": "ABC123",
559 "importDuties": null
560 });
561 let rules = vec![InputObjectRule::ConditionalRequired {
562 if_field: "isInternational".to_string(),
563 then_fields: vec!["customsCode".to_string(), "importDuties".to_string()],
564 }];
565 let result = validate_input_object(&input, &rules, None);
566 assert!(result.is_err());
567 }
568
569 #[test]
570 fn test_validation_result_aggregation() {
571 let mut result = InputObjectValidationResult::new();
572 assert!(!result.has_errors());
573 assert_eq!(result.error_count, 0);
574
575 result.add_error("Error 1".to_string());
576 assert!(result.has_errors());
577 assert_eq!(result.error_count, 1);
578
579 result.add_errors(vec!["Error 2".to_string(), "Error 3".to_string()]);
580 assert_eq!(result.error_count, 3);
581 }
582
583 #[test]
584 fn test_validation_result_into_result_success() {
585 let result = InputObjectValidationResult::new();
586 assert!(result.into_result().is_ok());
587 }
588
589 #[test]
590 fn test_validation_result_into_result_failure() {
591 let mut result = InputObjectValidationResult::new();
592 result.add_error("Test error".to_string());
593 assert!(result.into_result().is_err());
594 }
595
596 #[test]
597 fn test_non_object_input() {
598 let input = json!([1, 2, 3]);
599 let rules = vec![InputObjectRule::AnyOf {
600 fields: vec!["field".to_string()],
601 }];
602 let result = validate_input_object(&input, &rules, None);
603 assert!(result.is_err());
604 }
605
606 #[test]
607 fn test_empty_rules() {
608 let input = json!({"field": "value"});
609 let rules: Vec<InputObjectRule> = vec![];
610 let result = validate_input_object(&input, &rules, None);
611 assert!(result.is_ok());
612 }
613
614 #[test]
615 fn test_custom_validator_not_implemented() {
616 let input = json!({"field": "value"});
617 let rules = vec![InputObjectRule::Custom {
618 name: "myValidator".to_string(),
619 }];
620 let result = validate_input_object(&input, &rules, None);
621 assert!(result.is_err());
622 if let Err(FraiseQLError::Validation { message, .. }) = result {
623 assert!(message.contains("not implemented"));
624 }
625 }
626
627 #[test]
628 fn test_complex_create_or_reference_pattern() {
629 let input = json!({
631 "entityId": "123",
632 "name": null,
633 "description": null
634 });
635 let rules = vec![InputObjectRule::OneOf {
636 fields: vec!["entityId".to_string(), "name".to_string()],
637 }];
638 let result = validate_input_object(&input, &rules, None);
639 assert!(result.is_ok());
640 }
641
642 #[test]
643 fn test_complex_address_pattern() {
644 let input = json!({
646 "addressId": null,
647 "street": "123 Main St",
648 "city": "Springfield",
649 "zip": "12345"
650 });
651 let rules = vec![InputObjectRule::RequiredIfAbsent {
652 absent_field: "addressId".to_string(),
653 then_fields: vec!["street".to_string(), "city".to_string(), "zip".to_string()],
654 }];
655 let result = validate_input_object(&input, &rules, None);
656 assert!(result.is_ok());
657 }
658}