Skip to main content

fraiseql_core/schema/
introspection.rs

1//! GraphQL introspection types per GraphQL spec §4.1-4.2.
2//!
3//! This module provides standard GraphQL introspection support, enabling
4//! tools like Apollo Sandbox, GraphiQL, and Altair to query the schema.
5//!
6//! # Architecture
7//!
8//! FraiseQL generates introspection responses at **compile time** for performance.
9//! The `IntrospectionSchema` is built from `CompiledSchema` and cached.
10//!
11//! # Supported Queries
12//!
13//! - `__schema` - Returns the full schema introspection
14//! - `__type(name: String!)` - Returns a specific type's introspection
15//! - `__typename` - Handled at projection level, not here
16
17use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21use super::{
22    CompiledSchema, DirectiveDefinition, DirectiveLocationKind, EnumDefinition, FieldDefinition,
23    FieldType, InputObjectDefinition, InterfaceDefinition, QueryDefinition, TypeDefinition,
24    UnionDefinition,
25};
26
27// =============================================================================
28// GraphQL Introspection Types (per spec §4.1)
29// =============================================================================
30
31/// `__Schema` introspection type.
32///
33/// Root type for schema introspection queries.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct IntrospectionSchema {
37    /// Description of the schema.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub description: Option<String>,
40
41    /// All types in the schema.
42    pub types: Vec<IntrospectionType>,
43
44    /// The root Query type.
45    pub query_type: IntrospectionTypeRef,
46
47    /// The root Mutation type (if any).
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub mutation_type: Option<IntrospectionTypeRef>,
50
51    /// The root Subscription type (if any).
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub subscription_type: Option<IntrospectionTypeRef>,
54
55    /// All directives supported by the schema.
56    pub directives: Vec<IntrospectionDirective>,
57}
58
59/// `__Type` introspection type.
60///
61/// Represents any type in the GraphQL type system.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct IntrospectionType {
65    /// The kind of type (SCALAR, OBJECT, etc.).
66    pub kind: TypeKind,
67
68    /// The name of the type.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub name: Option<String>,
71
72    /// Description of the type.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub description: Option<String>,
75
76    /// Fields (for OBJECT and INTERFACE types).
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub fields: Option<Vec<IntrospectionField>>,
79
80    /// Interfaces this type implements (for OBJECT types).
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub interfaces: Option<Vec<IntrospectionTypeRef>>,
83
84    /// Possible types (for INTERFACE and UNION types).
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub possible_types: Option<Vec<IntrospectionTypeRef>>,
87
88    /// Enum values (for ENUM types).
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub enum_values: Option<Vec<IntrospectionEnumValue>>,
91
92    /// Input fields (for INPUT_OBJECT types).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub input_fields: Option<Vec<IntrospectionInputValue>>,
95
96    /// The wrapped type (for NON_NULL and LIST types).
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub of_type: Option<Box<IntrospectionType>>,
99
100    /// Specified by URL (for custom scalars per GraphQL spec §3.5.5).
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub specified_by_u_r_l: Option<String>,
103}
104
105impl IntrospectionType {
106    /// Filter out deprecated fields if `include_deprecated` is false.
107    ///
108    /// Per GraphQL spec, the `fields` introspection field accepts an
109    /// `includeDeprecated` argument (default: false).
110    #[must_use]
111    pub fn filter_deprecated_fields(&self, include_deprecated: bool) -> Self {
112        let mut result = self.clone();
113
114        if !include_deprecated {
115            if let Some(ref fields) = result.fields {
116                result.fields = Some(fields.iter().filter(|f| !f.is_deprecated).cloned().collect());
117            }
118        }
119
120        result
121    }
122
123    /// Filter out deprecated enum values if `include_deprecated` is false.
124    ///
125    /// Per GraphQL spec, the `enumValues` introspection field accepts an
126    /// `includeDeprecated` argument (default: false).
127    #[must_use]
128    pub fn filter_deprecated_enum_values(&self, include_deprecated: bool) -> Self {
129        let mut result = self.clone();
130
131        if !include_deprecated {
132            if let Some(ref values) = result.enum_values {
133                result.enum_values =
134                    Some(values.iter().filter(|v| !v.is_deprecated).cloned().collect());
135            }
136        }
137
138        result
139    }
140
141    /// Filter out all deprecated items (fields and enum values).
142    ///
143    /// Convenience method combining both filters.
144    #[must_use]
145    pub fn filter_all_deprecated(&self, include_deprecated: bool) -> Self {
146        self.filter_deprecated_fields(include_deprecated)
147            .filter_deprecated_enum_values(include_deprecated)
148    }
149}
150
151/// Type reference (simplified type with just name).
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct IntrospectionTypeRef {
154    /// The name of the referenced type.
155    pub name: String,
156}
157
158/// `__Field` introspection type.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct IntrospectionField {
162    /// Field name.
163    pub name: String,
164
165    /// Field description.
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub description: Option<String>,
168
169    /// Field arguments.
170    pub args: Vec<IntrospectionInputValue>,
171
172    /// Field return type.
173    #[serde(rename = "type")]
174    pub field_type: IntrospectionType,
175
176    /// Whether the field is deprecated.
177    pub is_deprecated: bool,
178
179    /// Deprecation reason (if deprecated).
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub deprecation_reason: Option<String>,
182}
183
184/// `__InputValue` introspection type.
185///
186/// Per GraphQL spec, input values (arguments and input fields) can be deprecated.
187/// The `isDeprecated` and `deprecationReason` fields are part of the June 2021 spec.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct IntrospectionInputValue {
191    /// Input name.
192    pub name: String,
193
194    /// Input description.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub description: Option<String>,
197
198    /// Input type.
199    #[serde(rename = "type")]
200    pub input_type: IntrospectionType,
201
202    /// Default value (as JSON string).
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub default_value: Option<String>,
205
206    /// Whether the input value is deprecated.
207    pub is_deprecated: bool,
208
209    /// Deprecation reason (if deprecated).
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub deprecation_reason: Option<String>,
212}
213
214/// `__EnumValue` introspection type.
215#[derive(Debug, Clone, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct IntrospectionEnumValue {
218    /// Enum value name.
219    pub name: String,
220
221    /// Enum value description.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub description: Option<String>,
224
225    /// Whether the value is deprecated.
226    pub is_deprecated: bool,
227
228    /// Deprecation reason (if deprecated).
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub deprecation_reason: Option<String>,
231}
232
233/// `__Directive` introspection type.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct IntrospectionDirective {
237    /// Directive name.
238    pub name: String,
239
240    /// Directive description.
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub description: Option<String>,
243
244    /// Valid locations for this directive.
245    pub locations: Vec<DirectiveLocation>,
246
247    /// Directive arguments.
248    pub args: Vec<IntrospectionInputValue>,
249
250    /// Whether the directive is repeatable.
251    #[serde(default)]
252    pub is_repeatable: bool,
253}
254
255/// `__TypeKind` enum per GraphQL spec §4.1.4.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
258pub enum TypeKind {
259    /// A scalar type (Int, String, Boolean, etc.)
260    Scalar,
261    /// An object type with fields.
262    Object,
263    /// An abstract interface type.
264    Interface,
265    /// A union of multiple object types.
266    Union,
267    /// An enumeration type.
268    Enum,
269    /// An input object type for mutations.
270    InputObject,
271    /// A list wrapper type.
272    List,
273    /// A non-null wrapper type.
274    NonNull,
275}
276
277/// `__DirectiveLocation` enum per GraphQL spec §4.1.5.
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
280pub enum DirectiveLocation {
281    /// Directive on query operation.
282    Query,
283    /// Directive on mutation operation.
284    Mutation,
285    /// Directive on subscription operation.
286    Subscription,
287    /// Directive on field selection.
288    Field,
289    /// Directive on fragment definition.
290    FragmentDefinition,
291    /// Directive on fragment spread.
292    FragmentSpread,
293    /// Directive on inline fragment.
294    InlineFragment,
295    /// Directive on variable definition.
296    VariableDefinition,
297    /// Directive on schema definition.
298    Schema,
299    /// Directive on scalar type definition.
300    Scalar,
301    /// Directive on object type definition.
302    Object,
303    /// Directive on field definition.
304    FieldDefinition,
305    /// Directive on argument definition.
306    ArgumentDefinition,
307    /// Directive on interface definition.
308    Interface,
309    /// Directive on union definition.
310    Union,
311    /// Directive on enum definition.
312    Enum,
313    /// Directive on enum value definition.
314    EnumValue,
315    /// Directive on input object definition.
316    InputObject,
317    /// Directive on input field definition.
318    InputFieldDefinition,
319}
320
321impl From<DirectiveLocationKind> for DirectiveLocation {
322    fn from(kind: DirectiveLocationKind) -> Self {
323        match kind {
324            DirectiveLocationKind::Query => Self::Query,
325            DirectiveLocationKind::Mutation => Self::Mutation,
326            DirectiveLocationKind::Subscription => Self::Subscription,
327            DirectiveLocationKind::Field => Self::Field,
328            DirectiveLocationKind::FragmentDefinition => Self::FragmentDefinition,
329            DirectiveLocationKind::FragmentSpread => Self::FragmentSpread,
330            DirectiveLocationKind::InlineFragment => Self::InlineFragment,
331            DirectiveLocationKind::VariableDefinition => Self::VariableDefinition,
332            DirectiveLocationKind::Schema => Self::Schema,
333            DirectiveLocationKind::Scalar => Self::Scalar,
334            DirectiveLocationKind::Object => Self::Object,
335            DirectiveLocationKind::FieldDefinition => Self::FieldDefinition,
336            DirectiveLocationKind::ArgumentDefinition => Self::ArgumentDefinition,
337            DirectiveLocationKind::Interface => Self::Interface,
338            DirectiveLocationKind::Union => Self::Union,
339            DirectiveLocationKind::Enum => Self::Enum,
340            DirectiveLocationKind::EnumValue => Self::EnumValue,
341            DirectiveLocationKind::InputObject => Self::InputObject,
342            DirectiveLocationKind::InputFieldDefinition => Self::InputFieldDefinition,
343        }
344    }
345}
346
347// =============================================================================
348// Introspection Builder
349// =============================================================================
350
351/// Builds introspection schema from compiled schema.
352pub struct IntrospectionBuilder;
353
354impl IntrospectionBuilder {
355    /// Build complete introspection schema from compiled schema.
356    #[must_use]
357    pub fn build(schema: &CompiledSchema) -> IntrospectionSchema {
358        let mut types = Vec::new();
359
360        // Add built-in scalar types
361        types.extend(Self::builtin_scalars());
362
363        // Add user-defined types
364        for type_def in &schema.types {
365            types.push(Self::build_object_type(type_def));
366        }
367
368        // Add enum types
369        for enum_def in &schema.enums {
370            types.push(Self::build_enum_type(enum_def));
371        }
372
373        // Add input object types
374        for input_def in &schema.input_types {
375            types.push(Self::build_input_object_type(input_def));
376        }
377
378        // Add interface types
379        for interface_def in &schema.interfaces {
380            types.push(Self::build_interface_type(interface_def, schema));
381        }
382
383        // Add union types
384        for union_def in &schema.unions {
385            types.push(Self::build_union_type(union_def));
386        }
387
388        // Add Query root type
389        types.push(Self::build_query_type(schema));
390
391        // Add Mutation root type if mutations exist
392        if !schema.mutations.is_empty() {
393            types.push(Self::build_mutation_type(schema));
394        }
395
396        // Add Subscription root type if subscriptions exist
397        if !schema.subscriptions.is_empty() {
398            types.push(Self::build_subscription_type(schema));
399        }
400
401        // Build directives: built-in + custom
402        let mut directives = Self::builtin_directives();
403        directives.extend(Self::build_custom_directives(&schema.directives));
404
405        IntrospectionSchema {
406            description: Some("FraiseQL GraphQL Schema".to_string()),
407            types,
408            query_type: IntrospectionTypeRef {
409                name: "Query".to_string(),
410            },
411            mutation_type: if schema.mutations.is_empty() {
412                None
413            } else {
414                Some(IntrospectionTypeRef {
415                    name: "Mutation".to_string(),
416                })
417            },
418            subscription_type: if schema.subscriptions.is_empty() {
419                None
420            } else {
421                Some(IntrospectionTypeRef {
422                    name: "Subscription".to_string(),
423                })
424            },
425            directives,
426        }
427    }
428
429    /// Build a lookup map for `__type(name:)` queries.
430    #[must_use]
431    pub fn build_type_map(schema: &IntrospectionSchema) -> HashMap<String, IntrospectionType> {
432        let mut map = HashMap::new();
433        for t in &schema.types {
434            if let Some(ref name) = t.name {
435                map.insert(name.clone(), t.clone());
436            }
437        }
438        map
439    }
440
441    /// Built-in GraphQL scalar types.
442    fn builtin_scalars() -> Vec<IntrospectionType> {
443        vec![
444            Self::scalar_type("Int", "Built-in Int scalar"),
445            Self::scalar_type("Float", "Built-in Float scalar"),
446            Self::scalar_type("String", "Built-in String scalar"),
447            Self::scalar_type("Boolean", "Built-in Boolean scalar"),
448            Self::scalar_type("ID", "Built-in ID scalar"),
449            // FraiseQL custom scalars (with specifiedByURL per GraphQL spec §3.5.5)
450            Self::scalar_type_with_url(
451                "DateTime",
452                "ISO-8601 datetime string",
453                Some("https://scalars.graphql.org/andimarek/date-time"),
454            ),
455            Self::scalar_type_with_url(
456                "Date",
457                "ISO-8601 date string",
458                Some("https://scalars.graphql.org/andimarek/local-date"),
459            ),
460            Self::scalar_type_with_url(
461                "Time",
462                "ISO-8601 time string",
463                Some("https://scalars.graphql.org/andimarek/local-time"),
464            ),
465            Self::scalar_type_with_url(
466                "UUID",
467                "UUID string",
468                Some("https://tools.ietf.org/html/rfc4122"),
469            ),
470            Self::scalar_type_with_url(
471                "JSON",
472                "Arbitrary JSON value",
473                Some("https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"),
474            ),
475            Self::scalar_type("Decimal", "Decimal number"),
476        ]
477    }
478
479    /// Create a scalar type introspection.
480    fn scalar_type(name: &str, description: &str) -> IntrospectionType {
481        Self::scalar_type_with_url(name, description, None)
482    }
483
484    /// Create a scalar type introspection with optional `specifiedByURL`.
485    fn scalar_type_with_url(
486        name: &str,
487        description: &str,
488        specified_by_url: Option<&str>,
489    ) -> IntrospectionType {
490        IntrospectionType {
491            kind:               TypeKind::Scalar,
492            name:               Some(name.to_string()),
493            description:        Some(description.to_string()),
494            fields:             None,
495            interfaces:         None,
496            possible_types:     None,
497            enum_values:        None,
498            input_fields:       None,
499            of_type:            None,
500            specified_by_u_r_l: specified_by_url.map(ToString::to_string),
501        }
502    }
503
504    /// Build object type from TypeDefinition.
505    fn build_object_type(type_def: &TypeDefinition) -> IntrospectionType {
506        let fields = type_def.fields.iter().map(|f| Self::build_field(f)).collect();
507
508        // Build interfaces that this type implements
509        let interfaces: Vec<IntrospectionTypeRef> = type_def
510            .implements
511            .iter()
512            .map(|name| IntrospectionTypeRef { name: name.clone() })
513            .collect();
514
515        IntrospectionType {
516            kind:               TypeKind::Object,
517            name:               Some(type_def.name.clone()),
518            description:        type_def.description.clone(),
519            fields:             Some(fields),
520            interfaces:         Some(interfaces),
521            possible_types:     None,
522            enum_values:        None,
523            input_fields:       None,
524            of_type:            None,
525            specified_by_u_r_l: None,
526        }
527    }
528
529    /// Build enum type from EnumDefinition.
530    fn build_enum_type(enum_def: &EnumDefinition) -> IntrospectionType {
531        let enum_values = enum_def
532            .values
533            .iter()
534            .map(|v| IntrospectionEnumValue {
535                name:               v.name.clone(),
536                description:        v.description.clone(),
537                is_deprecated:      v.deprecation.is_some(),
538                deprecation_reason: v.deprecation.as_ref().and_then(|d| d.reason.clone()),
539            })
540            .collect();
541
542        IntrospectionType {
543            kind:               TypeKind::Enum,
544            name:               Some(enum_def.name.clone()),
545            description:        enum_def.description.clone(),
546            fields:             None,
547            interfaces:         None,
548            possible_types:     None,
549            enum_values:        Some(enum_values),
550            input_fields:       None,
551            of_type:            None,
552            specified_by_u_r_l: None,
553        }
554    }
555
556    /// Build input object type from InputObjectDefinition.
557    fn build_input_object_type(input_def: &InputObjectDefinition) -> IntrospectionType {
558        let input_fields = input_def
559            .fields
560            .iter()
561            .map(|f| IntrospectionInputValue {
562                name:               f.name.clone(),
563                description:        f.description.clone(),
564                input_type:         Self::type_ref(&f.field_type),
565                default_value:      f.default_value.clone(),
566                is_deprecated:      f.is_deprecated(),
567                deprecation_reason: f.deprecation.as_ref().and_then(|d| d.reason.clone()),
568            })
569            .collect();
570
571        IntrospectionType {
572            kind:               TypeKind::InputObject,
573            name:               Some(input_def.name.clone()),
574            description:        input_def.description.clone(),
575            fields:             None,
576            interfaces:         None,
577            possible_types:     None,
578            enum_values:        None,
579            input_fields:       Some(input_fields),
580            of_type:            None,
581            specified_by_u_r_l: None,
582        }
583    }
584
585    /// Build interface type from InterfaceDefinition.
586    fn build_interface_type(
587        interface_def: &InterfaceDefinition,
588        schema: &CompiledSchema,
589    ) -> IntrospectionType {
590        // Build fields for the interface
591        let fields = interface_def.fields.iter().map(|f| Self::build_field(f)).collect();
592
593        // Find all types that implement this interface
594        let possible_types: Vec<IntrospectionTypeRef> = schema
595            .find_implementors(&interface_def.name)
596            .iter()
597            .map(|t| IntrospectionTypeRef {
598                name: t.name.clone(),
599            })
600            .collect();
601
602        IntrospectionType {
603            kind:               TypeKind::Interface,
604            name:               Some(interface_def.name.clone()),
605            description:        interface_def.description.clone(),
606            fields:             Some(fields),
607            interfaces:         None,
608            possible_types:     if possible_types.is_empty() {
609                None
610            } else {
611                Some(possible_types)
612            },
613            enum_values:        None,
614            input_fields:       None,
615            of_type:            None,
616            specified_by_u_r_l: None,
617        }
618    }
619
620    /// Build union type from UnionDefinition.
621    fn build_union_type(union_def: &UnionDefinition) -> IntrospectionType {
622        // Build possible types for the union
623        let possible_types: Vec<IntrospectionTypeRef> = union_def
624            .member_types
625            .iter()
626            .map(|name| IntrospectionTypeRef { name: name.clone() })
627            .collect();
628
629        IntrospectionType {
630            kind:               TypeKind::Union,
631            name:               Some(union_def.name.clone()),
632            description:        union_def.description.clone(),
633            fields:             None, // Unions don't have fields
634            interfaces:         None,
635            possible_types:     if possible_types.is_empty() {
636                None
637            } else {
638                Some(possible_types)
639            },
640            enum_values:        None,
641            input_fields:       None,
642            of_type:            None,
643            specified_by_u_r_l: None,
644        }
645    }
646
647    /// Build field introspection from FieldDefinition.
648    fn build_field(field: &FieldDefinition) -> IntrospectionField {
649        IntrospectionField {
650            name:               field.output_name().to_string(),
651            description:        field.description.clone(),
652            args:               vec![], // Regular fields don't have args
653            field_type:         Self::field_type_to_introspection(
654                &field.field_type,
655                field.nullable,
656            ),
657            is_deprecated:      field.is_deprecated(),
658            deprecation_reason: field.deprecation_reason().map(ToString::to_string),
659        }
660    }
661
662    /// Convert FieldType to introspection type.
663    fn field_type_to_introspection(field_type: &FieldType, nullable: bool) -> IntrospectionType {
664        let inner = match field_type {
665            FieldType::Int => Self::type_ref("Int"),
666            FieldType::Float => Self::type_ref("Float"),
667            FieldType::String => Self::type_ref("String"),
668            FieldType::Boolean => Self::type_ref("Boolean"),
669            FieldType::Id => Self::type_ref("ID"),
670            FieldType::DateTime => Self::type_ref("DateTime"),
671            FieldType::Date => Self::type_ref("Date"),
672            FieldType::Time => Self::type_ref("Time"),
673            FieldType::Uuid => Self::type_ref("UUID"),
674            FieldType::Json => Self::type_ref("JSON"),
675            FieldType::Decimal => Self::type_ref("Decimal"),
676            FieldType::Object(name) => Self::type_ref(name),
677            FieldType::Enum(name) => Self::type_ref(name),
678            FieldType::Input(name) => Self::type_ref(name),
679            FieldType::Interface(name) => Self::type_ref(name),
680            FieldType::Union(name) => Self::type_ref(name),
681            FieldType::Scalar(name) => Self::type_ref(name), // Rich/custom scalars
682            FieldType::List(inner) => IntrospectionType {
683                kind:               TypeKind::List,
684                name:               None,
685                description:        None,
686                fields:             None,
687                interfaces:         None,
688                possible_types:     None,
689                enum_values:        None,
690                input_fields:       None,
691                of_type:            Some(Box::new(Self::field_type_to_introspection(inner, true))),
692                specified_by_u_r_l: None,
693            },
694            FieldType::Vector => Self::type_ref("JSON"), // Vectors are exposed as JSON
695        };
696
697        if nullable {
698            inner
699        } else {
700            // Wrap in NON_NULL
701            IntrospectionType {
702                kind:               TypeKind::NonNull,
703                name:               None,
704                description:        None,
705                fields:             None,
706                interfaces:         None,
707                possible_types:     None,
708                enum_values:        None,
709                input_fields:       None,
710                of_type:            Some(Box::new(inner)),
711                specified_by_u_r_l: None,
712            }
713        }
714    }
715
716    /// Create a type reference.
717    fn type_ref(name: &str) -> IntrospectionType {
718        IntrospectionType {
719            kind:               TypeKind::Scalar, // Will be overwritten if it's an object
720            name:               Some(name.to_string()),
721            description:        None,
722            fields:             None,
723            interfaces:         None,
724            possible_types:     None,
725            enum_values:        None,
726            input_fields:       None,
727            of_type:            None,
728            specified_by_u_r_l: None,
729        }
730    }
731
732    /// Build Query root type.
733    fn build_query_type(schema: &CompiledSchema) -> IntrospectionType {
734        let fields: Vec<IntrospectionField> =
735            schema.queries.iter().map(|q| Self::build_query_field(q)).collect();
736
737        IntrospectionType {
738            kind:               TypeKind::Object,
739            name:               Some("Query".to_string()),
740            description:        Some("Root query type".to_string()),
741            fields:             Some(fields),
742            interfaces:         Some(vec![]),
743            possible_types:     None,
744            enum_values:        None,
745            input_fields:       None,
746            of_type:            None,
747            specified_by_u_r_l: None,
748        }
749    }
750
751    /// Build Mutation root type.
752    fn build_mutation_type(schema: &CompiledSchema) -> IntrospectionType {
753        let fields: Vec<IntrospectionField> =
754            schema.mutations.iter().map(|m| Self::build_mutation_field(m)).collect();
755
756        IntrospectionType {
757            kind:               TypeKind::Object,
758            name:               Some("Mutation".to_string()),
759            description:        Some("Root mutation type".to_string()),
760            fields:             Some(fields),
761            interfaces:         Some(vec![]),
762            possible_types:     None,
763            enum_values:        None,
764            input_fields:       None,
765            of_type:            None,
766            specified_by_u_r_l: None,
767        }
768    }
769
770    /// Build Subscription root type.
771    fn build_subscription_type(schema: &CompiledSchema) -> IntrospectionType {
772        let fields: Vec<IntrospectionField> =
773            schema.subscriptions.iter().map(|s| Self::build_subscription_field(s)).collect();
774
775        IntrospectionType {
776            kind:               TypeKind::Object,
777            name:               Some("Subscription".to_string()),
778            description:        Some("Root subscription type".to_string()),
779            fields:             Some(fields),
780            interfaces:         Some(vec![]),
781            possible_types:     None,
782            enum_values:        None,
783            input_fields:       None,
784            of_type:            None,
785            specified_by_u_r_l: None,
786        }
787    }
788
789    /// Build query field introspection.
790    fn build_query_field(query: &QueryDefinition) -> IntrospectionField {
791        let return_type = Self::type_ref(&query.return_type);
792        let return_type = if query.returns_list {
793            IntrospectionType {
794                kind:               TypeKind::List,
795                name:               None,
796                description:        None,
797                fields:             None,
798                interfaces:         None,
799                possible_types:     None,
800                enum_values:        None,
801                input_fields:       None,
802                of_type:            Some(Box::new(return_type)),
803                specified_by_u_r_l: None,
804            }
805        } else {
806            return_type
807        };
808
809        let return_type = if query.nullable {
810            return_type
811        } else {
812            IntrospectionType {
813                kind:               TypeKind::NonNull,
814                name:               None,
815                description:        None,
816                fields:             None,
817                interfaces:         None,
818                possible_types:     None,
819                enum_values:        None,
820                input_fields:       None,
821                of_type:            Some(Box::new(return_type)),
822                specified_by_u_r_l: None,
823            }
824        };
825
826        // Build arguments
827        let args: Vec<IntrospectionInputValue> = query
828            .arguments
829            .iter()
830            .map(|arg| IntrospectionInputValue {
831                name:               arg.name.clone(),
832                description:        arg.description.clone(),
833                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
834                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
835                is_deprecated:      arg.is_deprecated(),
836                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
837            })
838            .collect();
839
840        IntrospectionField {
841            name: query.name.clone(),
842            description: query.description.clone(),
843            args,
844            field_type: return_type,
845            is_deprecated: query.is_deprecated(),
846            deprecation_reason: query.deprecation_reason().map(ToString::to_string),
847        }
848    }
849
850    /// Build mutation field introspection.
851    fn build_mutation_field(mutation: &super::MutationDefinition) -> IntrospectionField {
852        // Mutations always return a single object (not a list)
853        let return_type = Self::type_ref(&mutation.return_type);
854
855        // Build arguments
856        let args: Vec<IntrospectionInputValue> = mutation
857            .arguments
858            .iter()
859            .map(|arg| IntrospectionInputValue {
860                name:               arg.name.clone(),
861                description:        arg.description.clone(),
862                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
863                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
864                is_deprecated:      arg.is_deprecated(),
865                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
866            })
867            .collect();
868
869        IntrospectionField {
870            name: mutation.name.clone(),
871            description: mutation.description.clone(),
872            args,
873            field_type: return_type,
874            is_deprecated: mutation.is_deprecated(),
875            deprecation_reason: mutation.deprecation_reason().map(ToString::to_string),
876        }
877    }
878
879    /// Build subscription field introspection.
880    fn build_subscription_field(
881        subscription: &super::SubscriptionDefinition,
882    ) -> IntrospectionField {
883        // Subscriptions typically return a single item per event
884        let return_type = Self::type_ref(&subscription.return_type);
885
886        // Build arguments
887        let args: Vec<IntrospectionInputValue> = subscription
888            .arguments
889            .iter()
890            .map(|arg| IntrospectionInputValue {
891                name:               arg.name.clone(),
892                description:        arg.description.clone(),
893                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
894                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
895                is_deprecated:      arg.is_deprecated(),
896                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
897            })
898            .collect();
899
900        IntrospectionField {
901            name: subscription.name.clone(),
902            description: subscription.description.clone(),
903            args,
904            field_type: return_type,
905            is_deprecated: subscription.is_deprecated(),
906            deprecation_reason: subscription.deprecation_reason().map(ToString::to_string),
907        }
908    }
909
910    /// Built-in GraphQL directives.
911    fn builtin_directives() -> Vec<IntrospectionDirective> {
912        vec![
913            IntrospectionDirective {
914                name: "skip".to_string(),
915                description: Some(
916                    "Directs the executor to skip this field or fragment when the `if` argument is true."
917                        .to_string(),
918                ),
919                locations: vec![
920                    DirectiveLocation::Field,
921                    DirectiveLocation::FragmentSpread,
922                    DirectiveLocation::InlineFragment,
923                ],
924                args: vec![IntrospectionInputValue {
925                    name: "if".to_string(),
926                    description: Some("Skipped when true.".to_string()),
927                    input_type: IntrospectionType {
928                        kind: TypeKind::NonNull,
929                        name: None,
930                        description: None,
931                        fields: None,
932                        interfaces: None,
933                        possible_types: None,
934                        enum_values: None,
935                        input_fields: None,
936                        of_type: Some(Box::new(Self::type_ref("Boolean"))),
937                        specified_by_u_r_l: None,
938                    },
939                    default_value: None,
940                    is_deprecated: false,
941                    deprecation_reason: None,
942                }],
943                is_repeatable: false,
944            },
945            IntrospectionDirective {
946                name: "include".to_string(),
947                description: Some(
948                    "Directs the executor to include this field or fragment only when the `if` argument is true."
949                        .to_string(),
950                ),
951                locations: vec![
952                    DirectiveLocation::Field,
953                    DirectiveLocation::FragmentSpread,
954                    DirectiveLocation::InlineFragment,
955                ],
956                args: vec![IntrospectionInputValue {
957                    name: "if".to_string(),
958                    description: Some("Included when true.".to_string()),
959                    input_type: IntrospectionType {
960                        kind: TypeKind::NonNull,
961                        name: None,
962                        description: None,
963                        fields: None,
964                        interfaces: None,
965                        possible_types: None,
966                        enum_values: None,
967                        input_fields: None,
968                        of_type: Some(Box::new(Self::type_ref("Boolean"))),
969                        specified_by_u_r_l: None,
970                    },
971                    default_value: None,
972                    is_deprecated: false,
973                    deprecation_reason: None,
974                }],
975                is_repeatable: false,
976            },
977            IntrospectionDirective {
978                name: "deprecated".to_string(),
979                description: Some(
980                    "Marks an element of a GraphQL schema as no longer supported.".to_string(),
981                ),
982                locations: vec![
983                    DirectiveLocation::FieldDefinition,
984                    DirectiveLocation::EnumValue,
985                    DirectiveLocation::ArgumentDefinition,
986                    DirectiveLocation::InputFieldDefinition,
987                ],
988                args: vec![IntrospectionInputValue {
989                    name: "reason".to_string(),
990                    description: Some(
991                        "Explains why this element was deprecated.".to_string(),
992                    ),
993                    input_type: Self::type_ref("String"),
994                    default_value: Some("\"No longer supported\"".to_string()),
995                    is_deprecated: false,
996                    deprecation_reason: None,
997                }],
998                is_repeatable: false,
999            },
1000        ]
1001    }
1002
1003    /// Build introspection directives from custom directive definitions.
1004    fn build_custom_directives(directives: &[DirectiveDefinition]) -> Vec<IntrospectionDirective> {
1005        directives.iter().map(|d| Self::build_custom_directive(d)).collect()
1006    }
1007
1008    /// Build a single introspection directive from a custom directive definition.
1009    fn build_custom_directive(directive: &DirectiveDefinition) -> IntrospectionDirective {
1010        let locations: Vec<DirectiveLocation> =
1011            directive.locations.iter().map(|loc| DirectiveLocation::from(*loc)).collect();
1012
1013        let args: Vec<IntrospectionInputValue> = directive
1014            .arguments
1015            .iter()
1016            .map(|arg| IntrospectionInputValue {
1017                name:               arg.name.clone(),
1018                description:        arg.description.clone(),
1019                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1020                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
1021                is_deprecated:      arg.is_deprecated(),
1022                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1023            })
1024            .collect();
1025
1026        IntrospectionDirective {
1027            name: directive.name.clone(),
1028            description: directive.description.clone(),
1029            locations,
1030            args,
1031            is_repeatable: directive.is_repeatable,
1032        }
1033    }
1034}
1035
1036// =============================================================================
1037// Introspection Response Wrapper
1038// =============================================================================
1039
1040/// Pre-built introspection responses for fast serving.
1041#[derive(Debug, Clone)]
1042pub struct IntrospectionResponses {
1043    /// Full `__schema` response JSON.
1044    pub schema_response: String,
1045    /// Map of type name -> `__type` response JSON.
1046    pub type_responses:  HashMap<String, String>,
1047}
1048
1049impl IntrospectionResponses {
1050    /// Build introspection responses from compiled schema.
1051    ///
1052    /// This is called once at server startup and cached.
1053    #[must_use]
1054    pub fn build(schema: &CompiledSchema) -> Self {
1055        let introspection = IntrospectionBuilder::build(schema);
1056        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1057
1058        // Build __schema response
1059        let schema_response = serde_json::json!({
1060            "data": {
1061                "__schema": introspection
1062            }
1063        })
1064        .to_string();
1065
1066        // Build __type responses for each type
1067        let mut type_responses = HashMap::new();
1068        for (name, t) in type_map {
1069            let response = serde_json::json!({
1070                "data": {
1071                    "__type": t
1072                }
1073            })
1074            .to_string();
1075            type_responses.insert(name, response);
1076        }
1077
1078        Self {
1079            schema_response,
1080            type_responses,
1081        }
1082    }
1083
1084    /// Get response for `__type(name: "...")` query.
1085    #[must_use]
1086    pub fn get_type_response(&self, type_name: &str) -> String {
1087        self.type_responses.get(type_name).cloned().unwrap_or_else(|| {
1088            serde_json::json!({
1089                "data": {
1090                    "__type": null
1091                }
1092            })
1093            .to_string()
1094        })
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::*;
1101    use crate::schema::{AutoParams, FieldType};
1102
1103    fn test_schema() -> CompiledSchema {
1104        let mut schema = CompiledSchema::new();
1105
1106        // Add a User type
1107        schema.types.push(
1108            TypeDefinition::new("User", "v_user")
1109                .with_field(FieldDefinition::new("id", FieldType::Id))
1110                .with_field(FieldDefinition::new("name", FieldType::String))
1111                .with_field(FieldDefinition::nullable("email", FieldType::String))
1112                .with_description("A user in the system"),
1113        );
1114
1115        // Add a users query
1116        schema.queries.push(QueryDefinition {
1117            name:         "users".to_string(),
1118            return_type:  "User".to_string(),
1119            returns_list: true,
1120            nullable:     false,
1121            arguments:    vec![],
1122            sql_source:   Some("v_user".to_string()),
1123            description:  Some("Get all users".to_string()),
1124            auto_params:  AutoParams::default(),
1125            deprecation:  None,
1126        });
1127
1128        // Add a user query with argument
1129        schema.queries.push(QueryDefinition {
1130            name:         "user".to_string(),
1131            return_type:  "User".to_string(),
1132            returns_list: false,
1133            nullable:     true,
1134            arguments:    vec![crate::schema::ArgumentDefinition {
1135                name:          "id".to_string(),
1136                arg_type:      FieldType::Id,
1137                nullable:      false, // required
1138                default_value: None,
1139                description:   Some("User ID".to_string()),
1140                deprecation:   None,
1141            }],
1142            sql_source:   Some("v_user".to_string()),
1143            description:  Some("Get user by ID".to_string()),
1144            auto_params:  AutoParams::default(),
1145            deprecation:  None,
1146        });
1147
1148        schema
1149    }
1150
1151    #[test]
1152    fn test_build_introspection_schema() {
1153        let schema = test_schema();
1154        let introspection = IntrospectionBuilder::build(&schema);
1155
1156        // Should have Query type
1157        assert_eq!(introspection.query_type.name, "Query");
1158
1159        // Should not have Mutation type (no mutations)
1160        assert!(introspection.mutation_type.is_none());
1161
1162        // Should have built-in scalars
1163        let scalar_names: Vec<_> = introspection
1164            .types
1165            .iter()
1166            .filter(|t| t.kind == TypeKind::Scalar)
1167            .filter_map(|t| t.name.as_ref())
1168            .collect();
1169        assert!(scalar_names.contains(&&"Int".to_string()));
1170        assert!(scalar_names.contains(&&"String".to_string()));
1171        assert!(scalar_names.contains(&&"Boolean".to_string()));
1172
1173        // Should have User type
1174        let user_type = introspection
1175            .types
1176            .iter()
1177            .find(|t| t.name.as_ref() == Some(&"User".to_string()));
1178        assert!(user_type.is_some());
1179        let user_type = user_type.unwrap();
1180        assert_eq!(user_type.kind, TypeKind::Object);
1181        assert!(user_type.fields.is_some());
1182        assert_eq!(user_type.fields.as_ref().unwrap().len(), 3);
1183    }
1184
1185    #[test]
1186    fn test_build_introspection_responses() {
1187        let schema = test_schema();
1188        let responses = IntrospectionResponses::build(&schema);
1189
1190        // Should have schema response
1191        assert!(responses.schema_response.contains("__schema"));
1192        assert!(responses.schema_response.contains("Query"));
1193
1194        // Should have type responses
1195        assert!(responses.type_responses.contains_key("User"));
1196        assert!(responses.type_responses.contains_key("Query"));
1197        assert!(responses.type_responses.contains_key("Int"));
1198
1199        // Unknown type should return null
1200        let unknown = responses.get_type_response("Unknown");
1201        assert!(unknown.contains("null"));
1202    }
1203
1204    #[test]
1205    fn test_query_field_introspection() {
1206        let schema = test_schema();
1207        let introspection = IntrospectionBuilder::build(&schema);
1208
1209        let query_type = introspection
1210            .types
1211            .iter()
1212            .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
1213            .unwrap();
1214
1215        let fields = query_type.fields.as_ref().unwrap();
1216
1217        // Should have 'users' query
1218        let users_field = fields.iter().find(|f| f.name == "users").unwrap();
1219        assert_eq!(users_field.field_type.kind, TypeKind::NonNull);
1220        assert!(users_field.args.is_empty());
1221
1222        // Should have 'user' query with argument
1223        let user_field = fields.iter().find(|f| f.name == "user").unwrap();
1224        assert!(!user_field.args.is_empty());
1225        assert_eq!(user_field.args[0].name, "id");
1226    }
1227
1228    #[test]
1229    fn test_field_type_non_null() {
1230        let schema = test_schema();
1231        let introspection = IntrospectionBuilder::build(&schema);
1232
1233        let user_type = introspection
1234            .types
1235            .iter()
1236            .find(|t| t.name.as_ref() == Some(&"User".to_string()))
1237            .unwrap();
1238
1239        let fields = user_type.fields.as_ref().unwrap();
1240
1241        // 'id' should be NON_NULL
1242        let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1243        assert_eq!(id_field.field_type.kind, TypeKind::NonNull);
1244
1245        // 'email' should be nullable (not wrapped in NON_NULL)
1246        let email_field = fields.iter().find(|f| f.name == "email").unwrap();
1247        assert_ne!(email_field.field_type.kind, TypeKind::NonNull);
1248    }
1249
1250    #[test]
1251    fn test_deprecated_field_introspection() {
1252        use crate::schema::DeprecationInfo;
1253
1254        // Create a schema with a deprecated field
1255        let mut schema = CompiledSchema::new();
1256        schema.types.push(TypeDefinition {
1257            name:                "Product".to_string(),
1258            sql_source:          "products".to_string(),
1259            jsonb_column:        "data".to_string(),
1260            description:         None,
1261            sql_projection_hint: None,
1262            implements:          vec![],
1263            fields:              vec![
1264                FieldDefinition::new("id", FieldType::Id),
1265                FieldDefinition {
1266                    name:           "oldSku".to_string(),
1267                    field_type:     FieldType::String,
1268                    nullable:       false,
1269                    description:    Some("Legacy SKU field".to_string()),
1270                    default_value:  None,
1271                    vector_config:  None,
1272                    alias:          None,
1273                    deprecation:    Some(DeprecationInfo {
1274                        reason: Some("Use 'sku' instead".to_string()),
1275                    }),
1276                    requires_scope: None,
1277                },
1278                FieldDefinition::new("sku", FieldType::String),
1279            ],
1280        });
1281
1282        let introspection = IntrospectionBuilder::build(&schema);
1283
1284        // Find Product type
1285        let product_type = introspection
1286            .types
1287            .iter()
1288            .find(|t| t.name.as_ref() == Some(&"Product".to_string()))
1289            .unwrap();
1290
1291        let fields = product_type.fields.as_ref().unwrap();
1292
1293        // 'oldSku' should be deprecated
1294        let old_sku_field = fields.iter().find(|f| f.name == "oldSku").unwrap();
1295        assert!(old_sku_field.is_deprecated);
1296        assert_eq!(old_sku_field.deprecation_reason, Some("Use 'sku' instead".to_string()));
1297
1298        // 'sku' should NOT be deprecated
1299        let sku_field = fields.iter().find(|f| f.name == "sku").unwrap();
1300        assert!(!sku_field.is_deprecated);
1301        assert!(sku_field.deprecation_reason.is_none());
1302
1303        // 'id' should NOT be deprecated
1304        let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1305        assert!(!id_field.is_deprecated);
1306        assert!(id_field.deprecation_reason.is_none());
1307    }
1308
1309    #[test]
1310    fn test_enum_type_introspection() {
1311        use crate::schema::{EnumDefinition, EnumValueDefinition};
1312
1313        let mut schema = CompiledSchema::new();
1314
1315        // Add an enum type with some values, one deprecated
1316        schema.enums.push(EnumDefinition {
1317            name:        "OrderStatus".to_string(),
1318            description: Some("Status of an order".to_string()),
1319            values:      vec![
1320                EnumValueDefinition {
1321                    name:        "PENDING".to_string(),
1322                    description: Some("Order is pending".to_string()),
1323                    deprecation: None,
1324                },
1325                EnumValueDefinition {
1326                    name:        "PROCESSING".to_string(),
1327                    description: None,
1328                    deprecation: None,
1329                },
1330                EnumValueDefinition {
1331                    name:        "SHIPPED".to_string(),
1332                    description: None,
1333                    deprecation: None,
1334                },
1335                EnumValueDefinition {
1336                    name:        "CANCELLED".to_string(),
1337                    description: Some("Order was cancelled".to_string()),
1338                    deprecation: Some(crate::schema::DeprecationInfo {
1339                        reason: Some("Use REFUNDED instead".to_string()),
1340                    }),
1341                },
1342            ],
1343        });
1344
1345        let introspection = IntrospectionBuilder::build(&schema);
1346
1347        // Find OrderStatus enum
1348        let order_status = introspection
1349            .types
1350            .iter()
1351            .find(|t| t.name.as_ref() == Some(&"OrderStatus".to_string()))
1352            .unwrap();
1353
1354        assert_eq!(order_status.kind, TypeKind::Enum);
1355        assert_eq!(order_status.description, Some("Status of an order".to_string()));
1356
1357        // Should have enum_values
1358        let enum_values = order_status.enum_values.as_ref().unwrap();
1359        assert_eq!(enum_values.len(), 4);
1360
1361        // Check PENDING value
1362        let pending = enum_values.iter().find(|v| v.name == "PENDING").unwrap();
1363        assert_eq!(pending.description, Some("Order is pending".to_string()));
1364        assert!(!pending.is_deprecated);
1365        assert!(pending.deprecation_reason.is_none());
1366
1367        // Check CANCELLED value (deprecated)
1368        let cancelled = enum_values.iter().find(|v| v.name == "CANCELLED").unwrap();
1369        assert!(cancelled.is_deprecated);
1370        assert_eq!(cancelled.deprecation_reason, Some("Use REFUNDED instead".to_string()));
1371
1372        // Enum should not have fields
1373        assert!(order_status.fields.is_none());
1374    }
1375
1376    #[test]
1377    fn test_input_object_introspection() {
1378        use crate::schema::{InputFieldDefinition, InputObjectDefinition};
1379
1380        let mut schema = CompiledSchema::new();
1381
1382        // Add an input object type
1383        schema.input_types.push(InputObjectDefinition {
1384            name:        "UserFilter".to_string(),
1385            description: Some("Filter for user queries".to_string()),
1386            fields:      vec![
1387                InputFieldDefinition {
1388                    name:          "name".to_string(),
1389                    field_type:    "String".to_string(),
1390                    description:   Some("Filter by name".to_string()),
1391                    default_value: None,
1392                    deprecation:   None,
1393                },
1394                InputFieldDefinition {
1395                    name:          "email".to_string(),
1396                    field_type:    "String".to_string(),
1397                    description:   None,
1398                    default_value: None,
1399                    deprecation:   None,
1400                },
1401                InputFieldDefinition {
1402                    name:          "limit".to_string(),
1403                    field_type:    "Int".to_string(),
1404                    description:   Some("Max results".to_string()),
1405                    default_value: Some("10".to_string()),
1406                    deprecation:   None,
1407                },
1408            ],
1409        });
1410
1411        let introspection = IntrospectionBuilder::build(&schema);
1412
1413        // Find UserFilter input type
1414        let user_filter = introspection
1415            .types
1416            .iter()
1417            .find(|t| t.name.as_ref() == Some(&"UserFilter".to_string()))
1418            .unwrap();
1419
1420        assert_eq!(user_filter.kind, TypeKind::InputObject);
1421        assert_eq!(user_filter.description, Some("Filter for user queries".to_string()));
1422
1423        // Should have input_fields
1424        let input_fields = user_filter.input_fields.as_ref().unwrap();
1425        assert_eq!(input_fields.len(), 3);
1426
1427        // Check name field
1428        let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
1429        assert_eq!(name_field.description, Some("Filter by name".to_string()));
1430        assert!(name_field.default_value.is_none());
1431
1432        // Check limit field with default value
1433        let limit_field = input_fields.iter().find(|f| f.name == "limit").unwrap();
1434        assert_eq!(limit_field.description, Some("Max results".to_string()));
1435        assert_eq!(limit_field.default_value, Some("10".to_string()));
1436
1437        // Input object should not have fields
1438        assert!(user_filter.fields.is_none());
1439    }
1440
1441    #[test]
1442    fn test_enum_in_type_map() {
1443        use crate::schema::EnumDefinition;
1444
1445        let mut schema = CompiledSchema::new();
1446        schema.enums.push(EnumDefinition {
1447            name:        "Status".to_string(),
1448            description: None,
1449            values:      vec![],
1450        });
1451
1452        let introspection = IntrospectionBuilder::build(&schema);
1453        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1454
1455        // Enum should be in the type map
1456        assert!(type_map.contains_key("Status"));
1457        let status = type_map.get("Status").unwrap();
1458        assert_eq!(status.kind, TypeKind::Enum);
1459    }
1460
1461    #[test]
1462    fn test_input_object_in_type_map() {
1463        use crate::schema::InputObjectDefinition;
1464
1465        let mut schema = CompiledSchema::new();
1466        schema.input_types.push(InputObjectDefinition {
1467            name:        "CreateUserInput".to_string(),
1468            description: None,
1469            fields:      vec![],
1470        });
1471
1472        let introspection = IntrospectionBuilder::build(&schema);
1473        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1474
1475        // Input object should be in the type map
1476        assert!(type_map.contains_key("CreateUserInput"));
1477        let input = type_map.get("CreateUserInput").unwrap();
1478        assert_eq!(input.kind, TypeKind::InputObject);
1479    }
1480
1481    #[test]
1482    fn test_interface_introspection() {
1483        use crate::schema::InterfaceDefinition;
1484
1485        let mut schema = CompiledSchema::new();
1486
1487        // Add a Node interface
1488        schema.interfaces.push(InterfaceDefinition {
1489            name:        "Node".to_string(),
1490            description: Some("An object with a globally unique ID".to_string()),
1491            fields:      vec![FieldDefinition::new("id", FieldType::Id)],
1492        });
1493
1494        // Add types that implement the interface
1495        schema.types.push(TypeDefinition {
1496            name:                "User".to_string(),
1497            sql_source:          "users".to_string(),
1498            jsonb_column:        "data".to_string(),
1499            description:         Some("A user".to_string()),
1500            sql_projection_hint: None,
1501            implements:          vec!["Node".to_string()],
1502            fields:              vec![
1503                FieldDefinition::new("id", FieldType::Id),
1504                FieldDefinition::new("name", FieldType::String),
1505            ],
1506        });
1507
1508        schema.types.push(TypeDefinition {
1509            name:                "Post".to_string(),
1510            sql_source:          "posts".to_string(),
1511            jsonb_column:        "data".to_string(),
1512            description:         Some("A blog post".to_string()),
1513            sql_projection_hint: None,
1514            implements:          vec!["Node".to_string()],
1515            fields:              vec![
1516                FieldDefinition::new("id", FieldType::Id),
1517                FieldDefinition::new("title", FieldType::String),
1518            ],
1519        });
1520
1521        let introspection = IntrospectionBuilder::build(&schema);
1522
1523        // Find Node interface
1524        let node = introspection
1525            .types
1526            .iter()
1527            .find(|t| t.name.as_ref() == Some(&"Node".to_string()))
1528            .unwrap();
1529
1530        assert_eq!(node.kind, TypeKind::Interface);
1531        assert_eq!(node.description, Some("An object with a globally unique ID".to_string()));
1532
1533        // Interface should have fields
1534        let fields = node.fields.as_ref().unwrap();
1535        assert_eq!(fields.len(), 1);
1536        assert_eq!(fields[0].name, "id");
1537
1538        // Interface should have possible_types (implementors)
1539        let possible_types = node.possible_types.as_ref().unwrap();
1540        assert_eq!(possible_types.len(), 2);
1541        assert!(possible_types.iter().any(|t| t.name == "User"));
1542        assert!(possible_types.iter().any(|t| t.name == "Post"));
1543
1544        // Interface should not have enum_values or input_fields
1545        assert!(node.enum_values.is_none());
1546        assert!(node.input_fields.is_none());
1547    }
1548
1549    #[test]
1550    fn test_type_implements_interface() {
1551        use crate::schema::InterfaceDefinition;
1552
1553        let mut schema = CompiledSchema::new();
1554
1555        // Add interfaces
1556        schema.interfaces.push(InterfaceDefinition {
1557            name:        "Node".to_string(),
1558            description: None,
1559            fields:      vec![FieldDefinition::new("id", FieldType::Id)],
1560        });
1561
1562        schema.interfaces.push(InterfaceDefinition {
1563            name:        "Timestamped".to_string(),
1564            description: None,
1565            fields:      vec![FieldDefinition::new("createdAt", FieldType::DateTime)],
1566        });
1567
1568        // Add a type that implements both interfaces
1569        schema.types.push(TypeDefinition {
1570            name:                "Comment".to_string(),
1571            sql_source:          "comments".to_string(),
1572            jsonb_column:        "data".to_string(),
1573            description:         None,
1574            sql_projection_hint: None,
1575            implements:          vec!["Node".to_string(), "Timestamped".to_string()],
1576            fields:              vec![
1577                FieldDefinition::new("id", FieldType::Id),
1578                FieldDefinition::new("createdAt", FieldType::DateTime),
1579                FieldDefinition::new("text", FieldType::String),
1580            ],
1581        });
1582
1583        let introspection = IntrospectionBuilder::build(&schema);
1584
1585        // Find Comment type
1586        let comment = introspection
1587            .types
1588            .iter()
1589            .find(|t| t.name.as_ref() == Some(&"Comment".to_string()))
1590            .unwrap();
1591
1592        assert_eq!(comment.kind, TypeKind::Object);
1593
1594        // Type should list interfaces it implements
1595        let interfaces = comment.interfaces.as_ref().unwrap();
1596        assert_eq!(interfaces.len(), 2);
1597        assert!(interfaces.iter().any(|i| i.name == "Node"));
1598        assert!(interfaces.iter().any(|i| i.name == "Timestamped"));
1599    }
1600
1601    #[test]
1602    fn test_interface_in_type_map() {
1603        use crate::schema::InterfaceDefinition;
1604
1605        let mut schema = CompiledSchema::new();
1606        schema.interfaces.push(InterfaceDefinition {
1607            name:        "Searchable".to_string(),
1608            description: None,
1609            fields:      vec![],
1610        });
1611
1612        let introspection = IntrospectionBuilder::build(&schema);
1613        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1614
1615        // Interface should be in the type map
1616        assert!(type_map.contains_key("Searchable"));
1617        let interface = type_map.get("Searchable").unwrap();
1618        assert_eq!(interface.kind, TypeKind::Interface);
1619    }
1620
1621    #[test]
1622    fn test_filter_deprecated_fields() {
1623        // Create a type with some deprecated fields
1624        let introspection_type = IntrospectionType {
1625            kind:               TypeKind::Object,
1626            name:               Some("TestType".to_string()),
1627            description:        None,
1628            fields:             Some(vec![
1629                IntrospectionField {
1630                    name:               "id".to_string(),
1631                    description:        None,
1632                    args:               vec![],
1633                    field_type:         IntrospectionBuilder::type_ref("ID"),
1634                    is_deprecated:      false,
1635                    deprecation_reason: None,
1636                },
1637                IntrospectionField {
1638                    name:               "oldField".to_string(),
1639                    description:        None,
1640                    args:               vec![],
1641                    field_type:         IntrospectionBuilder::type_ref("String"),
1642                    is_deprecated:      true,
1643                    deprecation_reason: Some("Use newField instead".to_string()),
1644                },
1645                IntrospectionField {
1646                    name:               "newField".to_string(),
1647                    description:        None,
1648                    args:               vec![],
1649                    field_type:         IntrospectionBuilder::type_ref("String"),
1650                    is_deprecated:      false,
1651                    deprecation_reason: None,
1652                },
1653            ]),
1654            interfaces:         None,
1655            possible_types:     None,
1656            enum_values:        None,
1657            input_fields:       None,
1658            of_type:            None,
1659            specified_by_u_r_l: None,
1660        };
1661
1662        // With includeDeprecated = false, should only have 2 fields
1663        let filtered = introspection_type.filter_deprecated_fields(false);
1664        let fields = filtered.fields.as_ref().unwrap();
1665        assert_eq!(fields.len(), 2);
1666        assert!(fields.iter().any(|f| f.name == "id"));
1667        assert!(fields.iter().any(|f| f.name == "newField"));
1668        assert!(!fields.iter().any(|f| f.name == "oldField"));
1669
1670        // With includeDeprecated = true, should have all 3 fields
1671        let unfiltered = introspection_type.filter_deprecated_fields(true);
1672        let fields = unfiltered.fields.as_ref().unwrap();
1673        assert_eq!(fields.len(), 3);
1674    }
1675
1676    #[test]
1677    fn test_filter_deprecated_enum_values() {
1678        // Create an enum type with some deprecated values
1679        let introspection_type = IntrospectionType {
1680            kind:               TypeKind::Enum,
1681            name:               Some("Status".to_string()),
1682            description:        None,
1683            fields:             None,
1684            interfaces:         None,
1685            possible_types:     None,
1686            enum_values:        Some(vec![
1687                IntrospectionEnumValue {
1688                    name:               "ACTIVE".to_string(),
1689                    description:        None,
1690                    is_deprecated:      false,
1691                    deprecation_reason: None,
1692                },
1693                IntrospectionEnumValue {
1694                    name:               "INACTIVE".to_string(),
1695                    description:        None,
1696                    is_deprecated:      true,
1697                    deprecation_reason: Some("Use DISABLED instead".to_string()),
1698                },
1699                IntrospectionEnumValue {
1700                    name:               "DISABLED".to_string(),
1701                    description:        None,
1702                    is_deprecated:      false,
1703                    deprecation_reason: None,
1704                },
1705            ]),
1706            input_fields:       None,
1707            of_type:            None,
1708            specified_by_u_r_l: None,
1709        };
1710
1711        // With includeDeprecated = false, should only have 2 values
1712        let filtered = introspection_type.filter_deprecated_enum_values(false);
1713        let values = filtered.enum_values.as_ref().unwrap();
1714        assert_eq!(values.len(), 2);
1715        assert!(values.iter().any(|v| v.name == "ACTIVE"));
1716        assert!(values.iter().any(|v| v.name == "DISABLED"));
1717        assert!(!values.iter().any(|v| v.name == "INACTIVE"));
1718
1719        // With includeDeprecated = true, should have all 3 values
1720        let unfiltered = introspection_type.filter_deprecated_enum_values(true);
1721        let values = unfiltered.enum_values.as_ref().unwrap();
1722        assert_eq!(values.len(), 3);
1723    }
1724
1725    #[test]
1726    fn test_specified_by_url_for_custom_scalars() {
1727        let schema = CompiledSchema::new();
1728        let introspection = IntrospectionBuilder::build(&schema);
1729
1730        // Find DateTime scalar
1731        let datetime = introspection
1732            .types
1733            .iter()
1734            .find(|t| t.name.as_ref() == Some(&"DateTime".to_string()))
1735            .unwrap();
1736
1737        assert_eq!(datetime.kind, TypeKind::Scalar);
1738        assert!(datetime.specified_by_u_r_l.is_some());
1739        assert!(datetime.specified_by_u_r_l.as_ref().unwrap().contains("date-time"));
1740
1741        // Find UUID scalar
1742        let uuid = introspection
1743            .types
1744            .iter()
1745            .find(|t| t.name.as_ref() == Some(&"UUID".to_string()))
1746            .unwrap();
1747
1748        assert_eq!(uuid.kind, TypeKind::Scalar);
1749        assert!(uuid.specified_by_u_r_l.is_some());
1750        assert!(uuid.specified_by_u_r_l.as_ref().unwrap().contains("rfc4122"));
1751
1752        // Built-in scalars (Int, String, etc.) should NOT have specifiedByURL
1753        let int = introspection
1754            .types
1755            .iter()
1756            .find(|t| t.name.as_ref() == Some(&"Int".to_string()))
1757            .unwrap();
1758
1759        assert_eq!(int.kind, TypeKind::Scalar);
1760        assert!(int.specified_by_u_r_l.is_none());
1761    }
1762
1763    #[test]
1764    fn test_deprecated_query_introspection() {
1765        use crate::schema::{ArgumentDefinition, AutoParams, DeprecationInfo};
1766
1767        let mut schema = CompiledSchema::new();
1768
1769        // Add a deprecated query
1770        schema.queries.push(QueryDefinition {
1771            name:         "oldUsers".to_string(),
1772            return_type:  "User".to_string(),
1773            returns_list: true,
1774            nullable:     false,
1775            arguments:    vec![],
1776            sql_source:   Some("v_user".to_string()),
1777            description:  Some("Old way to get users".to_string()),
1778            auto_params:  AutoParams::default(),
1779            deprecation:  Some(DeprecationInfo {
1780                reason: Some("Use 'users' instead".to_string()),
1781            }),
1782        });
1783
1784        // Add a non-deprecated query with a deprecated argument
1785        schema.queries.push(QueryDefinition {
1786            name:         "users".to_string(),
1787            return_type:  "User".to_string(),
1788            returns_list: true,
1789            nullable:     false,
1790            arguments:    vec![
1791                ArgumentDefinition {
1792                    name:          "first".to_string(),
1793                    arg_type:      FieldType::Int,
1794                    nullable:      true,
1795                    default_value: None,
1796                    description:   Some("Number of results to return".to_string()),
1797                    deprecation:   None,
1798                },
1799                ArgumentDefinition {
1800                    name:          "limit".to_string(),
1801                    arg_type:      FieldType::Int,
1802                    nullable:      true,
1803                    default_value: None,
1804                    description:   Some("Old parameter for limiting results".to_string()),
1805                    deprecation:   Some(DeprecationInfo {
1806                        reason: Some("Use 'first' instead".to_string()),
1807                    }),
1808                },
1809            ],
1810            sql_source:   Some("v_user".to_string()),
1811            description:  Some("Get users with pagination".to_string()),
1812            auto_params:  AutoParams::default(),
1813            deprecation:  None,
1814        });
1815
1816        let introspection = IntrospectionBuilder::build(&schema);
1817
1818        // Find Query type
1819        let query_type = introspection
1820            .types
1821            .iter()
1822            .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
1823            .unwrap();
1824
1825        let fields = query_type.fields.as_ref().unwrap();
1826
1827        // 'oldUsers' should be deprecated
1828        let old_users = fields.iter().find(|f| f.name == "oldUsers").unwrap();
1829        assert!(old_users.is_deprecated);
1830        assert_eq!(old_users.deprecation_reason, Some("Use 'users' instead".to_string()));
1831
1832        // 'users' should NOT be deprecated
1833        let users = fields.iter().find(|f| f.name == "users").unwrap();
1834        assert!(!users.is_deprecated);
1835        assert!(users.deprecation_reason.is_none());
1836
1837        // 'users' should have 2 arguments
1838        assert_eq!(users.args.len(), 2);
1839
1840        // 'first' argument should NOT be deprecated
1841        let first_arg = users.args.iter().find(|a| a.name == "first").unwrap();
1842        assert!(!first_arg.is_deprecated);
1843        assert!(first_arg.deprecation_reason.is_none());
1844
1845        // 'limit' argument should be deprecated
1846        let limit_arg = users.args.iter().find(|a| a.name == "limit").unwrap();
1847        assert!(limit_arg.is_deprecated);
1848        assert_eq!(limit_arg.deprecation_reason, Some("Use 'first' instead".to_string()));
1849    }
1850
1851    #[test]
1852    fn test_deprecated_input_field_introspection() {
1853        use crate::schema::{DeprecationInfo, InputFieldDefinition, InputObjectDefinition};
1854
1855        let mut schema = CompiledSchema::new();
1856
1857        // Add an input type with a deprecated field
1858        schema.input_types.push(InputObjectDefinition {
1859            name:        "CreateUserInput".to_string(),
1860            description: Some("Input for creating a user".to_string()),
1861            fields:      vec![
1862                InputFieldDefinition {
1863                    name:          "name".to_string(),
1864                    field_type:    "String!".to_string(),
1865                    default_value: None,
1866                    description:   Some("User name".to_string()),
1867                    deprecation:   None,
1868                },
1869                InputFieldDefinition {
1870                    name:          "oldEmail".to_string(),
1871                    field_type:    "String".to_string(),
1872                    default_value: None,
1873                    description:   Some("Legacy email field".to_string()),
1874                    deprecation:   Some(DeprecationInfo {
1875                        reason: Some("Use 'email' instead".to_string()),
1876                    }),
1877                },
1878            ],
1879        });
1880
1881        let introspection = IntrospectionBuilder::build(&schema);
1882
1883        // Find CreateUserInput type
1884        let create_user_input = introspection
1885            .types
1886            .iter()
1887            .find(|t| t.name.as_ref() == Some(&"CreateUserInput".to_string()))
1888            .unwrap();
1889
1890        let input_fields = create_user_input.input_fields.as_ref().unwrap();
1891
1892        // 'name' should NOT be deprecated
1893        let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
1894        assert!(!name_field.is_deprecated);
1895        assert!(name_field.deprecation_reason.is_none());
1896
1897        // 'oldEmail' should be deprecated
1898        let old_email = input_fields.iter().find(|f| f.name == "oldEmail").unwrap();
1899        assert!(old_email.is_deprecated);
1900        assert_eq!(old_email.deprecation_reason, Some("Use 'email' instead".to_string()));
1901    }
1902}