1use std::collections::HashSet;
6
7use anyhow::Result;
8use tracing::{debug, info};
9
10use super::intermediate::IntermediateSchema;
11
12#[derive(Debug, Clone)]
14pub struct ValidationError {
15 pub message: String,
17 pub path: String,
19 pub severity: ErrorSeverity,
21 pub suggestion: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ErrorSeverity {
28 Error,
30 Warning,
32}
33
34pub struct SchemaValidator;
36
37impl SchemaValidator {
38 pub fn validate(schema: &IntermediateSchema) -> Result<ValidationReport> {
40 info!("Validating schema structure");
41
42 let mut report = ValidationReport::default();
43
44 let mut type_names = HashSet::new();
46 for type_def in &schema.types {
47 if type_names.contains(&type_def.name) {
48 report.errors.push(ValidationError {
49 message: format!("Duplicate type name: '{}'", type_def.name),
50 path: format!("types[{}].name", type_names.len()),
51 severity: ErrorSeverity::Error,
52 suggestion: Some("Type names must be unique".to_string()),
53 });
54 }
55 type_names.insert(type_def.name.clone());
56 }
57
58 type_names.insert("Int".to_string());
60 type_names.insert("Float".to_string());
61 type_names.insert("String".to_string());
62 type_names.insert("Boolean".to_string());
63 type_names.insert("ID".to_string());
64
65 let mut query_names = HashSet::new();
67 for (idx, query) in schema.queries.iter().enumerate() {
68 debug!("Validating query: {}", query.name);
69
70 if query_names.contains(&query.name) {
72 report.errors.push(ValidationError {
73 message: format!("Duplicate query name: '{}'", query.name),
74 path: format!("queries[{idx}].name"),
75 severity: ErrorSeverity::Error,
76 suggestion: Some("Query names must be unique".to_string()),
77 });
78 }
79 query_names.insert(query.name.clone());
80
81 if !type_names.contains(&query.return_type) {
83 report.errors.push(ValidationError {
84 message: format!(
85 "Query '{}' references unknown type '{}'",
86 query.name, query.return_type
87 ),
88 path: format!("queries[{idx}].return_type"),
89 severity: ErrorSeverity::Error,
90 suggestion: Some(format!(
91 "Available types: {}",
92 Self::suggest_similar_type(&query.return_type, &type_names)
93 )),
94 });
95 }
96
97 for (arg_idx, arg) in query.arguments.iter().enumerate() {
99 if !type_names.contains(&arg.arg_type) {
100 report.errors.push(ValidationError {
101 message: format!(
102 "Query '{}' argument '{}' references unknown type '{}'",
103 query.name, arg.name, arg.arg_type
104 ),
105 path: format!("queries[{idx}].arguments[{arg_idx}].type"),
106 severity: ErrorSeverity::Error,
107 suggestion: Some(format!(
108 "Available types: {}",
109 Self::suggest_similar_type(&arg.arg_type, &type_names)
110 )),
111 });
112 }
113 }
114
115 if query.sql_source.is_none() && query.returns_list {
117 report.errors.push(ValidationError {
118 message: format!(
119 "Query '{}' returns a list but has no sql_source",
120 query.name
121 ),
122 path: format!("queries[{idx}]"),
123 severity: ErrorSeverity::Warning,
124 suggestion: Some("Add sql_source for SQL-backed queries".to_string()),
125 });
126 }
127 }
128
129 let mut mutation_names = HashSet::new();
131 for (idx, mutation) in schema.mutations.iter().enumerate() {
132 debug!("Validating mutation: {}", mutation.name);
133
134 if mutation_names.contains(&mutation.name) {
136 report.errors.push(ValidationError {
137 message: format!("Duplicate mutation name: '{}'", mutation.name),
138 path: format!("mutations[{idx}].name"),
139 severity: ErrorSeverity::Error,
140 suggestion: Some("Mutation names must be unique".to_string()),
141 });
142 }
143 mutation_names.insert(mutation.name.clone());
144
145 if !type_names.contains(&mutation.return_type) {
147 report.errors.push(ValidationError {
148 message: format!(
149 "Mutation '{}' references unknown type '{}'",
150 mutation.name, mutation.return_type
151 ),
152 path: format!("mutations[{idx}].return_type"),
153 severity: ErrorSeverity::Error,
154 suggestion: Some(format!(
155 "Available types: {}",
156 Self::suggest_similar_type(&mutation.return_type, &type_names)
157 )),
158 });
159 }
160
161 for (arg_idx, arg) in mutation.arguments.iter().enumerate() {
163 if !type_names.contains(&arg.arg_type) {
164 report.errors.push(ValidationError {
165 message: format!(
166 "Mutation '{}' argument '{}' references unknown type '{}'",
167 mutation.name, arg.name, arg.arg_type
168 ),
169 path: format!("mutations[{idx}].arguments[{arg_idx}].type"),
170 severity: ErrorSeverity::Error,
171 suggestion: Some(format!(
172 "Available types: {}",
173 Self::suggest_similar_type(&arg.arg_type, &type_names)
174 )),
175 });
176 }
177 }
178 }
179
180 if let Some(observers) = &schema.observers {
182 let mut observer_names = HashSet::new();
183 for (idx, observer) in observers.iter().enumerate() {
184 debug!("Validating observer: {}", observer.name);
185
186 if observer_names.contains(&observer.name) {
188 report.errors.push(ValidationError {
189 message: format!("Duplicate observer name: '{}'", observer.name),
190 path: format!("observers[{idx}].name"),
191 severity: ErrorSeverity::Error,
192 suggestion: Some("Observer names must be unique".to_string()),
193 });
194 }
195 observer_names.insert(observer.name.clone());
196
197 if !type_names.contains(&observer.entity) {
199 report.errors.push(ValidationError {
200 message: format!(
201 "Observer '{}' references unknown entity '{}'",
202 observer.name, observer.entity
203 ),
204 path: format!("observers[{idx}].entity"),
205 severity: ErrorSeverity::Error,
206 suggestion: Some(format!(
207 "Available types: {}",
208 Self::suggest_similar_type(&observer.entity, &type_names)
209 )),
210 });
211 }
212
213 let valid_events = ["INSERT", "UPDATE", "DELETE"];
215 if !valid_events.contains(&observer.event.as_str()) {
216 report.errors.push(ValidationError {
217 message: format!(
218 "Observer '{}' has invalid event '{}'. Must be INSERT, UPDATE, or DELETE",
219 observer.name, observer.event
220 ),
221 path: format!("observers[{idx}].event"),
222 severity: ErrorSeverity::Error,
223 suggestion: Some("Valid events: INSERT, UPDATE, DELETE".to_string()),
224 });
225 }
226
227 if observer.actions.is_empty() {
229 report.errors.push(ValidationError {
230 message: format!(
231 "Observer '{}' must have at least one action",
232 observer.name
233 ),
234 path: format!("observers[{idx}].actions"),
235 severity: ErrorSeverity::Error,
236 suggestion: Some("Add a webhook, slack, or email action".to_string()),
237 });
238 }
239
240 for (action_idx, action) in observer.actions.iter().enumerate() {
242 if let Some(obj) = action.as_object() {
243 if let Some(action_type) = obj.get("type").and_then(|v| v.as_str()) {
245 let valid_action_types = ["webhook", "slack", "email"];
246 if !valid_action_types.contains(&action_type) {
247 report.errors.push(ValidationError {
248 message: format!(
249 "Observer '{}' action {} has invalid type '{}'",
250 observer.name, action_idx, action_type
251 ),
252 path: format!(
253 "observers[{idx}].actions[{action_idx}].type"
254 ),
255 severity: ErrorSeverity::Error,
256 suggestion: Some(
257 "Valid action types: webhook, slack, email".to_string(),
258 ),
259 });
260 }
261
262 match action_type {
264 "webhook" => {
265 let has_url = obj.contains_key("url");
266 let has_url_env = obj.contains_key("url_env");
267 if !has_url && !has_url_env {
268 report.errors.push(ValidationError {
269 message: format!(
270 "Observer '{}' webhook action must have 'url' or 'url_env'",
271 observer.name
272 ),
273 path: format!("observers[{idx}].actions[{action_idx}]"),
274 severity: ErrorSeverity::Error,
275 suggestion: Some("Add 'url' or 'url_env' field".to_string()),
276 });
277 }
278 },
279 "slack" => {
280 if !obj.contains_key("channel") {
281 report.errors.push(ValidationError {
282 message: format!(
283 "Observer '{}' slack action must have 'channel' field",
284 observer.name
285 ),
286 path: format!("observers[{idx}].actions[{action_idx}]"),
287 severity: ErrorSeverity::Error,
288 suggestion: Some("Add 'channel' field (e.g., '#sales')".to_string()),
289 });
290 }
291 if !obj.contains_key("message") {
292 report.errors.push(ValidationError {
293 message: format!(
294 "Observer '{}' slack action must have 'message' field",
295 observer.name
296 ),
297 path: format!("observers[{idx}].actions[{action_idx}]"),
298 severity: ErrorSeverity::Error,
299 suggestion: Some("Add 'message' field".to_string()),
300 });
301 }
302 },
303 "email" => {
304 let required_fields = ["to", "subject", "body"];
305 for field in &required_fields {
306 if !obj.contains_key(*field) {
307 report.errors.push(ValidationError {
308 message: format!(
309 "Observer '{}' email action must have '{}' field",
310 observer.name, field
311 ),
312 path: format!("observers[{idx}].actions[{action_idx}]"),
313 severity: ErrorSeverity::Error,
314 suggestion: Some(format!("Add '{field}' field")),
315 });
316 }
317 }
318 },
319 _ => {},
320 }
321 } else {
322 report.errors.push(ValidationError {
323 message: format!(
324 "Observer '{}' action {} missing 'type' field",
325 observer.name, action_idx
326 ),
327 path: format!("observers[{idx}].actions[{action_idx}]"),
328 severity: ErrorSeverity::Error,
329 suggestion: Some(
330 "Add 'type' field (webhook, slack, or email)".to_string(),
331 ),
332 });
333 }
334 } else {
335 report.errors.push(ValidationError {
336 message: format!(
337 "Observer '{}' action {} must be an object",
338 observer.name, action_idx
339 ),
340 path: format!("observers[{idx}].actions[{action_idx}]"),
341 severity: ErrorSeverity::Error,
342 suggestion: None,
343 });
344 }
345 }
346
347 let valid_backoff_strategies = ["exponential", "linear", "fixed"];
349 if !valid_backoff_strategies.contains(&observer.retry.backoff_strategy.as_str()) {
350 report.errors.push(ValidationError {
351 message: format!(
352 "Observer '{}' has invalid backoff_strategy '{}'",
353 observer.name, observer.retry.backoff_strategy
354 ),
355 path: format!("observers[{idx}].retry.backoff_strategy"),
356 severity: ErrorSeverity::Error,
357 suggestion: Some(
358 "Valid strategies: exponential, linear, fixed".to_string(),
359 ),
360 });
361 }
362
363 if observer.retry.max_attempts == 0 {
364 report.errors.push(ValidationError {
365 message: format!(
366 "Observer '{}' has max_attempts=0, actions will never execute",
367 observer.name
368 ),
369 path: format!("observers[{idx}].retry.max_attempts"),
370 severity: ErrorSeverity::Warning,
371 suggestion: Some("Set max_attempts >= 1".to_string()),
372 });
373 }
374
375 if observer.retry.initial_delay_ms == 0 {
376 report.errors.push(ValidationError {
377 message: format!(
378 "Observer '{}' has initial_delay_ms=0, retries will be immediate",
379 observer.name
380 ),
381 path: format!("observers[{idx}].retry.initial_delay_ms"),
382 severity: ErrorSeverity::Warning,
383 suggestion: Some("Consider setting initial_delay_ms > 0".to_string()),
384 });
385 }
386
387 if observer.retry.max_delay_ms < observer.retry.initial_delay_ms {
388 report.errors.push(ValidationError {
389 message: format!(
390 "Observer '{}' has max_delay_ms < initial_delay_ms",
391 observer.name
392 ),
393 path: format!("observers[{idx}].retry.max_delay_ms"),
394 severity: ErrorSeverity::Error,
395 suggestion: Some("max_delay_ms must be >= initial_delay_ms".to_string()),
396 });
397 }
398 }
399 }
400
401 info!(
402 "Validation complete: {} errors, {} warnings",
403 report.error_count(),
404 report.warning_count()
405 );
406
407 Ok(report)
408 }
409
410 fn suggest_similar_type(typo: &str, available: &HashSet<String>) -> String {
412 let similar: Vec<&String> = available
414 .iter()
415 .filter(|name| {
416 name.to_lowercase().starts_with(&typo[0..1].to_lowercase())
417 || typo.to_lowercase().starts_with(&name[0..1].to_lowercase())
418 })
419 .take(3)
420 .collect();
421
422 if similar.is_empty() {
423 available.iter().take(5).cloned().collect::<Vec<_>>().join(", ")
424 } else {
425 similar.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
426 }
427 }
428}
429
430#[derive(Debug, Default)]
432pub struct ValidationReport {
433 pub errors: Vec<ValidationError>,
435}
436
437impl ValidationReport {
438 pub fn is_valid(&self) -> bool {
440 !self.has_errors()
441 }
442
443 pub fn has_errors(&self) -> bool {
445 self.errors.iter().any(|e| e.severity == ErrorSeverity::Error)
446 }
447
448 pub fn error_count(&self) -> usize {
450 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Error).count()
451 }
452
453 pub fn warning_count(&self) -> usize {
455 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Warning).count()
456 }
457
458 pub fn print(&self) {
460 if self.errors.is_empty() {
461 return;
462 }
463
464 println!("\nš Validation Report:");
465
466 let errors: Vec<_> =
467 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Error).collect();
468
469 let warnings: Vec<_> =
470 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Warning).collect();
471
472 if !errors.is_empty() {
473 println!("\n ā Errors ({}):", errors.len());
474 for error in errors {
475 println!(" {}", error.message);
476 println!(" at: {}", error.path);
477 if let Some(suggestion) = &error.suggestion {
478 println!(" š” {suggestion}");
479 }
480 println!();
481 }
482 }
483
484 if !warnings.is_empty() {
485 println!("\n ā ļø Warnings ({}):", warnings.len());
486 for warning in warnings {
487 println!(" {}", warning.message);
488 println!(" at: {}", warning.path);
489 if let Some(suggestion) = &warning.suggestion {
490 println!(" š” {suggestion}");
491 }
492 println!();
493 }
494 }
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::schema::intermediate::{IntermediateQuery, IntermediateType};
502
503 #[test]
504 fn test_validate_empty_schema() {
505 let schema = IntermediateSchema {
506 security: None,
507 version: "2.0.0".to_string(),
508 types: vec![],
509 enums: vec![],
510 input_types: vec![],
511 interfaces: vec![],
512 unions: vec![],
513 queries: vec![],
514 mutations: vec![],
515 subscriptions: vec![],
516 fragments: None,
517 directives: None,
518 fact_tables: None,
519 aggregate_queries: None,
520 observers: None,
521 custom_scalars: None,
522 };
523
524 let report = SchemaValidator::validate(&schema).unwrap();
525 assert!(report.is_valid());
526 }
527
528 #[test]
529 fn test_detect_unknown_return_type() {
530 let schema = IntermediateSchema {
531 security: None,
532 version: "2.0.0".to_string(),
533 types: vec![],
534 enums: vec![],
535 input_types: vec![],
536 interfaces: vec![],
537 unions: vec![],
538 queries: vec![IntermediateQuery {
539 name: "users".to_string(),
540 return_type: "UnknownType".to_string(),
541 returns_list: true,
542 nullable: false,
543 arguments: vec![],
544 description: None,
545 sql_source: Some("users".to_string()),
546 auto_params: None,
547 deprecated: None,
548 jsonb_column: None,
549 }],
550 mutations: vec![],
551 subscriptions: vec![],
552 fragments: None,
553 directives: None,
554 fact_tables: None,
555 aggregate_queries: None,
556 observers: None,
557 custom_scalars: None,
558 };
559
560 let report = SchemaValidator::validate(&schema).unwrap();
561 assert!(!report.is_valid());
562 assert_eq!(report.error_count(), 1);
563 assert!(report.errors[0].message.contains("unknown type 'UnknownType'"));
564 }
565
566 #[test]
567 fn test_detect_duplicate_query_names() {
568 let schema = IntermediateSchema {
569 security: None,
570 version: "2.0.0".to_string(),
571 types: vec![IntermediateType {
572 name: "User".to_string(),
573 fields: vec![],
574 description: None,
575 implements: vec![],
576 }],
577 enums: vec![],
578 input_types: vec![],
579 interfaces: vec![],
580 unions: vec![],
581 queries: vec![
582 IntermediateQuery {
583 name: "users".to_string(),
584 return_type: "User".to_string(),
585 returns_list: true,
586 nullable: false,
587 arguments: vec![],
588 description: None,
589 sql_source: Some("users".to_string()),
590 auto_params: None,
591 deprecated: None,
592 jsonb_column: None,
593 },
594 IntermediateQuery {
595 name: "users".to_string(), return_type: "User".to_string(),
597 returns_list: true,
598 nullable: false,
599 arguments: vec![],
600 description: None,
601 sql_source: Some("users".to_string()),
602 auto_params: None,
603 deprecated: None,
604 jsonb_column: None,
605 },
606 ],
607 mutations: vec![],
608 subscriptions: vec![],
609 fragments: None,
610 directives: None,
611 fact_tables: None,
612 aggregate_queries: None,
613 observers: None,
614 custom_scalars: None,
615 };
616
617 let report = SchemaValidator::validate(&schema).unwrap();
618 assert!(!report.is_valid());
619 assert!(report.errors.iter().any(|e| e.message.contains("Duplicate query name")));
620 }
621
622 #[test]
623 fn test_warning_for_query_without_sql_source() {
624 let schema = IntermediateSchema {
625 security: None,
626 version: "2.0.0".to_string(),
627 types: vec![IntermediateType {
628 name: "User".to_string(),
629 fields: vec![],
630 description: None,
631 implements: vec![],
632 }],
633 enums: vec![],
634 input_types: vec![],
635 interfaces: vec![],
636 unions: vec![],
637 queries: vec![IntermediateQuery {
638 name: "users".to_string(),
639 return_type: "User".to_string(),
640 returns_list: true,
641 nullable: false,
642 arguments: vec![],
643 description: None,
644 sql_source: None, auto_params: None,
646 deprecated: None,
647 jsonb_column: None,
648 }],
649 mutations: vec![],
650 subscriptions: vec![],
651 fragments: None,
652 directives: None,
653 fact_tables: None,
654 aggregate_queries: None,
655 observers: None,
656 custom_scalars: None,
657 };
658
659 let report = SchemaValidator::validate(&schema).unwrap();
660 assert!(report.is_valid()); assert_eq!(report.warning_count(), 1);
662 assert!(report.errors[0].message.contains("no sql_source"));
663 }
664
665 #[test]
666 fn test_valid_observer() {
667 use serde_json::json;
668
669 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
670
671 let schema = IntermediateSchema {
672 security: None,
673 version: "2.0.0".to_string(),
674 types: vec![IntermediateType {
675 name: "Order".to_string(),
676 fields: vec![],
677 description: None,
678 implements: vec![],
679 }],
680 enums: vec![],
681 input_types: vec![],
682 interfaces: vec![],
683 unions: vec![],
684 queries: vec![],
685 mutations: vec![],
686 subscriptions: vec![],
687 fragments: None,
688 directives: None,
689 fact_tables: None,
690 aggregate_queries: None,
691 observers: Some(vec![IntermediateObserver {
692 name: "onOrderCreated".to_string(),
693 entity: "Order".to_string(),
694 event: "INSERT".to_string(),
695 actions: vec![json!({
696 "type": "webhook",
697 "url": "https://example.com/orders"
698 })],
699 condition: None,
700 retry: IntermediateRetryConfig {
701 max_attempts: 3,
702 backoff_strategy: "exponential".to_string(),
703 initial_delay_ms: 100,
704 max_delay_ms: 60000,
705 },
706 }]),
707 custom_scalars: None,
708 };
709
710 let report = SchemaValidator::validate(&schema).unwrap();
711 assert!(report.is_valid(), "Valid observer should pass validation");
712 assert_eq!(report.error_count(), 0);
713 }
714
715 #[test]
716 fn test_observer_with_unknown_entity() {
717 use serde_json::json;
718
719 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
720
721 let schema = IntermediateSchema {
722 security: None,
723 version: "2.0.0".to_string(),
724 types: vec![],
725 enums: vec![],
726 input_types: vec![],
727 interfaces: vec![],
728 unions: vec![],
729 queries: vec![],
730 mutations: vec![],
731 subscriptions: vec![],
732 fragments: None,
733 directives: None,
734 fact_tables: None,
735 aggregate_queries: None,
736 observers: Some(vec![IntermediateObserver {
737 name: "onOrderCreated".to_string(),
738 entity: "UnknownEntity".to_string(),
739 event: "INSERT".to_string(),
740 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
741 condition: None,
742 retry: IntermediateRetryConfig {
743 max_attempts: 3,
744 backoff_strategy: "exponential".to_string(),
745 initial_delay_ms: 100,
746 max_delay_ms: 60000,
747 },
748 }]),
749 custom_scalars: None,
750 };
751
752 let report = SchemaValidator::validate(&schema).unwrap();
753 assert!(!report.is_valid());
754 assert!(report.errors.iter().any(|e| e.message.contains("unknown entity")));
755 }
756
757 #[test]
758 fn test_observer_with_invalid_event() {
759 use serde_json::json;
760
761 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
762
763 let schema = IntermediateSchema {
764 security: None,
765 version: "2.0.0".to_string(),
766 types: vec![IntermediateType {
767 name: "Order".to_string(),
768 fields: vec![],
769 description: None,
770 implements: vec![],
771 }],
772 enums: vec![],
773 input_types: vec![],
774 interfaces: vec![],
775 unions: vec![],
776 queries: vec![],
777 mutations: vec![],
778 subscriptions: vec![],
779 fragments: None,
780 directives: None,
781 fact_tables: None,
782 aggregate_queries: None,
783 observers: Some(vec![IntermediateObserver {
784 name: "onOrderCreated".to_string(),
785 entity: "Order".to_string(),
786 event: "INVALID_EVENT".to_string(),
787 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
788 condition: None,
789 retry: IntermediateRetryConfig {
790 max_attempts: 3,
791 backoff_strategy: "exponential".to_string(),
792 initial_delay_ms: 100,
793 max_delay_ms: 60000,
794 },
795 }]),
796 custom_scalars: None,
797 };
798
799 let report = SchemaValidator::validate(&schema).unwrap();
800 assert!(!report.is_valid());
801 assert!(report.errors.iter().any(|e| e.message.contains("invalid event")));
802 }
803
804 #[test]
805 fn test_observer_with_invalid_action_type() {
806 use serde_json::json;
807
808 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
809
810 let schema = IntermediateSchema {
811 security: None,
812 version: "2.0.0".to_string(),
813 types: vec![IntermediateType {
814 name: "Order".to_string(),
815 fields: vec![],
816 description: None,
817 implements: vec![],
818 }],
819 enums: vec![],
820 input_types: vec![],
821 interfaces: vec![],
822 unions: vec![],
823 queries: vec![],
824 mutations: vec![],
825 subscriptions: vec![],
826 fragments: None,
827 directives: None,
828 fact_tables: None,
829 aggregate_queries: None,
830 observers: Some(vec![IntermediateObserver {
831 name: "onOrderCreated".to_string(),
832 entity: "Order".to_string(),
833 event: "INSERT".to_string(),
834 actions: vec![json!({"type": "invalid_action"})],
835 condition: None,
836 retry: IntermediateRetryConfig {
837 max_attempts: 3,
838 backoff_strategy: "exponential".to_string(),
839 initial_delay_ms: 100,
840 max_delay_ms: 60000,
841 },
842 }]),
843 custom_scalars: None,
844 };
845
846 let report = SchemaValidator::validate(&schema).unwrap();
847 assert!(!report.is_valid());
848 assert!(report.errors.iter().any(|e| e.message.contains("invalid type")));
849 }
850
851 #[test]
852 fn test_observer_with_invalid_retry_config() {
853 use serde_json::json;
854
855 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
856
857 let schema = IntermediateSchema {
858 security: None,
859 version: "2.0.0".to_string(),
860 types: vec![IntermediateType {
861 name: "Order".to_string(),
862 fields: vec![],
863 description: None,
864 implements: vec![],
865 }],
866 enums: vec![],
867 input_types: vec![],
868 interfaces: vec![],
869 unions: vec![],
870 queries: vec![],
871 mutations: vec![],
872 subscriptions: vec![],
873 fragments: None,
874 directives: None,
875 fact_tables: None,
876 aggregate_queries: None,
877 observers: Some(vec![IntermediateObserver {
878 name: "onOrderCreated".to_string(),
879 entity: "Order".to_string(),
880 event: "INSERT".to_string(),
881 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
882 condition: None,
883 retry: IntermediateRetryConfig {
884 max_attempts: 3,
885 backoff_strategy: "invalid_strategy".to_string(),
886 initial_delay_ms: 100,
887 max_delay_ms: 60000,
888 },
889 }]),
890 custom_scalars: None,
891 };
892
893 let report = SchemaValidator::validate(&schema).unwrap();
894 assert!(!report.is_valid());
895 assert!(report.errors.iter().any(|e| e.message.contains("invalid backoff_strategy")));
896 }
897}