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 #[allow(dead_code)]
420 pub fn validate_federation(
421 metadata: &fraiseql_core::federation::types::FederationMetadata,
422 ) -> Result<()> {
423 use fraiseql_core::federation::DependencyGraph;
424
425 if !metadata.enabled {
427 return Ok(());
428 }
429
430 for federated_type in &metadata.types {
432 for (field_name, directives) in &federated_type.field_directives {
433 for required in &directives.requires {
435 if required.path.is_empty() {
438 return Err(anyhow::anyhow!(
439 "Invalid @requires on {}.{}: empty field path",
440 federated_type.name,
441 field_name
442 ));
443 }
444 }
445
446 for provided in &directives.provides {
448 if provided.path.is_empty() {
449 return Err(anyhow::anyhow!(
450 "Invalid @provides on {}.{}: empty field path",
451 federated_type.name,
452 field_name
453 ));
454 }
455 }
456
457 if directives.external && !federated_type.is_extends {
459 return Err(anyhow::anyhow!(
460 "@external field {}.{} can only appear on @extends types",
461 federated_type.name,
462 field_name
463 ));
464 }
465 }
466 }
467
468 let graph = DependencyGraph::build(metadata)
470 .map_err(|e| anyhow::anyhow!("Failed to build dependency graph: {e}"))?;
471
472 let cycles = graph.detect_cycles();
473 if !cycles.is_empty() {
474 return Err(anyhow::anyhow!("Circular @requires dependencies detected: {cycles:?}"));
475 }
476
477 info!("Federation metadata validation passed");
478 Ok(())
479 }
480
481 fn suggest_similar_type(typo: &str, available: &HashSet<String>) -> String {
483 let similar: Vec<&String> = available
485 .iter()
486 .filter(|name| {
487 name.to_lowercase().starts_with(&typo[0..1].to_lowercase())
488 || typo.to_lowercase().starts_with(&name[0..1].to_lowercase())
489 })
490 .take(3)
491 .collect();
492
493 if similar.is_empty() {
494 available.iter().take(5).cloned().collect::<Vec<_>>().join(", ")
495 } else {
496 similar.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
497 }
498 }
499}
500
501#[derive(Debug, Default)]
503pub struct ValidationReport {
504 pub errors: Vec<ValidationError>,
506}
507
508impl ValidationReport {
509 pub fn is_valid(&self) -> bool {
511 !self.has_errors()
512 }
513
514 pub fn has_errors(&self) -> bool {
516 self.errors.iter().any(|e| e.severity == ErrorSeverity::Error)
517 }
518
519 pub fn error_count(&self) -> usize {
521 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Error).count()
522 }
523
524 pub fn warning_count(&self) -> usize {
526 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Warning).count()
527 }
528
529 pub fn print(&self) {
531 if self.errors.is_empty() {
532 return;
533 }
534
535 println!("\nš Validation Report:");
536
537 let errors: Vec<_> =
538 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Error).collect();
539
540 let warnings: Vec<_> =
541 self.errors.iter().filter(|e| e.severity == ErrorSeverity::Warning).collect();
542
543 if !errors.is_empty() {
544 println!("\n ā Errors ({}):", errors.len());
545 for error in errors {
546 println!(" {}", error.message);
547 println!(" at: {}", error.path);
548 if let Some(suggestion) = &error.suggestion {
549 println!(" š” {suggestion}");
550 }
551 println!();
552 }
553 }
554
555 if !warnings.is_empty() {
556 println!("\n ā ļø Warnings ({}):", warnings.len());
557 for warning in warnings {
558 println!(" {}", warning.message);
559 println!(" at: {}", warning.path);
560 if let Some(suggestion) = &warning.suggestion {
561 println!(" š” {suggestion}");
562 }
563 println!();
564 }
565 }
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use crate::schema::intermediate::{IntermediateQuery, IntermediateType};
573
574 #[test]
575 fn test_validate_empty_schema() {
576 let schema = IntermediateSchema {
577 security: None,
578 version: "2.0.0".to_string(),
579 types: vec![],
580 enums: vec![],
581 input_types: vec![],
582 interfaces: vec![],
583 unions: vec![],
584 queries: vec![],
585 mutations: vec![],
586 subscriptions: vec![],
587 fragments: None,
588 directives: None,
589 fact_tables: None,
590 aggregate_queries: None,
591 observers: None,
592 };
593
594 let report = SchemaValidator::validate(&schema).unwrap();
595 assert!(report.is_valid());
596 }
597
598 #[test]
599 fn test_detect_unknown_return_type() {
600 let schema = IntermediateSchema {
601 security: None,
602 version: "2.0.0".to_string(),
603 types: vec![],
604 enums: vec![],
605 input_types: vec![],
606 interfaces: vec![],
607 unions: vec![],
608 queries: vec![IntermediateQuery {
609 name: "users".to_string(),
610 return_type: "UnknownType".to_string(),
611 returns_list: true,
612 nullable: false,
613 arguments: vec![],
614 description: None,
615 sql_source: Some("users".to_string()),
616 auto_params: None,
617 deprecated: None,
618 }],
619 mutations: vec![],
620 subscriptions: vec![],
621 fragments: None,
622 directives: None,
623 fact_tables: None,
624 aggregate_queries: None,
625 observers: None,
626 };
627
628 let report = SchemaValidator::validate(&schema).unwrap();
629 assert!(!report.is_valid());
630 assert_eq!(report.error_count(), 1);
631 assert!(report.errors[0].message.contains("unknown type 'UnknownType'"));
632 }
633
634 #[test]
635 fn test_detect_duplicate_query_names() {
636 let schema = IntermediateSchema {
637 security: None,
638 version: "2.0.0".to_string(),
639 types: vec![IntermediateType {
640 name: "User".to_string(),
641 fields: vec![],
642 description: None,
643 implements: vec![],
644 }],
645 enums: vec![],
646 input_types: vec![],
647 interfaces: vec![],
648 unions: vec![],
649 queries: vec![
650 IntermediateQuery {
651 name: "users".to_string(),
652 return_type: "User".to_string(),
653 returns_list: true,
654 nullable: false,
655 arguments: vec![],
656 description: None,
657 sql_source: Some("users".to_string()),
658 auto_params: None,
659 deprecated: None,
660 },
661 IntermediateQuery {
662 name: "users".to_string(), return_type: "User".to_string(),
664 returns_list: true,
665 nullable: false,
666 arguments: vec![],
667 description: None,
668 sql_source: Some("users".to_string()),
669 auto_params: None,
670 deprecated: None,
671 },
672 ],
673 mutations: vec![],
674 subscriptions: vec![],
675 fragments: None,
676 directives: None,
677 fact_tables: None,
678 aggregate_queries: None,
679 observers: None,
680 };
681
682 let report = SchemaValidator::validate(&schema).unwrap();
683 assert!(!report.is_valid());
684 assert!(report.errors.iter().any(|e| e.message.contains("Duplicate query name")));
685 }
686
687 #[test]
688 fn test_warning_for_query_without_sql_source() {
689 let schema = IntermediateSchema {
690 security: None,
691 version: "2.0.0".to_string(),
692 types: vec![IntermediateType {
693 name: "User".to_string(),
694 fields: vec![],
695 description: None,
696 implements: vec![],
697 }],
698 enums: vec![],
699 input_types: vec![],
700 interfaces: vec![],
701 unions: vec![],
702 queries: vec![IntermediateQuery {
703 name: "users".to_string(),
704 return_type: "User".to_string(),
705 returns_list: true,
706 nullable: false,
707 arguments: vec![],
708 description: None,
709 sql_source: None, auto_params: None,
711 deprecated: None,
712 }],
713 mutations: vec![],
714 subscriptions: vec![],
715 fragments: None,
716 directives: None,
717 fact_tables: None,
718 aggregate_queries: None,
719 observers: None,
720 };
721
722 let report = SchemaValidator::validate(&schema).unwrap();
723 assert!(report.is_valid()); assert_eq!(report.warning_count(), 1);
725 assert!(report.errors[0].message.contains("no sql_source"));
726 }
727
728 #[test]
729 fn test_valid_observer() {
730 use serde_json::json;
731
732 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
733
734 let schema = IntermediateSchema {
735 security: None,
736 version: "2.0.0".to_string(),
737 types: vec![IntermediateType {
738 name: "Order".to_string(),
739 fields: vec![],
740 description: None,
741 implements: vec![],
742 }],
743 enums: vec![],
744 input_types: vec![],
745 interfaces: vec![],
746 unions: vec![],
747 queries: vec![],
748 mutations: vec![],
749 subscriptions: vec![],
750 fragments: None,
751 directives: None,
752 fact_tables: None,
753 aggregate_queries: None,
754 observers: Some(vec![IntermediateObserver {
755 name: "onOrderCreated".to_string(),
756 entity: "Order".to_string(),
757 event: "INSERT".to_string(),
758 actions: vec![json!({
759 "type": "webhook",
760 "url": "https://example.com/orders"
761 })],
762 condition: None,
763 retry: IntermediateRetryConfig {
764 max_attempts: 3,
765 backoff_strategy: "exponential".to_string(),
766 initial_delay_ms: 100,
767 max_delay_ms: 60000,
768 },
769 }]),
770 };
771
772 let report = SchemaValidator::validate(&schema).unwrap();
773 assert!(report.is_valid(), "Valid observer should pass validation");
774 assert_eq!(report.error_count(), 0);
775 }
776
777 #[test]
778 fn test_observer_with_unknown_entity() {
779 use serde_json::json;
780
781 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
782
783 let schema = IntermediateSchema {
784 security: None,
785 version: "2.0.0".to_string(),
786 types: vec![],
787 enums: vec![],
788 input_types: vec![],
789 interfaces: vec![],
790 unions: vec![],
791 queries: vec![],
792 mutations: vec![],
793 subscriptions: vec![],
794 fragments: None,
795 directives: None,
796 fact_tables: None,
797 aggregate_queries: None,
798 observers: Some(vec![IntermediateObserver {
799 name: "onOrderCreated".to_string(),
800 entity: "UnknownEntity".to_string(),
801 event: "INSERT".to_string(),
802 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
803 condition: None,
804 retry: IntermediateRetryConfig {
805 max_attempts: 3,
806 backoff_strategy: "exponential".to_string(),
807 initial_delay_ms: 100,
808 max_delay_ms: 60000,
809 },
810 }]),
811 };
812
813 let report = SchemaValidator::validate(&schema).unwrap();
814 assert!(!report.is_valid());
815 assert!(report.errors.iter().any(|e| e.message.contains("unknown entity")));
816 }
817
818 #[test]
819 fn test_observer_with_invalid_event() {
820 use serde_json::json;
821
822 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
823
824 let schema = IntermediateSchema {
825 security: None,
826 version: "2.0.0".to_string(),
827 types: vec![IntermediateType {
828 name: "Order".to_string(),
829 fields: vec![],
830 description: None,
831 implements: vec![],
832 }],
833 enums: vec![],
834 input_types: vec![],
835 interfaces: vec![],
836 unions: vec![],
837 queries: vec![],
838 mutations: vec![],
839 subscriptions: vec![],
840 fragments: None,
841 directives: None,
842 fact_tables: None,
843 aggregate_queries: None,
844 observers: Some(vec![IntermediateObserver {
845 name: "onOrderCreated".to_string(),
846 entity: "Order".to_string(),
847 event: "INVALID_EVENT".to_string(),
848 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
849 condition: None,
850 retry: IntermediateRetryConfig {
851 max_attempts: 3,
852 backoff_strategy: "exponential".to_string(),
853 initial_delay_ms: 100,
854 max_delay_ms: 60000,
855 },
856 }]),
857 };
858
859 let report = SchemaValidator::validate(&schema).unwrap();
860 assert!(!report.is_valid());
861 assert!(report.errors.iter().any(|e| e.message.contains("invalid event")));
862 }
863
864 #[test]
865 fn test_observer_with_invalid_action_type() {
866 use serde_json::json;
867
868 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
869
870 let schema = IntermediateSchema {
871 security: None,
872 version: "2.0.0".to_string(),
873 types: vec![IntermediateType {
874 name: "Order".to_string(),
875 fields: vec![],
876 description: None,
877 implements: vec![],
878 }],
879 enums: vec![],
880 input_types: vec![],
881 interfaces: vec![],
882 unions: vec![],
883 queries: vec![],
884 mutations: vec![],
885 subscriptions: vec![],
886 fragments: None,
887 directives: None,
888 fact_tables: None,
889 aggregate_queries: None,
890 observers: Some(vec![IntermediateObserver {
891 name: "onOrderCreated".to_string(),
892 entity: "Order".to_string(),
893 event: "INSERT".to_string(),
894 actions: vec![json!({"type": "invalid_action"})],
895 condition: None,
896 retry: IntermediateRetryConfig {
897 max_attempts: 3,
898 backoff_strategy: "exponential".to_string(),
899 initial_delay_ms: 100,
900 max_delay_ms: 60000,
901 },
902 }]),
903 };
904
905 let report = SchemaValidator::validate(&schema).unwrap();
906 assert!(!report.is_valid());
907 assert!(report.errors.iter().any(|e| e.message.contains("invalid type")));
908 }
909
910 #[test]
911 fn test_observer_with_invalid_retry_config() {
912 use serde_json::json;
913
914 use super::super::intermediate::{IntermediateObserver, IntermediateRetryConfig};
915
916 let schema = IntermediateSchema {
917 security: None,
918 version: "2.0.0".to_string(),
919 types: vec![IntermediateType {
920 name: "Order".to_string(),
921 fields: vec![],
922 description: None,
923 implements: vec![],
924 }],
925 enums: vec![],
926 input_types: vec![],
927 interfaces: vec![],
928 unions: vec![],
929 queries: vec![],
930 mutations: vec![],
931 subscriptions: vec![],
932 fragments: None,
933 directives: None,
934 fact_tables: None,
935 aggregate_queries: None,
936 observers: Some(vec![IntermediateObserver {
937 name: "onOrderCreated".to_string(),
938 entity: "Order".to_string(),
939 event: "INSERT".to_string(),
940 actions: vec![json!({"type": "webhook", "url": "https://example.com"})],
941 condition: None,
942 retry: IntermediateRetryConfig {
943 max_attempts: 3,
944 backoff_strategy: "invalid_strategy".to_string(),
945 initial_delay_ms: 100,
946 max_delay_ms: 60000,
947 },
948 }]),
949 };
950
951 let report = SchemaValidator::validate(&schema).unwrap();
952 assert!(!report.is_valid());
953 assert!(report.errors.iter().any(|e| e.message.contains("invalid backoff_strategy")));
954 }
955}