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    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
493/// Auto-detect format and parse GraphQL schema
494///
495/// # Arguments
496/// * `path` - Path to schema file (.graphql, .gql, or .json)
497///
498/// # Returns
499/// Parsed `GraphQLSchema` or error
500pub 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/// Parse GraphQL introspection JSON (internal - not exposed in mod.rs)
530#[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
541/// Parse GraphQL introspection from a `serde_json` Value (internal - not exposed in mod.rs)
542fn 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
747/// Format a GraphQL default value as a proper GraphQL literal
748/// Converts `graphql_parser` Value types to their GraphQL string representation
749/// Examples:
750/// - IntValue(10) -> "10"
751/// - StringValue("hello") -> "\"hello\""
752/// - BooleanValue(true) -> "true"
753/// - EnumValue("ACTIVE") -> "ACTIVE"
754/// - `ListValue`([1, 2, 3]) -> "[1, 2, 3]"
755fn 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
782/// Extract deprecation reason from directive arguments
783fn 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
796/// Extract fields from an Object type definition (SDL)
797fn 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
825/// Extract fields from an Interface type definition (SDL)
826fn 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/// Format a GraphQL type for display
856#[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
865/// Extract the bare type name (e.g., "String" from "String!" or "[String!]!")
866/// This should be used for `type_name` field to avoid double notation in SDL reconstruction
867fn 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
875/// Check if a type is nullable (not wrapped in `NonNull`)
876const fn is_nullable_type(type_def: &graphql_parser::schema::Type<String>) -> bool {
877    !matches!(type_def, graphql_parser::schema::Type::NonNullType(_))
878}
879
880/// Check if a type is a list
881fn 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
889/// Extract whether list items are nullable
890/// For a type like [String!], returns false (items are non-null)
891/// For a type like [String], returns true (items are nullable)
892/// For non-list types, returns true (default)
893fn 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}