1pub mod advanced_types;
7pub mod analytics;
8pub mod fragments;
9pub mod operations;
10pub mod subscriptions;
11pub mod types;
12
13pub use advanced_types::{
14 IntermediateInputField, IntermediateInputObject, IntermediateInterface, IntermediateUnion,
15};
16pub use analytics::{
17 IntermediateAggregateQuery, IntermediateDimensionPath, IntermediateDimensions,
18 IntermediateFactTable, IntermediateFilter, IntermediateMeasure,
19};
20pub use fragments::{
21 IntermediateAppliedDirective, IntermediateDirective, IntermediateFragment,
22 IntermediateFragmentField, IntermediateFragmentFieldDef,
23};
24use fraiseql_core::schema::{DebugConfig, McpConfig, SubscriptionsConfig, ValidationConfig};
25pub use operations::{
26 IntermediateArgument, IntermediateAutoParams, IntermediateMutation, IntermediateQuery,
27 IntermediateQueryDefaults,
28};
29use serde::{Deserialize, Serialize};
30pub use subscriptions::{
31 IntermediateFilterCondition, IntermediateObserver, IntermediateObserverAction,
32 IntermediateRetryConfig, IntermediateSubscription, IntermediateSubscriptionFilter,
33};
34pub use types::{
35 IntermediateDeprecation, IntermediateEnum, IntermediateEnumValue, IntermediateField,
36 IntermediateScalar, IntermediateType,
37};
38
39#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
41pub struct IntermediateSchema {
42 #[serde(default = "default_version")]
44 pub version: String,
45
46 #[serde(default)]
48 pub types: Vec<IntermediateType>,
49
50 #[serde(default)]
52 pub enums: Vec<IntermediateEnum>,
53
54 #[serde(default)]
56 pub input_types: Vec<IntermediateInputObject>,
57
58 #[serde(default)]
60 pub interfaces: Vec<IntermediateInterface>,
61
62 #[serde(default)]
64 pub unions: Vec<IntermediateUnion>,
65
66 #[serde(default)]
68 pub queries: Vec<IntermediateQuery>,
69
70 #[serde(default)]
72 pub mutations: Vec<IntermediateMutation>,
73
74 #[serde(default)]
76 pub subscriptions: Vec<IntermediateSubscription>,
77
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub fragments: Option<Vec<IntermediateFragment>>,
81
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub directives: Option<Vec<IntermediateDirective>>,
85
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub fact_tables: Option<Vec<IntermediateFactTable>>,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub aggregate_queries: Option<Vec<IntermediateAggregateQuery>>,
93
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub observers: Option<Vec<IntermediateObserver>>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub custom_scalars: Option<Vec<IntermediateScalar>>,
105
106 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub security: Option<serde_json::Value>,
111
112 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub observers_config: Option<serde_json::Value>,
118
119 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub federation_config: Option<serde_json::Value>,
125
126 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub subscriptions_config: Option<SubscriptionsConfig>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub validation_config: Option<ValidationConfig>,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub debug_config: Option<DebugConfig>,
146
147 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub mcp_config: Option<McpConfig>,
153
154 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub query_defaults: Option<IntermediateQueryDefaults>,
160}
161
162fn default_version() -> String {
163 "2.0.0".to_string()
164}
165
166#[cfg(test)]
167#[allow(clippy::unwrap_used)] mod tests {
169 use super::*;
170
171 #[test]
172 fn test_parse_minimal_schema() {
173 let json = r#"{
174 "types": [],
175 "queries": [],
176 "mutations": []
177 }"#;
178
179 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
180 assert_eq!(schema.version, "2.0.0");
181 assert_eq!(schema.types.len(), 0);
182 assert_eq!(schema.queries.len(), 0);
183 assert_eq!(schema.mutations.len(), 0);
184 }
185
186 #[test]
187 fn test_parse_type_with_type_field() {
188 let json = r#"{
189 "types": [{
190 "name": "User",
191 "fields": [
192 {
193 "name": "id",
194 "type": "Int",
195 "nullable": false
196 },
197 {
198 "name": "name",
199 "type": "String",
200 "nullable": false
201 }
202 ]
203 }],
204 "queries": [],
205 "mutations": []
206 }"#;
207
208 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
209 assert_eq!(schema.types.len(), 1);
210 assert_eq!(schema.types[0].name, "User");
211 assert_eq!(schema.types[0].fields.len(), 2);
212 assert_eq!(schema.types[0].fields[0].name, "id");
213 assert_eq!(schema.types[0].fields[0].field_type, "Int");
214 assert!(!schema.types[0].fields[0].nullable);
215 }
216
217 #[test]
218 fn test_parse_query_with_arguments() {
219 let json = r#"{
220 "types": [],
221 "queries": [{
222 "name": "users",
223 "return_type": "User",
224 "returns_list": true,
225 "nullable": false,
226 "arguments": [
227 {
228 "name": "limit",
229 "type": "Int",
230 "nullable": false,
231 "default": 10
232 }
233 ],
234 "sql_source": "v_user"
235 }],
236 "mutations": []
237 }"#;
238
239 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
240 assert_eq!(schema.queries.len(), 1);
241 assert_eq!(schema.queries[0].arguments.len(), 1);
242 assert_eq!(schema.queries[0].arguments[0].arg_type, "Int");
243 assert_eq!(schema.queries[0].arguments[0].default, Some(serde_json::json!(10)));
244 }
245
246 #[test]
247 fn test_parse_fragment_simple() {
248 let json = r#"{
249 "types": [],
250 "queries": [],
251 "mutations": [],
252 "fragments": [{
253 "name": "UserFields",
254 "on": "User",
255 "fields": ["id", "name", "email"]
256 }]
257 }"#;
258
259 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
260 assert!(schema.fragments.is_some());
261 let fragments = schema.fragments.unwrap();
262 assert_eq!(fragments.len(), 1);
263 assert_eq!(fragments[0].name, "UserFields");
264 assert_eq!(fragments[0].type_condition, "User");
265 assert_eq!(fragments[0].fields.len(), 3);
266
267 match &fragments[0].fields[0] {
269 IntermediateFragmentField::Simple(name) => assert_eq!(name, "id"),
270 IntermediateFragmentField::Complex(_) => panic!("Expected simple field"),
271 }
272 }
273
274 #[test]
275 fn test_parse_fragment_with_nested_fields() {
276 let json = r#"{
277 "types": [],
278 "queries": [],
279 "mutations": [],
280 "fragments": [{
281 "name": "PostFields",
282 "on": "Post",
283 "fields": [
284 "id",
285 "title",
286 {
287 "name": "author",
288 "alias": "writer",
289 "fields": ["id", "name"]
290 }
291 ]
292 }]
293 }"#;
294
295 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
296 let fragments = schema.fragments.unwrap();
297 assert_eq!(fragments[0].fields.len(), 3);
298
299 match &fragments[0].fields[2] {
301 IntermediateFragmentField::Complex(def) => {
302 assert_eq!(def.name, "author");
303 assert_eq!(def.alias, Some("writer".to_string()));
304 assert!(def.fields.is_some());
305 assert_eq!(def.fields.as_ref().unwrap().len(), 2);
306 },
307 IntermediateFragmentField::Simple(_) => panic!("Expected complex field"),
308 }
309 }
310
311 #[test]
312 fn test_parse_directive_definition() {
313 let json = r#"{
314 "types": [],
315 "queries": [],
316 "mutations": [],
317 "directives": [{
318 "name": "auth",
319 "locations": ["FIELD_DEFINITION", "OBJECT"],
320 "arguments": [
321 {"name": "role", "type": "String", "nullable": false}
322 ],
323 "description": "Requires authentication"
324 }]
325 }"#;
326
327 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
328 assert!(schema.directives.is_some());
329 let directives = schema.directives.unwrap();
330 assert_eq!(directives.len(), 1);
331 assert_eq!(directives[0].name, "auth");
332 assert_eq!(directives[0].locations, vec!["FIELD_DEFINITION", "OBJECT"]);
333 assert_eq!(directives[0].arguments.len(), 1);
334 assert_eq!(directives[0].description, Some("Requires authentication".to_string()));
335 }
336
337 #[test]
338 fn test_parse_field_with_directive() {
339 let json = r#"{
340 "types": [{
341 "name": "User",
342 "fields": [
343 {
344 "name": "oldId",
345 "type": "Int",
346 "nullable": false,
347 "directives": [
348 {"name": "deprecated", "arguments": {"reason": "Use 'id' instead"}}
349 ]
350 }
351 ]
352 }],
353 "queries": [],
354 "mutations": []
355 }"#;
356
357 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
358 let field = &schema.types[0].fields[0];
359 assert_eq!(field.name, "oldId");
360 assert!(field.directives.is_some());
361 let directives = field.directives.as_ref().unwrap();
362 assert_eq!(directives.len(), 1);
363 assert_eq!(directives[0].name, "deprecated");
364 assert_eq!(
365 directives[0].arguments,
366 Some(serde_json::json!({"reason": "Use 'id' instead"}))
367 );
368 }
369
370 #[test]
371 fn test_parse_fragment_with_spread() {
372 let json = r#"{
373 "types": [],
374 "queries": [],
375 "mutations": [],
376 "fragments": [
377 {
378 "name": "UserFields",
379 "on": "User",
380 "fields": ["id", "name"]
381 },
382 {
383 "name": "PostWithAuthor",
384 "on": "Post",
385 "fields": [
386 "id",
387 "title",
388 {
389 "name": "author",
390 "spread": "UserFields"
391 }
392 ]
393 }
394 ]
395 }"#;
396
397 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
398 let fragments = schema.fragments.unwrap();
399 assert_eq!(fragments.len(), 2);
400
401 match &fragments[1].fields[2] {
403 IntermediateFragmentField::Complex(def) => {
404 assert_eq!(def.name, "author");
405 assert_eq!(def.spread, Some("UserFields".to_string()));
406 },
407 IntermediateFragmentField::Simple(_) => panic!("Expected complex field"),
408 }
409 }
410
411 #[test]
412 fn test_parse_enum() {
413 let json = r#"{
414 "types": [],
415 "queries": [],
416 "mutations": [],
417 "enums": [{
418 "name": "OrderStatus",
419 "values": [
420 {"name": "PENDING"},
421 {"name": "PROCESSING", "description": "Currently being processed"},
422 {"name": "SHIPPED"},
423 {"name": "DELIVERED"}
424 ],
425 "description": "Possible states of an order"
426 }]
427 }"#;
428
429 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
430 assert_eq!(schema.enums.len(), 1);
431 let enum_def = &schema.enums[0];
432 assert_eq!(enum_def.name, "OrderStatus");
433 assert_eq!(enum_def.description, Some("Possible states of an order".to_string()));
434 assert_eq!(enum_def.values.len(), 4);
435 assert_eq!(enum_def.values[0].name, "PENDING");
436 assert_eq!(enum_def.values[1].description, Some("Currently being processed".to_string()));
437 }
438
439 #[test]
440 fn test_parse_enum_with_deprecated_value() {
441 let json = r#"{
442 "types": [],
443 "queries": [],
444 "mutations": [],
445 "enums": [{
446 "name": "UserRole",
447 "values": [
448 {"name": "ADMIN"},
449 {"name": "USER"},
450 {"name": "GUEST", "deprecated": {"reason": "Use USER with limited permissions instead"}}
451 ]
452 }]
453 }"#;
454
455 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
456 let enum_def = &schema.enums[0];
457 assert_eq!(enum_def.values.len(), 3);
458
459 let guest = &enum_def.values[2];
461 assert_eq!(guest.name, "GUEST");
462 assert!(guest.deprecated.is_some());
463 assert_eq!(
464 guest.deprecated.as_ref().unwrap().reason,
465 Some("Use USER with limited permissions instead".to_string())
466 );
467 }
468
469 #[test]
470 fn test_parse_input_object() {
471 let json = r#"{
472 "types": [],
473 "queries": [],
474 "mutations": [],
475 "input_types": [{
476 "name": "UserFilter",
477 "fields": [
478 {"name": "name", "type": "String", "nullable": true},
479 {"name": "email", "type": "String", "nullable": true},
480 {"name": "active", "type": "Boolean", "nullable": true, "default": true}
481 ],
482 "description": "Filter criteria for users"
483 }]
484 }"#;
485
486 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
487 assert_eq!(schema.input_types.len(), 1);
488 let input = &schema.input_types[0];
489 assert_eq!(input.name, "UserFilter");
490 assert_eq!(input.description, Some("Filter criteria for users".to_string()));
491 assert_eq!(input.fields.len(), 3);
492
493 assert_eq!(input.fields[0].name, "name");
495 assert_eq!(input.fields[0].field_type, "String");
496 assert!(input.fields[0].nullable);
497
498 assert_eq!(input.fields[2].name, "active");
500 assert_eq!(input.fields[2].default, Some(serde_json::json!(true)));
501 }
502
503 #[test]
504 fn test_parse_interface() {
505 let json = r#"{
506 "types": [],
507 "queries": [],
508 "mutations": [],
509 "interfaces": [{
510 "name": "Node",
511 "fields": [
512 {"name": "id", "type": "ID", "nullable": false}
513 ],
514 "description": "An object with a globally unique ID"
515 }]
516 }"#;
517
518 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
519 assert_eq!(schema.interfaces.len(), 1);
520 let interface = &schema.interfaces[0];
521 assert_eq!(interface.name, "Node");
522 assert_eq!(interface.description, Some("An object with a globally unique ID".to_string()));
523 assert_eq!(interface.fields.len(), 1);
524 assert_eq!(interface.fields[0].name, "id");
525 assert_eq!(interface.fields[0].field_type, "ID");
526 assert!(!interface.fields[0].nullable);
527 }
528
529 #[test]
530 fn test_parse_type_implements_interface() {
531 let json = r#"{
532 "types": [{
533 "name": "User",
534 "fields": [
535 {"name": "id", "type": "ID", "nullable": false},
536 {"name": "name", "type": "String", "nullable": false}
537 ],
538 "implements": ["Node"]
539 }],
540 "queries": [],
541 "mutations": [],
542 "interfaces": [{
543 "name": "Node",
544 "fields": [
545 {"name": "id", "type": "ID", "nullable": false}
546 ]
547 }]
548 }"#;
549
550 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
551 assert_eq!(schema.types.len(), 1);
552 assert_eq!(schema.types[0].name, "User");
553 assert_eq!(schema.types[0].implements, vec!["Node"]);
554
555 assert_eq!(schema.interfaces.len(), 1);
556 assert_eq!(schema.interfaces[0].name, "Node");
557 }
558
559 #[test]
560 fn test_parse_input_object_with_deprecated_field() {
561 let json = r#"{
562 "types": [],
563 "queries": [],
564 "mutations": [],
565 "input_types": [{
566 "name": "CreateUserInput",
567 "fields": [
568 {"name": "email", "type": "String!", "nullable": false},
569 {"name": "name", "type": "String!", "nullable": false},
570 {
571 "name": "username",
572 "type": "String",
573 "nullable": true,
574 "deprecated": {"reason": "Use email as unique identifier instead"}
575 }
576 ]
577 }]
578 }"#;
579
580 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
581 let input = &schema.input_types[0];
582
583 let username_field = &input.fields[2];
585 assert_eq!(username_field.name, "username");
586 assert!(username_field.deprecated.is_some());
587 assert_eq!(
588 username_field.deprecated.as_ref().unwrap().reason,
589 Some("Use email as unique identifier instead".to_string())
590 );
591 }
592
593 #[test]
594 fn test_parse_union() {
595 let json = r#"{
596 "types": [
597 {"name": "User", "fields": [{"name": "id", "type": "ID", "nullable": false}]},
598 {"name": "Post", "fields": [{"name": "id", "type": "ID", "nullable": false}]}
599 ],
600 "queries": [],
601 "mutations": [],
602 "unions": [{
603 "name": "SearchResult",
604 "member_types": ["User", "Post"],
605 "description": "Result from a search query"
606 }]
607 }"#;
608
609 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
610 assert_eq!(schema.unions.len(), 1);
611 let union_def = &schema.unions[0];
612 assert_eq!(union_def.name, "SearchResult");
613 assert_eq!(union_def.member_types, vec!["User", "Post"]);
614 assert_eq!(union_def.description, Some("Result from a search query".to_string()));
615 }
616
617 #[test]
618 fn test_parse_field_with_requires_scope() {
619 let json = r#"{
620 "types": [{
621 "name": "Employee",
622 "fields": [
623 {
624 "name": "id",
625 "type": "ID",
626 "nullable": false
627 },
628 {
629 "name": "name",
630 "type": "String",
631 "nullable": false
632 },
633 {
634 "name": "salary",
635 "type": "Float",
636 "nullable": false,
637 "description": "Employee salary - protected field",
638 "requires_scope": "read:Employee.salary"
639 },
640 {
641 "name": "ssn",
642 "type": "String",
643 "nullable": true,
644 "description": "Social Security Number",
645 "requires_scope": "admin"
646 }
647 ]
648 }],
649 "queries": [],
650 "mutations": []
651 }"#;
652
653 let schema: IntermediateSchema = serde_json::from_str(json).unwrap();
654 assert_eq!(schema.types.len(), 1);
655
656 let employee = &schema.types[0];
657 assert_eq!(employee.name, "Employee");
658 assert_eq!(employee.fields.len(), 4);
659
660 assert_eq!(employee.fields[0].name, "id");
662 assert!(employee.fields[0].requires_scope.is_none());
663
664 assert_eq!(employee.fields[1].name, "name");
666 assert!(employee.fields[1].requires_scope.is_none());
667
668 assert_eq!(employee.fields[2].name, "salary");
670 assert_eq!(employee.fields[2].requires_scope, Some("read:Employee.salary".to_string()));
671
672 assert_eq!(employee.fields[3].name, "ssn");
674 assert_eq!(employee.fields[3].requires_scope, Some("admin".to_string()));
675 }
676}