Skip to main content

spikard_cli/codegen/graphql/
spec_parser.rs

1//! GraphQL SDL and Introspection schema parsing and extraction.
2//!
3//! This module handles parsing GraphQL schemas from both SDL (Schema Definition Language)
4//! and introspection JSON formats, extracting structured data for code generation including
5//! types, fields, arguments, directives, and their relationships.
6
7use 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/// Parsed GraphQL schema representation
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct GraphQLSchema {
18    /// Map of type names to their definitions
19    pub types: HashMap<String, GraphQLType>,
20    /// Query root type fields
21    pub queries: Vec<GraphQLField>,
22    /// Mutation root type fields (if mutations are supported)
23    pub mutations: Vec<GraphQLField>,
24    /// Subscription root type fields (if subscriptions are supported)
25    pub subscriptions: Vec<GraphQLField>,
26    /// Custom directives defined in the schema
27    pub directives: Vec<GraphQLDirective>,
28    /// Schema description
29    pub description: Option<String>,
30}
31
32/// Represents a GraphQL type definition
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct GraphQLType {
35    /// Type name (e.g., "User", "Post")
36    pub name: String,
37    /// The kind of type (Object, Interface, Union, Enum, `InputObject`, Scalar)
38    pub kind: TypeKind,
39    /// Fields for Object and Interface types
40    pub fields: Vec<GraphQLField>,
41    /// Type description from schema
42    pub description: Option<String>,
43    /// Possible types for Union or Interface implementations
44    pub possible_types: Vec<String>,
45    /// Enum values (for Enum types)
46    pub enum_values: Vec<GraphQLEnumValue>,
47    /// Input fields (for `InputObject` types)
48    pub input_fields: Vec<GraphQLInputField>,
49}
50
51/// GraphQL type category
52#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
53pub enum TypeKind {
54    /// Object type with named fields
55    Object,
56    /// Interface type (abstract)
57    Interface,
58    /// Union type (multiple possible types)
59    Union,
60    /// Enumeration type
61    Enum,
62    /// Input object type
63    InputObject,
64    /// Scalar type (built-in or custom)
65    Scalar,
66    /// List wrapper type
67    List,
68    /// Non-null wrapper type
69    NonNull,
70}
71
72/// Represents a GraphQL field (on Object or Interface types)
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct GraphQLField {
75    /// Field name
76    pub name: String,
77    /// Field return type name
78    pub type_name: String,
79    /// Whether the field returns a list
80    pub is_list: bool,
81    /// Whether list items are nullable (only meaningful if `is_list` is true)
82    pub list_item_nullable: bool,
83    /// Whether the field is nullable (default true)
84    pub is_nullable: bool,
85    /// Field arguments
86    pub arguments: Vec<GraphQLArgument>,
87    /// Field description
88    pub description: Option<String>,
89    /// Deprecation reason (if deprecated)
90    pub deprecation_reason: Option<String>,
91}
92
93/// Represents a GraphQL field argument
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct GraphQLArgument {
96    /// Argument name
97    pub name: String,
98    /// Argument type name
99    pub type_name: String,
100    /// Whether argument is nullable
101    pub is_nullable: bool,
102    /// Whether argument type is a list
103    pub is_list: bool,
104    /// Whether list items are nullable (only meaningful if `is_list` is true)
105    pub list_item_nullable: bool,
106    /// Default value as string (e.g., "10", "\"default\"")
107    pub default_value: Option<String>,
108    /// Argument description
109    pub description: Option<String>,
110}
111
112/// Represents a GraphQL directive
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct GraphQLDirective {
115    /// Directive name (without @)
116    pub name: String,
117    /// Locations where directive can be used
118    pub locations: Vec<String>,
119    /// Directive arguments
120    pub arguments: Vec<GraphQLArgument>,
121    /// Directive description
122    pub description: Option<String>,
123}
124
125/// Represents an enum value
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct GraphQLEnumValue {
128    /// Enum value name
129    pub name: String,
130    /// Enum value description
131    pub description: Option<String>,
132    /// Whether this value is deprecated
133    pub is_deprecated: bool,
134    /// Deprecation reason
135    pub deprecation_reason: Option<String>,
136}
137
138/// Represents an input field (for `InputObject` types)
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct GraphQLInputField {
141    /// Field name
142    pub name: String,
143    /// Field type name
144    pub type_name: String,
145    /// Whether field is nullable
146    pub is_nullable: bool,
147    /// Whether field type is a list
148    pub is_list: bool,
149    /// Whether list items are nullable (only meaningful if `is_list` is true)
150    pub list_item_nullable: bool,
151    /// Default value as string
152    pub default_value: Option<String>,
153    /// Field description
154    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
251/// Parse GraphQL SDL from a file
252///
253/// # Arguments
254/// * `path` - Path to .graphql or .gql file containing SDL
255///
256/// # Returns
257/// Parsed `GraphQLSchema` or error
258pub 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
265/// Parse GraphQL SDL from a string
266pub 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    // Extract directives
279    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    // Extract type definitions
305    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                    // Check if this is the Query type
321                    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                        // Detect duplicate type definitions
329                        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                    // Detect duplicate type definitions
340                    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                    // Detect duplicate type definitions
362                    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                    // Detect duplicate type definitions
384                    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                    // Detect duplicate type definitions
416                    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                    // Detect duplicate type definitions
451                    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    // Validate that schema is not empty
488    if schema.types.is_empty() && schema.queries.is_empty() {
489        return Err(anyhow!("Empty GraphQL schema - no types or queries defined"));
490    }
491
492    // Validate that Query type exists (required by GraphQL spec)
493    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
504/// Auto-detect format and parse GraphQL schema
505///
506/// # Arguments
507/// * `path` - Path to schema file (.graphql, .gql, or .json)
508///
509/// # Returns
510/// Parsed `GraphQLSchema` or error
511pub 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            // Try to parse as introspection, fall back to SDL
517            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            // Fall back to treating as SDL
524            parse_graphql_sdl_string(&content)
525        }
526        Some("graphql" | "gql") => parse_graphql_sdl(path),
527        _ => {
528            // Try to detect by content
529            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                // Likely JSON
534                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                // Likely SDL
539                parse_graphql_sdl_string(&content)
540            }
541        }
542    }
543}
544
545/// Parse GraphQL introspection JSON (internal - not exposed in mod.rs)
546#[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
557/// Parse GraphQL introspection from a `serde_json` Value (internal - not exposed in mod.rs)
558fn 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
763// Helper functions
764
765/// Format a GraphQL default value as a proper GraphQL literal
766/// Converts `graphql_parser` Value types to their GraphQL string representation
767/// Examples:
768/// - IntValue(10) -> "10"
769/// - StringValue("hello") -> "\"hello\""
770/// - BooleanValue(true) -> "true"
771/// - EnumValue("ACTIVE") -> "ACTIVE"
772/// - `ListValue`([1, 2, 3]) -> "[1, 2, 3]"
773fn 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            // Extract the integer value from IntValue
779            i.as_i64().map_or_else(|| format!("{i:?}"), |num| format!("{num}"))
780        }
781        Value::Float(f) => format!("{f}"),
782        Value::String(s) => {
783            // Escape quotes in string values
784            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            // Recursively format list items
791            let formatted: Vec<String> = items.iter().map(|v| format_default_value(v)).collect();
792            format!("[{}]", formatted.join(", "))
793        }
794        Value::Object(fields) => {
795            // Recursively format object fields
796            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        // Fallback for any other variants (variables not typically used in defaults)
803        _ => format!("{value:?}"),
804    }
805}
806
807/// Extract deprecation reason from directive arguments
808fn 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
821/// Extract fields from an Object type definition (SDL)
822fn 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
850/// Extract fields from an Interface type definition (SDL)
851fn 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/// Format a GraphQL type for display
881#[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
890/// Extract the bare type name (e.g., "String" from "String!" or "[String!]!")
891/// This should be used for `type_name` field to avoid double notation in SDL reconstruction
892fn 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
900/// Check if a type is nullable (not wrapped in `NonNull`)
901const fn is_nullable_type(type_def: &graphql_parser::schema::Type<String>) -> bool {
902    !matches!(type_def, graphql_parser::schema::Type::NonNullType(_))
903}
904
905/// Check if a type is a list
906fn 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
914/// Extract whether list items are nullable
915/// For a type like [String!], returns false (items are non-null)
916/// For a type like [String], returns true (items are nullable)
917/// For non-list types, returns true (default)
918fn 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        // Check ACTIVE (not deprecated)
1131        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        // Check INACTIVE (deprecated with custom reason)
1137        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        // Check PENDING (deprecated with default reason)
1143        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        // Check id (not deprecated)
1169        let id_field = &user_type.fields[0];
1170        assert_eq!(id_field.name, "id");
1171        assert!(id_field.deprecation_reason.is_none());
1172
1173        // Check name (not deprecated)
1174        let name_field = &user_type.fields[1];
1175        assert_eq!(name_field.name, "name");
1176        assert!(name_field.deprecation_reason.is_none());
1177
1178        // Check email (deprecated with custom reason)
1179        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        // Check oldField (deprecated with default reason)
1187        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        // Check id (not deprecated)
1210        let id_field = &node_interface.fields[0];
1211        assert_eq!(id_field.name, "id");
1212        assert!(id_field.deprecation_reason.is_none());
1213
1214        // Check createdAt (deprecated with custom reason)
1215        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        // [String] → Option<Vec<Option<String>>>
1237        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        // [String!] → Vec<Option<String>>
1247        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        // [String]! → Option<Vec<String>>
1257        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        // [String!]! → Vec<String>
1267        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}