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