1use anyhow::{Context, Result, anyhow};
8use graphql_parser::schema::{Document, ObjectType, TypeDefinition, parse_schema};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12use std::fs;
13use std::path::Path;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct GraphQLSchema {
18 pub types: HashMap<String, GraphQLType>,
20 pub queries: Vec<GraphQLField>,
22 pub mutations: Vec<GraphQLField>,
24 pub subscriptions: Vec<GraphQLField>,
26 pub directives: Vec<GraphQLDirective>,
28 pub description: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct GraphQLType {
35 pub name: String,
37 pub kind: TypeKind,
39 pub fields: Vec<GraphQLField>,
41 pub description: Option<String>,
43 pub possible_types: Vec<String>,
45 pub enum_values: Vec<GraphQLEnumValue>,
47 pub input_fields: Vec<GraphQLInputField>,
49}
50
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
53pub enum TypeKind {
54 Object,
56 Interface,
58 Union,
60 Enum,
62 InputObject,
64 Scalar,
66 List,
68 NonNull,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GraphQLField {
75 pub name: String,
77 pub type_name: String,
79 pub is_list: bool,
81 pub list_item_nullable: bool,
83 pub is_nullable: bool,
85 pub arguments: Vec<GraphQLArgument>,
87 pub description: Option<String>,
89 pub deprecation_reason: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct GraphQLArgument {
96 pub name: String,
98 pub type_name: String,
100 pub is_nullable: bool,
102 pub is_list: bool,
104 pub list_item_nullable: bool,
106 pub default_value: Option<String>,
108 pub description: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct GraphQLDirective {
115 pub name: String,
117 pub locations: Vec<String>,
119 pub arguments: Vec<GraphQLArgument>,
121 pub description: Option<String>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct GraphQLEnumValue {
128 pub name: String,
130 pub description: Option<String>,
132 pub is_deprecated: bool,
134 pub deprecation_reason: Option<String>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct GraphQLInputField {
141 pub name: String,
143 pub type_name: String,
145 pub is_nullable: bool,
147 pub is_list: bool,
149 pub list_item_nullable: bool,
151 pub default_value: Option<String>,
153 pub description: Option<String>,
155}
156
157#[derive(Debug, Deserialize)]
158struct IntrospectionEnvelope {
159 #[serde(rename = "__schema")]
160 schema: Option<IntrospectionSchemaDoc>,
161 data: Option<IntrospectionData>,
162}
163
164#[derive(Debug, Deserialize)]
165struct IntrospectionData {
166 #[serde(rename = "__schema")]
167 schema: IntrospectionSchemaDoc,
168}
169
170#[derive(Debug, Deserialize)]
171struct IntrospectionSchemaDoc {
172 description: Option<String>,
173 #[serde(rename = "queryType")]
174 query_type: Option<IntrospectionNamedTypeRef>,
175 #[serde(rename = "mutationType")]
176 mutation_type: Option<IntrospectionNamedTypeRef>,
177 #[serde(rename = "subscriptionType")]
178 subscription_type: Option<IntrospectionNamedTypeRef>,
179 types: Vec<IntrospectionTypeDef>,
180 directives: Vec<IntrospectionDirectiveDef>,
181}
182
183#[derive(Debug, Deserialize)]
184struct IntrospectionNamedTypeRef {
185 name: String,
186}
187
188#[derive(Debug, Deserialize)]
189struct IntrospectionTypeDef {
190 kind: String,
191 name: Option<String>,
192 description: Option<String>,
193 fields: Option<Vec<IntrospectionFieldDef>>,
194 #[serde(rename = "inputFields")]
195 input_fields: Option<Vec<IntrospectionInputValueDef>>,
196 #[serde(rename = "enumValues")]
197 enum_values: Option<Vec<IntrospectionEnumValueDef>>,
198 #[serde(rename = "possibleTypes")]
199 possible_types: Option<Vec<IntrospectionNamedTypeRef>>,
200}
201
202#[derive(Debug, Deserialize)]
203struct IntrospectionFieldDef {
204 name: String,
205 description: Option<String>,
206 args: Vec<IntrospectionInputValueDef>,
207 #[serde(rename = "type")]
208 field_type: IntrospectionTypeRef,
209 #[serde(rename = "isDeprecated", default)]
210 is_deprecated: bool,
211 #[serde(rename = "deprecationReason")]
212 deprecation_reason: Option<String>,
213}
214
215#[derive(Debug, Deserialize)]
216struct IntrospectionInputValueDef {
217 name: String,
218 description: Option<String>,
219 #[serde(rename = "type")]
220 value_type: IntrospectionTypeRef,
221 #[serde(rename = "defaultValue")]
222 default_value: Option<String>,
223}
224
225#[derive(Debug, Deserialize)]
226struct IntrospectionEnumValueDef {
227 name: String,
228 description: Option<String>,
229 #[serde(rename = "isDeprecated", default)]
230 is_deprecated: bool,
231 #[serde(rename = "deprecationReason")]
232 deprecation_reason: Option<String>,
233}
234
235#[derive(Debug, Deserialize)]
236struct IntrospectionDirectiveDef {
237 name: String,
238 description: Option<String>,
239 locations: Vec<String>,
240 args: Vec<IntrospectionInputValueDef>,
241}
242
243#[derive(Debug, Clone, Deserialize)]
244struct IntrospectionTypeRef {
245 kind: String,
246 name: Option<String>,
247 #[serde(rename = "ofType")]
248 of_type: Option<Box<IntrospectionTypeRef>>,
249}
250
251pub fn parse_graphql_sdl(path: &Path) -> Result<GraphQLSchema> {
259 let content =
260 fs::read_to_string(path).with_context(|| format!("Failed to read GraphQL SDL file: {}", path.display()))?;
261
262 parse_graphql_sdl_string(&content).with_context(|| format!("Failed to parse GraphQL SDL from {}", path.display()))
263}
264
265pub fn parse_graphql_sdl_string(content: &str) -> Result<GraphQLSchema> {
267 let doc: Document<String> = parse_schema(content).map_err(|e| anyhow!("GraphQL parsing error: {e}"))?;
268
269 let mut schema = GraphQLSchema {
270 types: HashMap::new(),
271 queries: Vec::new(),
272 mutations: Vec::new(),
273 subscriptions: Vec::new(),
274 directives: Vec::new(),
275 description: None,
276 };
277
278 for directive_def in &doc.definitions {
280 if let graphql_parser::schema::Definition::DirectiveDefinition(dir_def) = directive_def {
281 let args = dir_def
282 .arguments
283 .iter()
284 .map(|arg| GraphQLArgument {
285 name: arg.name.clone(),
286 type_name: extract_bare_type_name(&arg.value_type),
287 is_nullable: is_nullable_type(&arg.value_type),
288 is_list: is_list_type(&arg.value_type),
289 list_item_nullable: extract_list_item_nullability(&arg.value_type),
290 default_value: arg.default_value.as_ref().map(|v| format_default_value(v)),
291 description: arg.description.clone(),
292 })
293 .collect();
294
295 schema.directives.push(GraphQLDirective {
296 name: dir_def.name.clone(),
297 locations: dir_def.locations.iter().map(|l| format!("{l:?}")).collect(),
298 arguments: args,
299 description: dir_def.description.clone(),
300 });
301 }
302 }
303
304 for definition in &doc.definitions {
306 if let graphql_parser::schema::Definition::TypeDefinition(type_def) = definition {
307 match type_def {
308 TypeDefinition::Object(obj) => {
309 let fields = extract_fields_from_object(obj);
310 let gql_type = GraphQLType {
311 name: obj.name.clone(),
312 kind: TypeKind::Object,
313 fields: fields.clone(),
314 description: obj.description.clone(),
315 possible_types: Vec::new(),
316 enum_values: Vec::new(),
317 input_fields: Vec::new(),
318 };
319
320 if obj.name == "Query" {
322 schema.queries = fields;
323 } else if obj.name == "Mutation" {
324 schema.mutations = fields;
325 } else if obj.name == "Subscription" {
326 schema.subscriptions = fields;
327 } else {
328 if schema.types.contains_key(&obj.name) {
330 return Err(anyhow!(
331 "Duplicate type definition: '{}' is defined more than once in the schema",
332 obj.name
333 ));
334 }
335 schema.types.insert(obj.name.clone(), gql_type);
336 }
337 }
338 TypeDefinition::Interface(interface) => {
339 if schema.types.contains_key(&interface.name) {
341 return Err(anyhow!(
342 "Duplicate type definition: '{}' is defined more than once in the schema",
343 interface.name
344 ));
345 }
346 let fields = extract_fields_from_interface(interface);
347 schema.types.insert(
348 interface.name.clone(),
349 GraphQLType {
350 name: interface.name.clone(),
351 kind: TypeKind::Interface,
352 fields,
353 description: interface.description.clone(),
354 possible_types: Vec::new(),
355 enum_values: Vec::new(),
356 input_fields: Vec::new(),
357 },
358 );
359 }
360 TypeDefinition::Union(union) => {
361 if schema.types.contains_key(&union.name) {
363 return Err(anyhow!(
364 "Duplicate type definition: '{}' is defined more than once in the schema",
365 union.name
366 ));
367 }
368 let possible_types = union.types.clone();
369 schema.types.insert(
370 union.name.clone(),
371 GraphQLType {
372 name: union.name.clone(),
373 kind: TypeKind::Union,
374 fields: Vec::new(),
375 description: union.description.clone(),
376 possible_types,
377 enum_values: Vec::new(),
378 input_fields: Vec::new(),
379 },
380 );
381 }
382 TypeDefinition::Enum(enum_type) => {
383 if schema.types.contains_key(&enum_type.name) {
385 return Err(anyhow!(
386 "Duplicate type definition: '{}' is defined more than once in the schema",
387 enum_type.name
388 ));
389 }
390 let enum_values = enum_type
391 .values
392 .iter()
393 .map(|v| GraphQLEnumValue {
394 name: v.name.clone(),
395 description: v.description.clone(),
396 is_deprecated: v.directives.iter().any(|d| d.name == "deprecated"),
397 deprecation_reason: extract_deprecation_reason(&v.directives),
398 })
399 .collect();
400
401 schema.types.insert(
402 enum_type.name.clone(),
403 GraphQLType {
404 name: enum_type.name.clone(),
405 kind: TypeKind::Enum,
406 fields: Vec::new(),
407 description: enum_type.description.clone(),
408 possible_types: Vec::new(),
409 enum_values,
410 input_fields: Vec::new(),
411 },
412 );
413 }
414 TypeDefinition::InputObject(input_obj) => {
415 if schema.types.contains_key(&input_obj.name) {
417 return Err(anyhow!(
418 "Duplicate type definition: '{}' is defined more than once in the schema",
419 input_obj.name
420 ));
421 }
422 let input_fields = input_obj
423 .fields
424 .iter()
425 .map(|f| GraphQLInputField {
426 name: f.name.clone(),
427 type_name: extract_bare_type_name(&f.value_type),
428 is_nullable: is_nullable_type(&f.value_type),
429 is_list: is_list_type(&f.value_type),
430 list_item_nullable: extract_list_item_nullability(&f.value_type),
431 default_value: f.default_value.as_ref().map(|v| format_default_value(v)),
432 description: f.description.clone(),
433 })
434 .collect();
435
436 schema.types.insert(
437 input_obj.name.clone(),
438 GraphQLType {
439 name: input_obj.name.clone(),
440 kind: TypeKind::InputObject,
441 fields: Vec::new(),
442 description: input_obj.description.clone(),
443 possible_types: Vec::new(),
444 enum_values: Vec::new(),
445 input_fields,
446 },
447 );
448 }
449 TypeDefinition::Scalar(scalar) => {
450 if schema.types.contains_key(&scalar.name) {
452 return Err(anyhow!(
453 "Duplicate type definition: '{}' is defined more than once in the schema",
454 scalar.name
455 ));
456 }
457 schema.types.insert(
458 scalar.name.clone(),
459 GraphQLType {
460 name: scalar.name.clone(),
461 kind: TypeKind::Scalar,
462 fields: Vec::new(),
463 description: scalar.description.clone(),
464 possible_types: Vec::new(),
465 enum_values: Vec::new(),
466 input_fields: Vec::new(),
467 },
468 );
469 }
470 }
471 }
472 }
473
474 for definition in &doc.definitions {
475 if let graphql_parser::schema::Definition::TypeDefinition(TypeDefinition::Object(obj)) = definition {
476 for interface_name in &obj.implements_interfaces {
477 if let Some(interface) = schema.types.get_mut(interface_name)
478 && interface.kind == TypeKind::Interface
479 && !interface.possible_types.contains(&obj.name)
480 {
481 interface.possible_types.push(obj.name.clone());
482 }
483 }
484 }
485 }
486
487 if schema.types.is_empty() && schema.queries.is_empty() {
489 return Err(anyhow!("Empty GraphQL schema - no types or queries defined"));
490 }
491
492 if schema.queries.is_empty() {
494 return Err(anyhow!(
495 "Invalid GraphQL schema - Query type is required by the GraphQL specification.\n\
496 Add a Query type to your schema:\n\
497 type Query {{\n hello: String!\n}}"
498 ));
499 }
500
501 Ok(schema)
502}
503
504pub fn parse_graphql_schema(path: &Path) -> Result<GraphQLSchema> {
512 let ext = path.extension().and_then(|s| s.to_str()).map(str::to_lowercase);
513
514 match ext.as_deref() {
515 Some("json") => {
516 let content = fs::read_to_string(path)?;
518 if let Ok(value) = serde_json::from_str::<Value>(&content)
519 && (value.get("__schema").is_some() || value.get("data").is_some())
520 {
521 return parse_graphql_introspection_value(&value);
522 }
523 parse_graphql_sdl_string(&content)
525 }
526 Some("graphql" | "gql") => parse_graphql_sdl(path),
527 _ => {
528 let content =
530 fs::read_to_string(path).with_context(|| format!("Failed to read file: {}", path.display()))?;
531
532 if content.trim().starts_with('{') {
533 let value: Value = serde_json::from_str(&content)
535 .with_context(|| format!("Failed to parse as JSON: {}", path.display()))?;
536 parse_graphql_introspection_value(&value)
537 } else {
538 parse_graphql_sdl_string(&content)
540 }
541 }
542 }
543}
544
545#[allow(dead_code)]
547fn parse_graphql_introspection(path: &Path) -> Result<GraphQLSchema> {
548 let content = fs::read_to_string(path)
549 .with_context(|| format!("Failed to read GraphQL introspection file: {}", path.display()))?;
550
551 let value: Value =
552 serde_json::from_str(&content).with_context(|| format!("Failed to parse JSON from {}", path.display()))?;
553
554 parse_graphql_introspection_value(&value)
555}
556
557fn parse_graphql_introspection_value(_value: &Value) -> Result<GraphQLSchema> {
559 let envelope: IntrospectionEnvelope =
560 serde_json::from_value(_value.clone()).context("Failed to deserialize GraphQL introspection JSON")?;
561
562 let introspection = envelope
563 .schema
564 .or(envelope.data.map(|data| data.schema))
565 .ok_or_else(|| anyhow!("GraphQL introspection JSON must contain '__schema' or 'data.__schema'"))?;
566
567 let query_type_name = introspection.query_type.as_ref().map(|t| t.name.as_str());
568 let mutation_type_name = introspection.mutation_type.as_ref().map(|t| t.name.as_str());
569 let subscription_type_name = introspection.subscription_type.as_ref().map(|t| t.name.as_str());
570
571 let mut schema = GraphQLSchema {
572 types: HashMap::new(),
573 queries: Vec::new(),
574 mutations: Vec::new(),
575 subscriptions: Vec::new(),
576 directives: introspection
577 .directives
578 .into_iter()
579 .map(|directive| {
580 Ok(GraphQLDirective {
581 name: directive.name,
582 locations: directive.locations,
583 arguments: directive
584 .args
585 .into_iter()
586 .map(introspection_argument_to_graphql)
587 .collect::<Result<Vec<_>>>()?,
588 description: directive.description,
589 })
590 })
591 .collect::<Result<Vec<_>>>()?,
592 description: introspection.description,
593 };
594
595 for type_def in introspection.types {
596 let Some(name) = type_def.name.clone() else {
597 continue;
598 };
599
600 if name.starts_with("__") {
601 continue;
602 }
603
604 let kind = introspection_kind_to_type_kind(&type_def.kind)?;
605 let gql_type = GraphQLType {
606 name: name.clone(),
607 kind,
608 fields: type_def
609 .fields
610 .unwrap_or_default()
611 .into_iter()
612 .map(introspection_field_to_graphql)
613 .collect::<Result<Vec<_>>>()?,
614 description: type_def.description,
615 possible_types: type_def
616 .possible_types
617 .unwrap_or_default()
618 .into_iter()
619 .map(|possible| possible.name)
620 .collect(),
621 enum_values: type_def
622 .enum_values
623 .unwrap_or_default()
624 .into_iter()
625 .map(|value| GraphQLEnumValue {
626 name: value.name,
627 description: value.description,
628 is_deprecated: value.is_deprecated,
629 deprecation_reason: value.deprecation_reason,
630 })
631 .collect(),
632 input_fields: type_def
633 .input_fields
634 .unwrap_or_default()
635 .into_iter()
636 .map(introspection_input_field_to_graphql)
637 .collect::<Result<Vec<_>>>()?,
638 };
639
640 if query_type_name == Some(name.as_str()) {
641 schema.queries = gql_type.fields;
642 } else if mutation_type_name == Some(name.as_str()) {
643 schema.mutations = gql_type.fields;
644 } else if subscription_type_name == Some(name.as_str()) {
645 schema.subscriptions = gql_type.fields;
646 } else if schema.types.insert(name.clone(), gql_type).is_some() {
647 return Err(anyhow!(
648 "Duplicate type definition: '{}' is defined more than once in the schema",
649 name
650 ));
651 }
652 }
653
654 if schema.types.is_empty() && schema.queries.is_empty() {
655 return Err(anyhow!("Empty GraphQL schema - no types or queries defined"));
656 }
657
658 if schema.queries.is_empty() {
659 return Err(anyhow!(
660 "Invalid GraphQL schema - Query type is required by the GraphQL specification.\n\
661 Add a Query type to your schema:\n\
662 type Query {{\n hello: String!\n}}"
663 ));
664 }
665
666 Ok(schema)
667}
668
669fn introspection_kind_to_type_kind(kind: &str) -> Result<TypeKind> {
670 match kind {
671 "OBJECT" => Ok(TypeKind::Object),
672 "INTERFACE" => Ok(TypeKind::Interface),
673 "UNION" => Ok(TypeKind::Union),
674 "ENUM" => Ok(TypeKind::Enum),
675 "INPUT_OBJECT" => Ok(TypeKind::InputObject),
676 "SCALAR" => Ok(TypeKind::Scalar),
677 other => Err(anyhow!("Unsupported GraphQL introspection type kind: {other}")),
678 }
679}
680
681fn introspection_field_to_graphql(field: IntrospectionFieldDef) -> Result<GraphQLField> {
682 Ok(GraphQLField {
683 name: field.name,
684 type_name: introspection_bare_type_name(&field.field_type)?,
685 is_list: introspection_is_list_type(&field.field_type),
686 list_item_nullable: introspection_list_item_nullable(&field.field_type),
687 is_nullable: introspection_is_nullable_type(&field.field_type),
688 arguments: field
689 .args
690 .into_iter()
691 .map(introspection_argument_to_graphql)
692 .collect::<Result<Vec<_>>>()?,
693 description: field.description,
694 deprecation_reason: if field.is_deprecated {
695 field.deprecation_reason.or_else(|| Some("Deprecated".to_string()))
696 } else {
697 field.deprecation_reason
698 },
699 })
700}
701
702fn introspection_argument_to_graphql(arg: IntrospectionInputValueDef) -> Result<GraphQLArgument> {
703 Ok(GraphQLArgument {
704 name: arg.name,
705 type_name: introspection_bare_type_name(&arg.value_type)?,
706 is_nullable: introspection_is_nullable_type(&arg.value_type),
707 is_list: introspection_is_list_type(&arg.value_type),
708 list_item_nullable: introspection_list_item_nullable(&arg.value_type),
709 default_value: arg.default_value,
710 description: arg.description,
711 })
712}
713
714fn introspection_input_field_to_graphql(field: IntrospectionInputValueDef) -> Result<GraphQLInputField> {
715 Ok(GraphQLInputField {
716 name: field.name,
717 type_name: introspection_bare_type_name(&field.value_type)?,
718 is_nullable: introspection_is_nullable_type(&field.value_type),
719 is_list: introspection_is_list_type(&field.value_type),
720 list_item_nullable: introspection_list_item_nullable(&field.value_type),
721 default_value: field.default_value,
722 description: field.description,
723 })
724}
725
726fn introspection_bare_type_name(type_ref: &IntrospectionTypeRef) -> Result<String> {
727 if let Some(name) = &type_ref.name {
728 return Ok(name.clone());
729 }
730
731 if let Some(inner) = &type_ref.of_type {
732 return introspection_bare_type_name(inner);
733 }
734
735 Err(anyhow!(
736 "Invalid GraphQL introspection type reference: missing terminal named type"
737 ))
738}
739
740fn introspection_is_nullable_type(type_ref: &IntrospectionTypeRef) -> bool {
741 type_ref.kind != "NON_NULL"
742}
743
744fn introspection_is_list_type(type_ref: &IntrospectionTypeRef) -> bool {
745 match type_ref.kind.as_str() {
746 "LIST" => true,
747 "NON_NULL" => type_ref.of_type.as_deref().is_some_and(introspection_is_list_type),
748 _ => false,
749 }
750}
751
752fn introspection_list_item_nullable(type_ref: &IntrospectionTypeRef) -> bool {
753 match type_ref.kind.as_str() {
754 "NON_NULL" => type_ref
755 .of_type
756 .as_deref()
757 .map_or(true, introspection_list_item_nullable),
758 "LIST" => type_ref.of_type.as_deref().map_or(true, introspection_is_nullable_type),
759 _ => true,
760 }
761}
762
763fn format_default_value(value: &graphql_parser::schema::Value<String>) -> String {
774 use graphql_parser::schema::Value;
775
776 match value {
777 Value::Int(i) => {
778 i.as_i64().map_or_else(|| format!("{i:?}"), |num| format!("{num}"))
780 }
781 Value::Float(f) => format!("{f}"),
782 Value::String(s) => {
783 format!("\"{}\"", s.replace('"', "\\\""))
785 }
786 Value::Boolean(b) => format!("{b}"),
787 Value::Null => "null".to_string(),
788 Value::Enum(e) => e.clone(),
789 Value::List(items) => {
790 let formatted: Vec<String> = items.iter().map(|v| format_default_value(v)).collect();
792 format!("[{}]", formatted.join(", "))
793 }
794 Value::Object(fields) => {
795 let formatted: Vec<String> = fields
797 .iter()
798 .map(|(k, v)| format!("{}: {}", k, format_default_value(v)))
799 .collect();
800 format!("{{{}}}", formatted.join(", "))
801 }
802 _ => format!("{value:?}"),
804 }
805}
806
807fn extract_deprecation_reason(directives: &[graphql_parser::schema::Directive<String>]) -> Option<String> {
809 directives.iter().find(|d| d.name == "deprecated").and_then(|d| {
810 d.arguments
811 .iter()
812 .find(|(arg_name, _)| arg_name == "reason")
813 .and_then(|(_, value)| match value {
814 graphql_parser::schema::Value::String(s) => Some(s.clone()),
815 _ => None,
816 })
817 .or_else(|| Some("Deprecated".to_string()))
818 })
819}
820
821fn extract_fields_from_object(obj: &ObjectType<String>) -> Vec<GraphQLField> {
823 obj.fields
824 .iter()
825 .map(|field| GraphQLField {
826 name: field.name.clone(),
827 type_name: extract_bare_type_name(&field.field_type),
828 is_list: is_list_type(&field.field_type),
829 list_item_nullable: extract_list_item_nullability(&field.field_type),
830 is_nullable: is_nullable_type(&field.field_type),
831 arguments: field
832 .arguments
833 .iter()
834 .map(|arg| GraphQLArgument {
835 name: arg.name.clone(),
836 type_name: extract_bare_type_name(&arg.value_type),
837 is_nullable: is_nullable_type(&arg.value_type),
838 is_list: is_list_type(&arg.value_type),
839 list_item_nullable: extract_list_item_nullability(&arg.value_type),
840 default_value: arg.default_value.as_ref().map(|v| format_default_value(v)),
841 description: arg.description.clone(),
842 })
843 .collect(),
844 description: field.description.clone(),
845 deprecation_reason: extract_deprecation_reason(&field.directives),
846 })
847 .collect()
848}
849
850fn extract_fields_from_interface(interface: &graphql_parser::schema::InterfaceType<String>) -> Vec<GraphQLField> {
852 interface
853 .fields
854 .iter()
855 .map(|field| GraphQLField {
856 name: field.name.clone(),
857 type_name: extract_bare_type_name(&field.field_type),
858 is_list: is_list_type(&field.field_type),
859 list_item_nullable: extract_list_item_nullability(&field.field_type),
860 is_nullable: is_nullable_type(&field.field_type),
861 arguments: field
862 .arguments
863 .iter()
864 .map(|arg| GraphQLArgument {
865 name: arg.name.clone(),
866 type_name: extract_bare_type_name(&arg.value_type),
867 is_nullable: is_nullable_type(&arg.value_type),
868 is_list: is_list_type(&arg.value_type),
869 list_item_nullable: extract_list_item_nullability(&arg.value_type),
870 default_value: arg.default_value.as_ref().map(|v| format_default_value(v)),
871 description: arg.description.clone(),
872 })
873 .collect(),
874 description: field.description.clone(),
875 deprecation_reason: extract_deprecation_reason(&field.directives),
876 })
877 .collect()
878}
879
880#[allow(dead_code)]
882fn format_type(type_def: &graphql_parser::schema::Type<String>) -> String {
883 match type_def {
884 graphql_parser::schema::Type::NamedType(name) => name.clone(),
885 graphql_parser::schema::Type::ListType(inner) => format!("[{}]", format_type(inner)),
886 graphql_parser::schema::Type::NonNullType(inner) => format!("{}!", format_type(inner)),
887 }
888}
889
890fn extract_bare_type_name(type_def: &graphql_parser::schema::Type<String>) -> String {
893 match type_def {
894 graphql_parser::schema::Type::NamedType(name) => name.clone(),
895 graphql_parser::schema::Type::ListType(inner) => extract_bare_type_name(inner),
896 graphql_parser::schema::Type::NonNullType(inner) => extract_bare_type_name(inner),
897 }
898}
899
900const fn is_nullable_type(type_def: &graphql_parser::schema::Type<String>) -> bool {
902 !matches!(type_def, graphql_parser::schema::Type::NonNullType(_))
903}
904
905fn is_list_type(type_def: &graphql_parser::schema::Type<String>) -> bool {
907 match type_def {
908 graphql_parser::schema::Type::ListType(_) => true,
909 graphql_parser::schema::Type::NonNullType(inner) => is_list_type(inner),
910 _ => false,
911 }
912}
913
914fn extract_list_item_nullability(type_def: &graphql_parser::schema::Type<String>) -> bool {
919 match type_def {
920 graphql_parser::schema::Type::NonNullType(inner) => extract_list_item_nullability(inner),
921 graphql_parser::schema::Type::ListType(inner) => is_nullable_type(inner),
922 _ => true,
923 }
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929 use serde_json::json;
930 use std::fs;
931 use tempfile::tempdir;
932
933 #[test]
934 fn test_parse_simple_sdl() {
935 let sdl = r#"
936 type Query {
937 hello: String!
938 user(id: ID!): User
939 }
940
941 type User {
942 id: ID!
943 name: String!
944 email: String
945 }
946 "#;
947
948 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
949 assert!(!schema.queries.is_empty());
950 assert_eq!(schema.queries[0].name, "hello");
951 assert!(schema.types.contains_key("User"));
952 }
953
954 #[test]
955 fn test_parse_sdl_with_enum() {
956 let sdl = r#"
957 type Query {
958 users(status: UserStatus): [User!]!
959 }
960
961 enum UserStatus {
962 ACTIVE
963 INACTIVE
964 PENDING
965 }
966
967 type User {
968 id: ID!
969 status: UserStatus!
970 }
971 "#;
972
973 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
974 assert!(schema.types.contains_key("UserStatus"));
975 let user_status = &schema.types["UserStatus"];
976 assert_eq!(user_status.kind, TypeKind::Enum);
977 assert_eq!(user_status.enum_values.len(), 3);
978 }
979
980 #[test]
981 fn test_parse_sdl_with_input_object() {
982 let sdl = r#"
983 type Query {
984 createUser(input: CreateUserInput!): User!
985 }
986
987 input CreateUserInput {
988 name: String!
989 email: String!
990 age: Int
991 }
992
993 type User {
994 id: ID!
995 name: String!
996 }
997 "#;
998
999 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1000 assert!(schema.types.contains_key("CreateUserInput"));
1001 let input = &schema.types["CreateUserInput"];
1002 assert_eq!(input.kind, TypeKind::InputObject);
1003 assert_eq!(input.input_fields.len(), 3);
1004 }
1005
1006 #[test]
1007 fn test_parse_sdl_with_interface() {
1008 let sdl = r#"
1009 interface Node {
1010 id: ID!
1011 }
1012
1013 type User implements Node {
1014 id: ID!
1015 name: String!
1016 }
1017
1018 type Query {
1019 node(id: ID!): Node
1020 }
1021 "#;
1022
1023 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1024 assert!(schema.types.contains_key("Node"));
1025 let node = &schema.types["Node"];
1026 assert_eq!(node.kind, TypeKind::Interface);
1027 }
1028
1029 #[test]
1030 fn test_parse_sdl_with_union() {
1031 let sdl = r#"
1032 union SearchResult = User | Post
1033
1034 type Query {
1035 search(query: String!): [SearchResult!]!
1036 }
1037
1038 type User {
1039 id: ID!
1040 name: String!
1041 }
1042
1043 type Post {
1044 id: ID!
1045 title: String!
1046 }
1047 "#;
1048
1049 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1050 assert!(schema.types.contains_key("SearchResult"));
1051 let union = &schema.types["SearchResult"];
1052 assert_eq!(union.kind, TypeKind::Union);
1053 assert_eq!(union.possible_types.len(), 2);
1054 }
1055
1056 #[test]
1057 fn test_parse_sdl_with_directives() {
1058 let sdl = r#"
1059 directive @auth(role: String!) on FIELD_DEFINITION
1060
1061 type Query {
1062 adminUsers: [User!]! @auth(role: "admin")
1063 }
1064
1065 type User {
1066 id: ID!
1067 name: String!
1068 }
1069 "#;
1070
1071 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1072 assert!(!schema.directives.is_empty());
1073 let auth_dir = schema
1074 .directives
1075 .iter()
1076 .find(|d| d.name == "auth")
1077 .expect("auth directive");
1078 assert_eq!(auth_dir.arguments.len(), 1);
1079 }
1080
1081 #[test]
1082 fn test_nullable_and_list_detection() {
1083 let sdl = r#"
1084 type Query {
1085 required: String!
1086 nullable: String
1087 list: [String!]!
1088 nullableList: [String!]
1089 listOfNullable: [String]!
1090 }
1091 "#;
1092
1093 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1094
1095 let required = &schema.queries[0];
1096 assert!(!required.is_nullable);
1097 assert!(!required.is_list);
1098
1099 let nullable = schema.queries.iter().find(|f| f.name == "nullable").unwrap();
1100 assert!(nullable.is_nullable);
1101 assert!(!nullable.is_list);
1102
1103 let list = schema.queries.iter().find(|f| f.name == "list").unwrap();
1104 assert!(!list.is_nullable);
1105 assert!(list.is_list);
1106
1107 let nullable_list = schema.queries.iter().find(|f| f.name == "nullableList").unwrap();
1108 assert!(nullable_list.is_nullable);
1109 assert!(nullable_list.is_list);
1110 }
1111
1112 #[test]
1113 fn test_enum_deprecation_with_custom_reason() {
1114 let sdl = r#"
1115 enum Status {
1116 ACTIVE
1117 INACTIVE @deprecated(reason: "Use ARCHIVED instead")
1118 PENDING @deprecated
1119 }
1120
1121 type Query {
1122 status: Status
1123 }
1124 "#;
1125
1126 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1127 let status_enum = &schema.types["Status"];
1128 assert_eq!(status_enum.enum_values.len(), 3);
1129
1130 let active = &status_enum.enum_values[0];
1132 assert_eq!(active.name, "ACTIVE");
1133 assert!(!active.is_deprecated);
1134 assert!(active.deprecation_reason.is_none());
1135
1136 let inactive = &status_enum.enum_values[1];
1138 assert_eq!(inactive.name, "INACTIVE");
1139 assert!(inactive.is_deprecated);
1140 assert_eq!(inactive.deprecation_reason, Some("Use ARCHIVED instead".to_string()));
1141
1142 let pending = &status_enum.enum_values[2];
1144 assert_eq!(pending.name, "PENDING");
1145 assert!(pending.is_deprecated);
1146 assert_eq!(pending.deprecation_reason, Some("Deprecated".to_string()));
1147 }
1148
1149 #[test]
1150 fn test_field_deprecation_with_custom_reason() {
1151 let sdl = r#"
1152 type User {
1153 id: ID!
1154 name: String!
1155 email: String @deprecated(reason: "Use emailAddress instead")
1156 oldField: String @deprecated
1157 }
1158
1159 type Query {
1160 user(id: ID!): User
1161 }
1162 "#;
1163
1164 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1165 let user_type = &schema.types["User"];
1166 assert_eq!(user_type.fields.len(), 4);
1167
1168 let id_field = &user_type.fields[0];
1170 assert_eq!(id_field.name, "id");
1171 assert!(id_field.deprecation_reason.is_none());
1172
1173 let name_field = &user_type.fields[1];
1175 assert_eq!(name_field.name, "name");
1176 assert!(name_field.deprecation_reason.is_none());
1177
1178 let email_field = &user_type.fields[2];
1180 assert_eq!(email_field.name, "email");
1181 assert_eq!(
1182 email_field.deprecation_reason,
1183 Some("Use emailAddress instead".to_string())
1184 );
1185
1186 let old_field = &user_type.fields[3];
1188 assert_eq!(old_field.name, "oldField");
1189 assert_eq!(old_field.deprecation_reason, Some("Deprecated".to_string()));
1190 }
1191
1192 #[test]
1193 fn test_interface_field_deprecation() {
1194 let sdl = r#"
1195 interface Node {
1196 id: ID!
1197 createdAt: String @deprecated(reason: "Use timestamp instead")
1198 }
1199
1200 type Query {
1201 node(id: ID!): Node
1202 }
1203 "#;
1204
1205 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1206 let node_interface = &schema.types["Node"];
1207 assert_eq!(node_interface.fields.len(), 2);
1208
1209 let id_field = &node_interface.fields[0];
1211 assert_eq!(id_field.name, "id");
1212 assert!(id_field.deprecation_reason.is_none());
1213
1214 let created_at_field = &node_interface.fields[1];
1216 assert_eq!(created_at_field.name, "createdAt");
1217 assert_eq!(
1218 created_at_field.deprecation_reason,
1219 Some("Use timestamp instead".to_string())
1220 );
1221 }
1222
1223 #[test]
1224 fn test_list_item_nullability_detection() {
1225 let sdl = r#"
1226 type Query {
1227 listOfNullableStrings: [String]
1228 listOfNonNullStrings: [String!]
1229 nonNullListOfNullableStrings: [String]!
1230 nonNullListOfNonNullStrings: [String!]!
1231 }
1232 "#;
1233
1234 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse SDL");
1235
1236 let list_nullable = schema
1238 .queries
1239 .iter()
1240 .find(|f| f.name == "listOfNullableStrings")
1241 .unwrap();
1242 assert!(list_nullable.is_nullable);
1243 assert!(list_nullable.is_list);
1244 assert!(list_nullable.list_item_nullable);
1245
1246 let list_non_null = schema
1248 .queries
1249 .iter()
1250 .find(|f| f.name == "listOfNonNullStrings")
1251 .unwrap();
1252 assert!(list_non_null.is_nullable);
1253 assert!(list_non_null.is_list);
1254 assert!(!list_non_null.list_item_nullable);
1255
1256 let non_null_list_nullable = schema
1258 .queries
1259 .iter()
1260 .find(|f| f.name == "nonNullListOfNullableStrings")
1261 .unwrap();
1262 assert!(!non_null_list_nullable.is_nullable);
1263 assert!(non_null_list_nullable.is_list);
1264 assert!(non_null_list_nullable.list_item_nullable);
1265
1266 let non_null_list_non_null = schema
1268 .queries
1269 .iter()
1270 .find(|f| f.name == "nonNullListOfNonNullStrings")
1271 .unwrap();
1272 assert!(!non_null_list_non_null.is_nullable);
1273 assert!(non_null_list_non_null.is_list);
1274 assert!(!non_null_list_non_null.list_item_nullable);
1275 }
1276
1277 #[test]
1278 fn test_empty_schema_rejected() {
1279 let sdl = r#"
1280 directive @example on FIELD_DEFINITION
1281 "#;
1282
1283 let result = parse_graphql_sdl_string(sdl);
1284 assert!(result.is_err());
1285 let error_msg = format!("{}", result.unwrap_err());
1286 assert!(error_msg.contains("Empty GraphQL schema"));
1287 assert!(error_msg.contains("no types or queries defined"));
1288 }
1289
1290 #[test]
1291 fn test_schema_without_query_rejected() {
1292 let sdl = r#"
1293 type Mutation {
1294 createUser(name: String!): User!
1295 }
1296
1297 type User {
1298 id: ID!
1299 name: String!
1300 }
1301 "#;
1302
1303 let result = parse_graphql_sdl_string(sdl);
1304 assert!(result.is_err());
1305 let error_msg = format!("{}", result.unwrap_err());
1306 assert!(error_msg.contains("Query type is required"));
1307 assert!(error_msg.contains("GraphQL specification"));
1308 }
1309
1310 #[test]
1311 fn test_duplicate_type_definition_rejected() {
1312 let sdl = r#"
1313 type Query {
1314 hello: String!
1315 }
1316
1317 type User {
1318 id: ID!
1319 name: String!
1320 }
1321
1322 type User {
1323 id: ID!
1324 email: String!
1325 }
1326 "#;
1327
1328 let result = parse_graphql_sdl_string(sdl);
1329 assert!(result.is_err());
1330 let error_msg = format!("{}", result.unwrap_err());
1331 assert!(error_msg.contains("Duplicate type definition"));
1332 assert!(error_msg.contains("User"));
1333 assert!(error_msg.contains("defined more than once"));
1334 }
1335
1336 #[test]
1337 fn test_duplicate_enum_definition_rejected() {
1338 let sdl = r#"
1339 enum Status {
1340 ACTIVE
1341 INACTIVE
1342 }
1343
1344 type Query {
1345 status: Status!
1346 }
1347
1348 enum Status {
1349 PENDING
1350 ARCHIVED
1351 }
1352 "#;
1353
1354 let result = parse_graphql_sdl_string(sdl);
1355 assert!(result.is_err());
1356 let error_msg = format!("{}", result.unwrap_err());
1357 assert!(error_msg.contains("Duplicate type definition"));
1358 assert!(error_msg.contains("Status"));
1359 }
1360
1361 #[test]
1362 fn test_duplicate_scalar_definition_rejected() {
1363 let sdl = r#"
1364 scalar DateTime
1365
1366 type Query {
1367 now: DateTime!
1368 }
1369
1370 scalar DateTime
1371 "#;
1372
1373 let result = parse_graphql_sdl_string(sdl);
1374 assert!(result.is_err());
1375 let error_msg = format!("{}", result.unwrap_err());
1376 assert!(error_msg.contains("Duplicate type definition"));
1377 assert!(error_msg.contains("DateTime"));
1378 }
1379
1380 #[test]
1381 fn test_duplicate_interface_definition_rejected() {
1382 let sdl = r#"
1383 interface Node {
1384 id: ID!
1385 }
1386
1387 type Query {
1388 node(id: ID!): Node
1389 }
1390
1391 interface Node {
1392 id: ID!
1393 createdAt: String!
1394 }
1395 "#;
1396
1397 let result = parse_graphql_sdl_string(sdl);
1398 assert!(result.is_err());
1399 let error_msg = format!("{}", result.unwrap_err());
1400 assert!(error_msg.contains("Duplicate type definition"));
1401 assert!(error_msg.contains("Node"));
1402 }
1403
1404 #[test]
1405 fn test_duplicate_input_object_definition_rejected() {
1406 let sdl = r#"
1407 input UserInput {
1408 name: String!
1409 }
1410
1411 type Query {
1412 createUser(input: UserInput!): String!
1413 }
1414
1415 input UserInput {
1416 email: String!
1417 }
1418 "#;
1419
1420 let result = parse_graphql_sdl_string(sdl);
1421 assert!(result.is_err());
1422 let error_msg = format!("{}", result.unwrap_err());
1423 assert!(error_msg.contains("Duplicate type definition"));
1424 assert!(error_msg.contains("UserInput"));
1425 }
1426
1427 #[test]
1428 fn test_duplicate_union_definition_rejected() {
1429 let sdl = r#"
1430 union SearchResult = User | Post
1431
1432 type Query {
1433 search(query: String!): SearchResult!
1434 }
1435
1436 type User {
1437 id: ID!
1438 }
1439
1440 type Post {
1441 id: ID!
1442 }
1443
1444 union SearchResult = User | Post | Comment
1445 "#;
1446
1447 let result = parse_graphql_sdl_string(sdl);
1448 assert!(result.is_err());
1449 let error_msg = format!("{}", result.unwrap_err());
1450 assert!(error_msg.contains("Duplicate type definition"));
1451 assert!(error_msg.contains("SearchResult"));
1452 }
1453
1454 #[test]
1455 fn test_valid_schema_with_query_and_mutations() {
1456 let sdl = r#"
1457 type Query {
1458 hello: String!
1459 user(id: ID!): User
1460 }
1461
1462 type Mutation {
1463 createUser(name: String!): User!
1464 }
1465
1466 type User {
1467 id: ID!
1468 name: String!
1469 }
1470 "#;
1471
1472 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse valid SDL");
1473 assert!(!schema.queries.is_empty());
1474 assert!(!schema.mutations.is_empty());
1475 assert!(schema.types.contains_key("User"));
1476 }
1477
1478 #[test]
1479 fn test_int_default_value() {
1480 let sdl = r#"
1481 type Query {
1482 items(limit: Int = 10): [String!]!
1483 }
1484 "#;
1485
1486 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1487 let query = &schema.queries[0];
1488 assert_eq!(query.arguments.len(), 1);
1489 assert_eq!(query.arguments[0].default_value, Some("10".to_string()));
1490 }
1491
1492 #[test]
1493 fn test_string_default_value() {
1494 let sdl = r#"
1495 type Query {
1496 search(query: String = "default"): [String!]!
1497 }
1498 "#;
1499
1500 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1501 let query = &schema.queries[0];
1502 assert_eq!(query.arguments[0].default_value, Some("\"default\"".to_string()));
1503 }
1504
1505 #[test]
1506 fn test_boolean_default_value() {
1507 let sdl = r#"
1508 type Query {
1509 items(active: Boolean = true): [String!]!
1510 }
1511 "#;
1512
1513 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1514 let query = &schema.queries[0];
1515 assert_eq!(query.arguments[0].default_value, Some("true".to_string()));
1516 }
1517
1518 #[test]
1519 fn test_list_default_value() {
1520 let sdl = r#"
1521 type Query {
1522 filter(tags: [String!] = ["a", "b"]): [String!]!
1523 }
1524 "#;
1525
1526 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1527 let query = &schema.queries[0];
1528 assert_eq!(query.arguments[0].default_value, Some("[\"a\", \"b\"]".to_string()));
1529 }
1530
1531 #[test]
1532 fn test_enum_default_value() {
1533 let sdl = r#"
1534 enum Status {
1535 ACTIVE
1536 INACTIVE
1537 }
1538
1539 type Query {
1540 users(status: Status = ACTIVE): [String!]!
1541 }
1542 "#;
1543
1544 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1545 let query = &schema.queries[0];
1546 assert_eq!(query.arguments[0].default_value, Some("ACTIVE".to_string()));
1547 }
1548
1549 #[test]
1550 fn test_input_field_default_value() {
1551 let sdl = r#"
1552 input FilterInput {
1553 limit: Int = 100
1554 name: String = "test"
1555 }
1556
1557 type Query {
1558 search(filter: FilterInput!): [String!]!
1559 }
1560 "#;
1561
1562 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1563 let input = &schema.types["FilterInput"];
1564 assert_eq!(input.input_fields[0].default_value, Some("100".to_string()));
1565 assert_eq!(input.input_fields[1].default_value, Some("\"test\"".to_string()));
1566 }
1567
1568 #[test]
1569 fn test_directive_argument_default_value() {
1570 let sdl = r#"
1571 directive @cache(ttl: Int = 3600) on FIELD_DEFINITION
1572
1573 type Query {
1574 cached: String!
1575 }
1576 "#;
1577
1578 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1579 let cache_dir = &schema.directives[0];
1580 assert_eq!(cache_dir.arguments[0].default_value, Some("3600".to_string()));
1581 }
1582
1583 #[test]
1584 fn test_multiple_default_values() {
1585 let sdl = r#"
1586 type Query {
1587 users(limit: Int = 10, offset: Int = 0): [String!]!
1588 }
1589 "#;
1590
1591 let schema = parse_graphql_sdl_string(sdl).expect("Failed to parse");
1592 let query = &schema.queries[0];
1593 assert_eq!(query.arguments.len(), 2);
1594 assert_eq!(query.arguments[0].default_value, Some("10".to_string()));
1595 assert_eq!(query.arguments[1].default_value, Some("0".to_string()));
1596 }
1597
1598 #[test]
1599 fn test_parse_graphql_introspection_value() {
1600 let introspection = json!({
1601 "__schema": {
1602 "description": "Test schema",
1603 "queryType": { "name": "Query" },
1604 "mutationType": { "name": "Mutation" },
1605 "subscriptionType": null,
1606 "directives": [
1607 {
1608 "name": "deprecated",
1609 "description": "Marks deprecated fields",
1610 "locations": ["FIELD_DEFINITION", "ENUM_VALUE"],
1611 "args": [
1612 {
1613 "name": "reason",
1614 "description": "Reason text",
1615 "type": { "kind": "SCALAR", "name": "String", "ofType": null },
1616 "defaultValue": "\"Deprecated\""
1617 }
1618 ]
1619 }
1620 ],
1621 "types": [
1622 {
1623 "kind": "OBJECT",
1624 "name": "Query",
1625 "description": "Root query",
1626 "fields": [
1627 {
1628 "name": "user",
1629 "description": "Lookup a user",
1630 "args": [
1631 {
1632 "name": "id",
1633 "description": "User ID",
1634 "type": {
1635 "kind": "NON_NULL",
1636 "name": null,
1637 "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
1638 },
1639 "defaultValue": null
1640 }
1641 ],
1642 "type": { "kind": "OBJECT", "name": "User", "ofType": null },
1643 "isDeprecated": false,
1644 "deprecationReason": null
1645 }
1646 ],
1647 "inputFields": null,
1648 "enumValues": null,
1649 "possibleTypes": null
1650 },
1651 {
1652 "kind": "OBJECT",
1653 "name": "Mutation",
1654 "description": "Root mutation",
1655 "fields": [
1656 {
1657 "name": "createUser",
1658 "description": null,
1659 "args": [],
1660 "type": {
1661 "kind": "NON_NULL",
1662 "name": null,
1663 "ofType": { "kind": "OBJECT", "name": "User", "ofType": null }
1664 },
1665 "isDeprecated": false,
1666 "deprecationReason": null
1667 }
1668 ],
1669 "inputFields": null,
1670 "enumValues": null,
1671 "possibleTypes": null
1672 },
1673 {
1674 "kind": "OBJECT",
1675 "name": "User",
1676 "description": "A user",
1677 "fields": [
1678 {
1679 "name": "id",
1680 "description": null,
1681 "args": [],
1682 "type": {
1683 "kind": "NON_NULL",
1684 "name": null,
1685 "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
1686 },
1687 "isDeprecated": false,
1688 "deprecationReason": null
1689 },
1690 {
1691 "name": "emails",
1692 "description": null,
1693 "args": [],
1694 "type": {
1695 "kind": "NON_NULL",
1696 "name": null,
1697 "ofType": {
1698 "kind": "LIST",
1699 "name": null,
1700 "ofType": {
1701 "kind": "NON_NULL",
1702 "name": null,
1703 "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
1704 }
1705 }
1706 },
1707 "isDeprecated": false,
1708 "deprecationReason": null
1709 }
1710 ],
1711 "inputFields": null,
1712 "enumValues": null,
1713 "possibleTypes": null
1714 },
1715 {
1716 "kind": "INPUT_OBJECT",
1717 "name": "CreateUserInput",
1718 "description": "User input",
1719 "fields": null,
1720 "inputFields": [
1721 {
1722 "name": "email",
1723 "description": null,
1724 "type": { "kind": "SCALAR", "name": "String", "ofType": null },
1725 "defaultValue": "\"test@example.com\""
1726 }
1727 ],
1728 "enumValues": null,
1729 "possibleTypes": null
1730 },
1731 {
1732 "kind": "ENUM",
1733 "name": "Status",
1734 "description": null,
1735 "fields": null,
1736 "inputFields": null,
1737 "enumValues": [
1738 {
1739 "name": "ACTIVE",
1740 "description": null,
1741 "isDeprecated": false,
1742 "deprecationReason": null
1743 }
1744 ],
1745 "possibleTypes": null
1746 },
1747 {
1748 "kind": "SCALAR",
1749 "name": "ID",
1750 "description": null,
1751 "fields": null,
1752 "inputFields": null,
1753 "enumValues": null,
1754 "possibleTypes": null
1755 },
1756 {
1757 "kind": "SCALAR",
1758 "name": "String",
1759 "description": null,
1760 "fields": null,
1761 "inputFields": null,
1762 "enumValues": null,
1763 "possibleTypes": null
1764 }
1765 ]
1766 }
1767 });
1768
1769 let schema = parse_graphql_introspection_value(&introspection).expect("Failed to parse introspection");
1770
1771 assert_eq!(schema.description, Some("Test schema".to_string()));
1772 assert_eq!(schema.queries.len(), 1);
1773 assert_eq!(schema.queries[0].name, "user");
1774 assert_eq!(schema.mutations.len(), 1);
1775 assert!(schema.types.contains_key("User"));
1776 assert!(schema.types.contains_key("CreateUserInput"));
1777 assert!(schema.types.contains_key("Status"));
1778 assert_eq!(schema.directives.len(), 1);
1779
1780 let user = &schema.types["User"];
1781 assert_eq!(user.fields[1].name, "emails");
1782 assert!(user.fields[1].is_list);
1783 assert!(!user.fields[1].list_item_nullable);
1784 assert!(!user.fields[1].is_nullable);
1785
1786 let input = &schema.types["CreateUserInput"];
1787 assert_eq!(
1788 input.input_fields[0].default_value,
1789 Some("\"test@example.com\"".to_string())
1790 );
1791 }
1792
1793 #[test]
1794 fn test_parse_graphql_schema_from_introspection_json_file() {
1795 let dir = tempdir().expect("temp dir");
1796 let path = dir.path().join("schema.json");
1797 let introspection = json!({
1798 "data": {
1799 "__schema": {
1800 "description": null,
1801 "queryType": { "name": "Query" },
1802 "mutationType": null,
1803 "subscriptionType": null,
1804 "directives": [],
1805 "types": [
1806 {
1807 "kind": "OBJECT",
1808 "name": "Query",
1809 "description": null,
1810 "fields": [
1811 {
1812 "name": "hello",
1813 "description": null,
1814 "args": [],
1815 "type": {
1816 "kind": "NON_NULL",
1817 "name": null,
1818 "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
1819 },
1820 "isDeprecated": false,
1821 "deprecationReason": null
1822 }
1823 ],
1824 "inputFields": null,
1825 "enumValues": null,
1826 "possibleTypes": null
1827 },
1828 {
1829 "kind": "SCALAR",
1830 "name": "String",
1831 "description": null,
1832 "fields": null,
1833 "inputFields": null,
1834 "enumValues": null,
1835 "possibleTypes": null
1836 }
1837 ]
1838 }
1839 }
1840 });
1841
1842 fs::write(&path, introspection.to_string()).expect("write introspection file");
1843 let schema = parse_graphql_schema(&path).expect("parse introspection file");
1844
1845 assert_eq!(schema.queries.len(), 1);
1846 assert_eq!(schema.queries[0].name, "hello");
1847 }
1848}