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/// Validation rule for input field in introspection format.
185///
186/// Converts internal ValidationRule enums to introspection-friendly format
187/// that clients can query and use for UI generation, form validation, etc.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct IntrospectionValidationRule {
191    /// Rule type name (required, pattern, range, enum, etc.)
192    pub rule_type: String,
193
194    /// Pattern regex (for pattern rules)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub pattern: Option<String>,
197
198    /// Pattern error message
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub pattern_message: Option<String>,
201
202    /// Minimum value or length
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub min: Option<i64>,
205
206    /// Maximum value or length
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub max: Option<i64>,
209
210    /// Allowed enum values
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub allowed_values: Option<Vec<String>>,
213
214    /// Checksum algorithm name
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub algorithm: Option<String>,
217
218    /// Referenced field name (for cross-field rules)
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub field_reference: Option<String>,
221
222    /// Comparison operator (for cross-field rules)
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub operator: Option<String>,
225
226    /// List of field names (for one_of, any_of, etc.)
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub field_list: Option<Vec<String>>,
229
230    /// Human-readable description
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub description: Option<String>,
233}
234
235/// `__InputValue` introspection type.
236///
237/// Per GraphQL spec, input values (arguments and input fields) can be deprecated.
238/// The `isDeprecated` and `deprecationReason` fields are part of the June 2021 spec.
239#[derive(Debug, Clone, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct IntrospectionInputValue {
242    /// Input name.
243    pub name: String,
244
245    /// Input description.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub description: Option<String>,
248
249    /// Input type.
250    #[serde(rename = "type")]
251    pub input_type: IntrospectionType,
252
253    /// Default value (as JSON string).
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub default_value: Option<String>,
256
257    /// Whether the input value is deprecated.
258    pub is_deprecated: bool,
259
260    /// Deprecation reason (if deprecated).
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub deprecation_reason: Option<String>,
263
264    /// Validation rules for this input value.
265    #[serde(default)]
266    pub validation_rules: Vec<IntrospectionValidationRule>,
267}
268
269/// `__EnumValue` introspection type.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct IntrospectionEnumValue {
273    /// Enum value name.
274    pub name: String,
275
276    /// Enum value description.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub description: Option<String>,
279
280    /// Whether the value is deprecated.
281    pub is_deprecated: bool,
282
283    /// Deprecation reason (if deprecated).
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub deprecation_reason: Option<String>,
286}
287
288/// `__Directive` introspection type.
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct IntrospectionDirective {
292    /// Directive name.
293    pub name: String,
294
295    /// Directive description.
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub description: Option<String>,
298
299    /// Valid locations for this directive.
300    pub locations: Vec<DirectiveLocation>,
301
302    /// Directive arguments.
303    pub args: Vec<IntrospectionInputValue>,
304
305    /// Whether the directive is repeatable.
306    #[serde(default)]
307    pub is_repeatable: bool,
308}
309
310/// `__TypeKind` enum per GraphQL spec §4.1.4.
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
313pub enum TypeKind {
314    /// A scalar type (Int, String, Boolean, etc.)
315    Scalar,
316    /// An object type with fields.
317    Object,
318    /// An abstract interface type.
319    Interface,
320    /// A union of multiple object types.
321    Union,
322    /// An enumeration type.
323    Enum,
324    /// An input object type for mutations.
325    InputObject,
326    /// A list wrapper type.
327    List,
328    /// A non-null wrapper type.
329    NonNull,
330}
331
332/// `__DirectiveLocation` enum per GraphQL spec §4.1.5.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
335pub enum DirectiveLocation {
336    /// Directive on query operation.
337    Query,
338    /// Directive on mutation operation.
339    Mutation,
340    /// Directive on subscription operation.
341    Subscription,
342    /// Directive on field selection.
343    Field,
344    /// Directive on fragment definition.
345    FragmentDefinition,
346    /// Directive on fragment spread.
347    FragmentSpread,
348    /// Directive on inline fragment.
349    InlineFragment,
350    /// Directive on variable definition.
351    VariableDefinition,
352    /// Directive on schema definition.
353    Schema,
354    /// Directive on scalar type definition.
355    Scalar,
356    /// Directive on object type definition.
357    Object,
358    /// Directive on field definition.
359    FieldDefinition,
360    /// Directive on argument definition.
361    ArgumentDefinition,
362    /// Directive on interface definition.
363    Interface,
364    /// Directive on union definition.
365    Union,
366    /// Directive on enum definition.
367    Enum,
368    /// Directive on enum value definition.
369    EnumValue,
370    /// Directive on input object definition.
371    InputObject,
372    /// Directive on input field definition.
373    InputFieldDefinition,
374}
375
376impl From<DirectiveLocationKind> for DirectiveLocation {
377    fn from(kind: DirectiveLocationKind) -> Self {
378        match kind {
379            DirectiveLocationKind::Query => Self::Query,
380            DirectiveLocationKind::Mutation => Self::Mutation,
381            DirectiveLocationKind::Subscription => Self::Subscription,
382            DirectiveLocationKind::Field => Self::Field,
383            DirectiveLocationKind::FragmentDefinition => Self::FragmentDefinition,
384            DirectiveLocationKind::FragmentSpread => Self::FragmentSpread,
385            DirectiveLocationKind::InlineFragment => Self::InlineFragment,
386            DirectiveLocationKind::VariableDefinition => Self::VariableDefinition,
387            DirectiveLocationKind::Schema => Self::Schema,
388            DirectiveLocationKind::Scalar => Self::Scalar,
389            DirectiveLocationKind::Object => Self::Object,
390            DirectiveLocationKind::FieldDefinition => Self::FieldDefinition,
391            DirectiveLocationKind::ArgumentDefinition => Self::ArgumentDefinition,
392            DirectiveLocationKind::Interface => Self::Interface,
393            DirectiveLocationKind::Union => Self::Union,
394            DirectiveLocationKind::Enum => Self::Enum,
395            DirectiveLocationKind::EnumValue => Self::EnumValue,
396            DirectiveLocationKind::InputObject => Self::InputObject,
397            DirectiveLocationKind::InputFieldDefinition => Self::InputFieldDefinition,
398        }
399    }
400}
401
402// =============================================================================
403// Introspection Builder
404// =============================================================================
405
406/// Builds introspection schema from compiled schema.
407pub struct IntrospectionBuilder;
408
409impl IntrospectionBuilder {
410    /// Build complete introspection schema from compiled schema.
411    #[must_use]
412    pub fn build(schema: &CompiledSchema) -> IntrospectionSchema {
413        let mut types = Vec::new();
414
415        // Add built-in scalar types
416        types.extend(Self::builtin_scalars());
417
418        // Add user-defined types
419        for type_def in &schema.types {
420            types.push(Self::build_object_type(type_def));
421        }
422
423        // Add enum types
424        for enum_def in &schema.enums {
425            types.push(Self::build_enum_type(enum_def));
426        }
427
428        // Add input object types
429        for input_def in &schema.input_types {
430            types.push(Self::build_input_object_type(input_def));
431        }
432
433        // Add interface types
434        for interface_def in &schema.interfaces {
435            types.push(Self::build_interface_type(interface_def, schema));
436        }
437
438        // Add union types
439        for union_def in &schema.unions {
440            types.push(Self::build_union_type(union_def));
441        }
442
443        // Add Query root type
444        types.push(Self::build_query_type(schema));
445
446        // Add Mutation root type if mutations exist
447        if !schema.mutations.is_empty() {
448            types.push(Self::build_mutation_type(schema));
449        }
450
451        // Add Subscription root type if subscriptions exist
452        if !schema.subscriptions.is_empty() {
453            types.push(Self::build_subscription_type(schema));
454        }
455
456        // Build directives: built-in + custom
457        let mut directives = Self::builtin_directives();
458        directives.extend(Self::build_custom_directives(&schema.directives));
459
460        IntrospectionSchema {
461            description: Some("FraiseQL GraphQL Schema".to_string()),
462            types,
463            query_type: IntrospectionTypeRef {
464                name: "Query".to_string(),
465            },
466            mutation_type: if schema.mutations.is_empty() {
467                None
468            } else {
469                Some(IntrospectionTypeRef {
470                    name: "Mutation".to_string(),
471                })
472            },
473            subscription_type: if schema.subscriptions.is_empty() {
474                None
475            } else {
476                Some(IntrospectionTypeRef {
477                    name: "Subscription".to_string(),
478                })
479            },
480            directives,
481        }
482    }
483
484    /// Build a lookup map for `__type(name:)` queries.
485    #[must_use]
486    pub fn build_type_map(schema: &IntrospectionSchema) -> HashMap<String, IntrospectionType> {
487        let mut map = HashMap::new();
488        for t in &schema.types {
489            if let Some(ref name) = t.name {
490                map.insert(name.clone(), t.clone());
491            }
492        }
493        map
494    }
495
496    /// Built-in GraphQL scalar types.
497    fn builtin_scalars() -> Vec<IntrospectionType> {
498        vec![
499            Self::scalar_type("Int", "Built-in Int scalar"),
500            Self::scalar_type("Float", "Built-in Float scalar"),
501            Self::scalar_type("String", "Built-in String scalar"),
502            Self::scalar_type("Boolean", "Built-in Boolean scalar"),
503            Self::scalar_type("ID", "Built-in ID scalar"),
504            // FraiseQL custom scalars (with specifiedByURL per GraphQL spec §3.5.5)
505            Self::scalar_type_with_url(
506                "DateTime",
507                "ISO-8601 datetime string",
508                Some("https://scalars.graphql.org/andimarek/date-time"),
509            ),
510            Self::scalar_type_with_url(
511                "Date",
512                "ISO-8601 date string",
513                Some("https://scalars.graphql.org/andimarek/local-date"),
514            ),
515            Self::scalar_type_with_url(
516                "Time",
517                "ISO-8601 time string",
518                Some("https://scalars.graphql.org/andimarek/local-time"),
519            ),
520            Self::scalar_type_with_url(
521                "UUID",
522                "UUID string",
523                Some("https://tools.ietf.org/html/rfc4122"),
524            ),
525            Self::scalar_type_with_url(
526                "JSON",
527                "Arbitrary JSON value",
528                Some("https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"),
529            ),
530            Self::scalar_type("Decimal", "Decimal number"),
531        ]
532    }
533
534    /// Create a scalar type introspection.
535    fn scalar_type(name: &str, description: &str) -> IntrospectionType {
536        Self::scalar_type_with_url(name, description, None)
537    }
538
539    /// Create a scalar type introspection with optional `specifiedByURL`.
540    fn scalar_type_with_url(
541        name: &str,
542        description: &str,
543        specified_by_url: Option<&str>,
544    ) -> IntrospectionType {
545        IntrospectionType {
546            kind:               TypeKind::Scalar,
547            name:               Some(name.to_string()),
548            description:        Some(description.to_string()),
549            fields:             None,
550            interfaces:         None,
551            possible_types:     None,
552            enum_values:        None,
553            input_fields:       None,
554            of_type:            None,
555            specified_by_u_r_l: specified_by_url.map(ToString::to_string),
556        }
557    }
558
559    /// Build object type from TypeDefinition.
560    fn build_object_type(type_def: &TypeDefinition) -> IntrospectionType {
561        let fields = type_def.fields.iter().map(|f| Self::build_field(f)).collect();
562
563        // Build interfaces that this type implements
564        let interfaces: Vec<IntrospectionTypeRef> = type_def
565            .implements
566            .iter()
567            .map(|name| IntrospectionTypeRef { name: name.clone() })
568            .collect();
569
570        IntrospectionType {
571            kind:               TypeKind::Object,
572            name:               Some(type_def.name.clone()),
573            description:        type_def.description.clone(),
574            fields:             Some(fields),
575            interfaces:         Some(interfaces),
576            possible_types:     None,
577            enum_values:        None,
578            input_fields:       None,
579            of_type:            None,
580            specified_by_u_r_l: None,
581        }
582    }
583
584    /// Build enum type from EnumDefinition.
585    fn build_enum_type(enum_def: &EnumDefinition) -> IntrospectionType {
586        let enum_values = enum_def
587            .values
588            .iter()
589            .map(|v| IntrospectionEnumValue {
590                name:               v.name.clone(),
591                description:        v.description.clone(),
592                is_deprecated:      v.deprecation.is_some(),
593                deprecation_reason: v.deprecation.as_ref().and_then(|d| d.reason.clone()),
594            })
595            .collect();
596
597        IntrospectionType {
598            kind:               TypeKind::Enum,
599            name:               Some(enum_def.name.clone()),
600            description:        enum_def.description.clone(),
601            fields:             None,
602            interfaces:         None,
603            possible_types:     None,
604            enum_values:        Some(enum_values),
605            input_fields:       None,
606            of_type:            None,
607            specified_by_u_r_l: None,
608        }
609    }
610
611    /// Build input object type from InputObjectDefinition.
612    fn build_input_object_type(input_def: &InputObjectDefinition) -> IntrospectionType {
613        let input_fields = input_def
614            .fields
615            .iter()
616            .map(|f| {
617                let validation_rules = f
618                    .validation_rules
619                    .iter()
620                    .map(|rule| Self::build_validation_rule(rule))
621                    .collect();
622
623                IntrospectionInputValue {
624                    name: f.name.clone(),
625                    description: f.description.clone(),
626                    input_type: Self::type_ref(&f.field_type),
627                    default_value: f.default_value.clone(),
628                    is_deprecated: f.is_deprecated(),
629                    deprecation_reason: f.deprecation.as_ref().and_then(|d| d.reason.clone()),
630                    validation_rules,
631                }
632            })
633            .collect();
634
635        IntrospectionType {
636            kind:               TypeKind::InputObject,
637            name:               Some(input_def.name.clone()),
638            description:        input_def.description.clone(),
639            fields:             None,
640            interfaces:         None,
641            possible_types:     None,
642            enum_values:        None,
643            input_fields:       Some(input_fields),
644            of_type:            None,
645            specified_by_u_r_l: None,
646        }
647    }
648
649    /// Build interface type from InterfaceDefinition.
650    fn build_interface_type(
651        interface_def: &InterfaceDefinition,
652        schema: &CompiledSchema,
653    ) -> IntrospectionType {
654        // Build fields for the interface
655        let fields = interface_def.fields.iter().map(|f| Self::build_field(f)).collect();
656
657        // Find all types that implement this interface
658        let possible_types: Vec<IntrospectionTypeRef> = schema
659            .find_implementors(&interface_def.name)
660            .iter()
661            .map(|t| IntrospectionTypeRef {
662                name: t.name.clone(),
663            })
664            .collect();
665
666        IntrospectionType {
667            kind:               TypeKind::Interface,
668            name:               Some(interface_def.name.clone()),
669            description:        interface_def.description.clone(),
670            fields:             Some(fields),
671            interfaces:         None,
672            possible_types:     if possible_types.is_empty() {
673                None
674            } else {
675                Some(possible_types)
676            },
677            enum_values:        None,
678            input_fields:       None,
679            of_type:            None,
680            specified_by_u_r_l: None,
681        }
682    }
683
684    /// Build union type from UnionDefinition.
685    fn build_union_type(union_def: &UnionDefinition) -> IntrospectionType {
686        // Build possible types for the union
687        let possible_types: Vec<IntrospectionTypeRef> = union_def
688            .member_types
689            .iter()
690            .map(|name| IntrospectionTypeRef { name: name.clone() })
691            .collect();
692
693        IntrospectionType {
694            kind:               TypeKind::Union,
695            name:               Some(union_def.name.clone()),
696            description:        union_def.description.clone(),
697            fields:             None, // Unions don't have fields
698            interfaces:         None,
699            possible_types:     if possible_types.is_empty() {
700                None
701            } else {
702                Some(possible_types)
703            },
704            enum_values:        None,
705            input_fields:       None,
706            of_type:            None,
707            specified_by_u_r_l: None,
708        }
709    }
710
711    /// Build field introspection from FieldDefinition.
712    fn build_field(field: &FieldDefinition) -> IntrospectionField {
713        IntrospectionField {
714            name:               field.output_name().to_string(),
715            description:        field.description.clone(),
716            args:               vec![], // Regular fields don't have args
717            field_type:         Self::field_type_to_introspection(
718                &field.field_type,
719                field.nullable,
720            ),
721            is_deprecated:      field.is_deprecated(),
722            deprecation_reason: field.deprecation_reason().map(ToString::to_string),
723        }
724    }
725
726    /// Convert FieldType to introspection type.
727    fn field_type_to_introspection(field_type: &FieldType, nullable: bool) -> IntrospectionType {
728        let inner = match field_type {
729            FieldType::Int => Self::type_ref("Int"),
730            FieldType::Float => Self::type_ref("Float"),
731            FieldType::String => Self::type_ref("String"),
732            FieldType::Boolean => Self::type_ref("Boolean"),
733            FieldType::Id => Self::type_ref("ID"),
734            FieldType::DateTime => Self::type_ref("DateTime"),
735            FieldType::Date => Self::type_ref("Date"),
736            FieldType::Time => Self::type_ref("Time"),
737            FieldType::Uuid => Self::type_ref("UUID"),
738            FieldType::Json => Self::type_ref("JSON"),
739            FieldType::Decimal => Self::type_ref("Decimal"),
740            FieldType::Object(name) => Self::type_ref(name),
741            FieldType::Enum(name) => Self::type_ref(name),
742            FieldType::Input(name) => Self::type_ref(name),
743            FieldType::Interface(name) => Self::type_ref(name),
744            FieldType::Union(name) => Self::type_ref(name),
745            FieldType::Scalar(name) => Self::type_ref(name), // Rich/custom scalars
746            FieldType::List(inner) => IntrospectionType {
747                kind:               TypeKind::List,
748                name:               None,
749                description:        None,
750                fields:             None,
751                interfaces:         None,
752                possible_types:     None,
753                enum_values:        None,
754                input_fields:       None,
755                of_type:            Some(Box::new(Self::field_type_to_introspection(inner, true))),
756                specified_by_u_r_l: None,
757            },
758            FieldType::Vector => Self::type_ref("JSON"), // Vectors are exposed as JSON
759        };
760
761        if nullable {
762            inner
763        } else {
764            // Wrap in NON_NULL
765            IntrospectionType {
766                kind:               TypeKind::NonNull,
767                name:               None,
768                description:        None,
769                fields:             None,
770                interfaces:         None,
771                possible_types:     None,
772                enum_values:        None,
773                input_fields:       None,
774                of_type:            Some(Box::new(inner)),
775                specified_by_u_r_l: None,
776            }
777        }
778    }
779
780    /// Create a type reference.
781    fn type_ref(name: &str) -> IntrospectionType {
782        IntrospectionType {
783            kind:               TypeKind::Scalar, // Will be overwritten if it's an object
784            name:               Some(name.to_string()),
785            description:        None,
786            fields:             None,
787            interfaces:         None,
788            possible_types:     None,
789            enum_values:        None,
790            input_fields:       None,
791            of_type:            None,
792            specified_by_u_r_l: None,
793        }
794    }
795
796    /// Convert ValidationRule to IntrospectionValidationRule.
797    fn build_validation_rule(
798        rule: &crate::validation::rules::ValidationRule,
799    ) -> IntrospectionValidationRule {
800        use crate::validation::rules::ValidationRule;
801
802        match rule {
803            ValidationRule::Required => IntrospectionValidationRule {
804                rule_type:       "required".to_string(),
805                pattern:         None,
806                pattern_message: None,
807                min:             None,
808                max:             None,
809                allowed_values:  None,
810                algorithm:       None,
811                field_reference: None,
812                operator:        None,
813                field_list:      None,
814                description:     Some("Field is required".to_string()),
815            },
816            ValidationRule::Pattern { pattern, message } => IntrospectionValidationRule {
817                rule_type:       "pattern".to_string(),
818                pattern:         Some(pattern.clone()),
819                pattern_message: message.clone(),
820                min:             None,
821                max:             None,
822                allowed_values:  None,
823                algorithm:       None,
824                field_reference: None,
825                operator:        None,
826                field_list:      None,
827                description:     message.clone(),
828            },
829            ValidationRule::Length { min, max } => IntrospectionValidationRule {
830                rule_type:       "length".to_string(),
831                pattern:         None,
832                pattern_message: None,
833                min:             min.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
834                max:             max.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
835                allowed_values:  None,
836                algorithm:       None,
837                field_reference: None,
838                operator:        None,
839                field_list:      None,
840                description:     None,
841            },
842            ValidationRule::Range { min, max } => IntrospectionValidationRule {
843                rule_type:       "range".to_string(),
844                pattern:         None,
845                pattern_message: None,
846                min:             *min,
847                max:             *max,
848                allowed_values:  None,
849                algorithm:       None,
850                field_reference: None,
851                operator:        None,
852                field_list:      None,
853                description:     None,
854            },
855            ValidationRule::Enum { values } => IntrospectionValidationRule {
856                rule_type:       "enum".to_string(),
857                pattern:         None,
858                pattern_message: None,
859                min:             None,
860                max:             None,
861                allowed_values:  Some(values.clone()),
862                algorithm:       None,
863                field_reference: None,
864                operator:        None,
865                field_list:      None,
866                description:     None,
867            },
868            ValidationRule::Checksum { algorithm } => IntrospectionValidationRule {
869                rule_type:       "checksum".to_string(),
870                pattern:         None,
871                pattern_message: None,
872                min:             None,
873                max:             None,
874                allowed_values:  None,
875                algorithm:       Some(algorithm.clone()),
876                field_reference: None,
877                operator:        None,
878                field_list:      None,
879                description:     None,
880            },
881            ValidationRule::CrossField { field, operator } => IntrospectionValidationRule {
882                rule_type:       "cross_field".to_string(),
883                pattern:         None,
884                pattern_message: None,
885                min:             None,
886                max:             None,
887                allowed_values:  None,
888                algorithm:       None,
889                field_reference: Some(field.clone()),
890                operator:        Some(operator.clone()),
891                field_list:      None,
892                description:     None,
893            },
894            ValidationRule::Conditional { .. } => IntrospectionValidationRule {
895                rule_type:       "conditional".to_string(),
896                pattern:         None,
897                pattern_message: None,
898                min:             None,
899                max:             None,
900                allowed_values:  None,
901                algorithm:       None,
902                field_reference: None,
903                operator:        None,
904                field_list:      None,
905                description:     Some("Conditional validation".to_string()),
906            },
907            ValidationRule::All(_) => IntrospectionValidationRule {
908                rule_type:       "all".to_string(),
909                pattern:         None,
910                pattern_message: None,
911                min:             None,
912                max:             None,
913                allowed_values:  None,
914                algorithm:       None,
915                field_reference: None,
916                operator:        None,
917                field_list:      None,
918                description:     Some("All rules must pass".to_string()),
919            },
920            ValidationRule::Any(_) => IntrospectionValidationRule {
921                rule_type:       "any".to_string(),
922                pattern:         None,
923                pattern_message: None,
924                min:             None,
925                max:             None,
926                allowed_values:  None,
927                algorithm:       None,
928                field_reference: None,
929                operator:        None,
930                field_list:      None,
931                description:     Some("At least one rule must pass".to_string()),
932            },
933            ValidationRule::OneOf { fields } => IntrospectionValidationRule {
934                rule_type:       "one_of".to_string(),
935                pattern:         None,
936                pattern_message: None,
937                min:             None,
938                max:             None,
939                allowed_values:  None,
940                algorithm:       None,
941                field_reference: None,
942                operator:        None,
943                field_list:      Some(fields.clone()),
944                description:     None,
945            },
946            ValidationRule::AnyOf { fields } => IntrospectionValidationRule {
947                rule_type:       "any_of".to_string(),
948                pattern:         None,
949                pattern_message: None,
950                min:             None,
951                max:             None,
952                allowed_values:  None,
953                algorithm:       None,
954                field_reference: None,
955                operator:        None,
956                field_list:      Some(fields.clone()),
957                description:     None,
958            },
959            ValidationRule::ConditionalRequired { .. } => IntrospectionValidationRule {
960                rule_type:       "conditional_required".to_string(),
961                pattern:         None,
962                pattern_message: None,
963                min:             None,
964                max:             None,
965                allowed_values:  None,
966                algorithm:       None,
967                field_reference: None,
968                operator:        None,
969                field_list:      None,
970                description:     None,
971            },
972            ValidationRule::RequiredIfAbsent { .. } => IntrospectionValidationRule {
973                rule_type:       "required_if_absent".to_string(),
974                pattern:         None,
975                pattern_message: None,
976                min:             None,
977                max:             None,
978                allowed_values:  None,
979                algorithm:       None,
980                field_reference: None,
981                operator:        None,
982                field_list:      None,
983                description:     None,
984            },
985        }
986    }
987
988    /// Build Query root type.
989    fn build_query_type(schema: &CompiledSchema) -> IntrospectionType {
990        let fields: Vec<IntrospectionField> =
991            schema.queries.iter().map(|q| Self::build_query_field(q)).collect();
992
993        IntrospectionType {
994            kind:               TypeKind::Object,
995            name:               Some("Query".to_string()),
996            description:        Some("Root query type".to_string()),
997            fields:             Some(fields),
998            interfaces:         Some(vec![]),
999            possible_types:     None,
1000            enum_values:        None,
1001            input_fields:       None,
1002            of_type:            None,
1003            specified_by_u_r_l: None,
1004        }
1005    }
1006
1007    /// Build Mutation root type.
1008    fn build_mutation_type(schema: &CompiledSchema) -> IntrospectionType {
1009        let fields: Vec<IntrospectionField> =
1010            schema.mutations.iter().map(|m| Self::build_mutation_field(m)).collect();
1011
1012        IntrospectionType {
1013            kind:               TypeKind::Object,
1014            name:               Some("Mutation".to_string()),
1015            description:        Some("Root mutation type".to_string()),
1016            fields:             Some(fields),
1017            interfaces:         Some(vec![]),
1018            possible_types:     None,
1019            enum_values:        None,
1020            input_fields:       None,
1021            of_type:            None,
1022            specified_by_u_r_l: None,
1023        }
1024    }
1025
1026    /// Build Subscription root type.
1027    fn build_subscription_type(schema: &CompiledSchema) -> IntrospectionType {
1028        let fields: Vec<IntrospectionField> =
1029            schema.subscriptions.iter().map(|s| Self::build_subscription_field(s)).collect();
1030
1031        IntrospectionType {
1032            kind:               TypeKind::Object,
1033            name:               Some("Subscription".to_string()),
1034            description:        Some("Root subscription type".to_string()),
1035            fields:             Some(fields),
1036            interfaces:         Some(vec![]),
1037            possible_types:     None,
1038            enum_values:        None,
1039            input_fields:       None,
1040            of_type:            None,
1041            specified_by_u_r_l: None,
1042        }
1043    }
1044
1045    /// Build query field introspection.
1046    fn build_query_field(query: &QueryDefinition) -> IntrospectionField {
1047        let return_type = Self::type_ref(&query.return_type);
1048        let return_type = if query.returns_list {
1049            IntrospectionType {
1050                kind:               TypeKind::List,
1051                name:               None,
1052                description:        None,
1053                fields:             None,
1054                interfaces:         None,
1055                possible_types:     None,
1056                enum_values:        None,
1057                input_fields:       None,
1058                of_type:            Some(Box::new(return_type)),
1059                specified_by_u_r_l: None,
1060            }
1061        } else {
1062            return_type
1063        };
1064
1065        let return_type = if query.nullable {
1066            return_type
1067        } else {
1068            IntrospectionType {
1069                kind:               TypeKind::NonNull,
1070                name:               None,
1071                description:        None,
1072                fields:             None,
1073                interfaces:         None,
1074                possible_types:     None,
1075                enum_values:        None,
1076                input_fields:       None,
1077                of_type:            Some(Box::new(return_type)),
1078                specified_by_u_r_l: None,
1079            }
1080        };
1081
1082        // Build arguments
1083        let args: Vec<IntrospectionInputValue> = query
1084            .arguments
1085            .iter()
1086            .map(|arg| IntrospectionInputValue {
1087                name:               arg.name.clone(),
1088                description:        arg.description.clone(),
1089                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1090                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
1091                is_deprecated:      arg.is_deprecated(),
1092                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1093                validation_rules:   vec![],
1094            })
1095            .collect();
1096
1097        IntrospectionField {
1098            name: query.name.clone(),
1099            description: query.description.clone(),
1100            args,
1101            field_type: return_type,
1102            is_deprecated: query.is_deprecated(),
1103            deprecation_reason: query.deprecation_reason().map(ToString::to_string),
1104        }
1105    }
1106
1107    /// Build mutation field introspection.
1108    fn build_mutation_field(mutation: &super::MutationDefinition) -> IntrospectionField {
1109        // Mutations always return a single object (not a list)
1110        let return_type = Self::type_ref(&mutation.return_type);
1111
1112        // Build arguments
1113        let args: Vec<IntrospectionInputValue> = mutation
1114            .arguments
1115            .iter()
1116            .map(|arg| IntrospectionInputValue {
1117                name:               arg.name.clone(),
1118                description:        arg.description.clone(),
1119                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1120                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
1121                is_deprecated:      arg.is_deprecated(),
1122                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1123                validation_rules:   vec![],
1124            })
1125            .collect();
1126
1127        IntrospectionField {
1128            name: mutation.name.clone(),
1129            description: mutation.description.clone(),
1130            args,
1131            field_type: return_type,
1132            is_deprecated: mutation.is_deprecated(),
1133            deprecation_reason: mutation.deprecation_reason().map(ToString::to_string),
1134        }
1135    }
1136
1137    /// Build subscription field introspection.
1138    fn build_subscription_field(
1139        subscription: &super::SubscriptionDefinition,
1140    ) -> IntrospectionField {
1141        // Subscriptions typically return a single item per event
1142        let return_type = Self::type_ref(&subscription.return_type);
1143
1144        // Build arguments
1145        let args: Vec<IntrospectionInputValue> = subscription
1146            .arguments
1147            .iter()
1148            .map(|arg| IntrospectionInputValue {
1149                name:               arg.name.clone(),
1150                description:        arg.description.clone(),
1151                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1152                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
1153                is_deprecated:      arg.is_deprecated(),
1154                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1155                validation_rules:   vec![],
1156            })
1157            .collect();
1158
1159        IntrospectionField {
1160            name: subscription.name.clone(),
1161            description: subscription.description.clone(),
1162            args,
1163            field_type: return_type,
1164            is_deprecated: subscription.is_deprecated(),
1165            deprecation_reason: subscription.deprecation_reason().map(ToString::to_string),
1166        }
1167    }
1168
1169    /// Built-in GraphQL directives.
1170    fn builtin_directives() -> Vec<IntrospectionDirective> {
1171        vec![
1172            IntrospectionDirective {
1173                name: "skip".to_string(),
1174                description: Some(
1175                    "Directs the executor to skip this field or fragment when the `if` argument is true."
1176                        .to_string(),
1177                ),
1178                locations: vec![
1179                    DirectiveLocation::Field,
1180                    DirectiveLocation::FragmentSpread,
1181                    DirectiveLocation::InlineFragment,
1182                ],
1183                args: vec![IntrospectionInputValue {
1184                    name: "if".to_string(),
1185                    description: Some("Skipped when true.".to_string()),
1186                    input_type: IntrospectionType {
1187                        kind: TypeKind::NonNull,
1188                        name: None,
1189                        description: None,
1190                        fields: None,
1191                        interfaces: None,
1192                        possible_types: None,
1193                        enum_values: None,
1194                        input_fields: None,
1195                        of_type: Some(Box::new(Self::type_ref("Boolean"))),
1196                        specified_by_u_r_l: None,
1197                    },
1198                    default_value: None,
1199                    is_deprecated: false,
1200                    deprecation_reason: None,
1201                    validation_rules: vec![],
1202                }],
1203                is_repeatable: false,
1204            },
1205            IntrospectionDirective {
1206                name: "include".to_string(),
1207                description: Some(
1208                    "Directs the executor to include this field or fragment only when the `if` argument is true."
1209                        .to_string(),
1210                ),
1211                locations: vec![
1212                    DirectiveLocation::Field,
1213                    DirectiveLocation::FragmentSpread,
1214                    DirectiveLocation::InlineFragment,
1215                ],
1216                args: vec![IntrospectionInputValue {
1217                    name: "if".to_string(),
1218                    description: Some("Included when true.".to_string()),
1219                    input_type: IntrospectionType {
1220                        kind: TypeKind::NonNull,
1221                        name: None,
1222                        description: None,
1223                        fields: None,
1224                        interfaces: None,
1225                        possible_types: None,
1226                        enum_values: None,
1227                        input_fields: None,
1228                        of_type: Some(Box::new(Self::type_ref("Boolean"))),
1229                        specified_by_u_r_l: None,
1230                    },
1231                    default_value: None,
1232                    is_deprecated: false,
1233                    deprecation_reason: None,
1234                    validation_rules: vec![],
1235                }],
1236                is_repeatable: false,
1237            },
1238            IntrospectionDirective {
1239                name: "deprecated".to_string(),
1240                description: Some(
1241                    "Marks an element of a GraphQL schema as no longer supported.".to_string(),
1242                ),
1243                locations: vec![
1244                    DirectiveLocation::FieldDefinition,
1245                    DirectiveLocation::EnumValue,
1246                    DirectiveLocation::ArgumentDefinition,
1247                    DirectiveLocation::InputFieldDefinition,
1248                ],
1249                args: vec![IntrospectionInputValue {
1250                    name: "reason".to_string(),
1251                    description: Some(
1252                        "Explains why this element was deprecated.".to_string(),
1253                    ),
1254                    input_type: Self::type_ref("String"),
1255                    default_value: Some("\"No longer supported\"".to_string()),
1256                    is_deprecated: false,
1257                    deprecation_reason: None,
1258                    validation_rules: vec![],
1259                }],
1260                is_repeatable: false,
1261            },
1262        ]
1263    }
1264
1265    /// Build introspection directives from custom directive definitions.
1266    fn build_custom_directives(directives: &[DirectiveDefinition]) -> Vec<IntrospectionDirective> {
1267        directives.iter().map(|d| Self::build_custom_directive(d)).collect()
1268    }
1269
1270    /// Build a single introspection directive from a custom directive definition.
1271    fn build_custom_directive(directive: &DirectiveDefinition) -> IntrospectionDirective {
1272        let locations: Vec<DirectiveLocation> =
1273            directive.locations.iter().map(|loc| DirectiveLocation::from(*loc)).collect();
1274
1275        let args: Vec<IntrospectionInputValue> = directive
1276            .arguments
1277            .iter()
1278            .map(|arg| IntrospectionInputValue {
1279                name:               arg.name.clone(),
1280                description:        arg.description.clone(),
1281                input_type:         Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1282                default_value:      arg.default_value.as_ref().map(|v| v.to_string()),
1283                is_deprecated:      arg.is_deprecated(),
1284                deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1285                validation_rules:   vec![],
1286            })
1287            .collect();
1288
1289        IntrospectionDirective {
1290            name: directive.name.clone(),
1291            description: directive.description.clone(),
1292            locations,
1293            args,
1294            is_repeatable: directive.is_repeatable,
1295        }
1296    }
1297}
1298
1299// =============================================================================
1300// Introspection Response Wrapper
1301// =============================================================================
1302
1303/// Pre-built introspection responses for fast serving.
1304#[derive(Debug, Clone)]
1305pub struct IntrospectionResponses {
1306    /// Full `__schema` response JSON.
1307    pub schema_response: String,
1308    /// Map of type name -> `__type` response JSON.
1309    pub type_responses:  HashMap<String, String>,
1310}
1311
1312impl IntrospectionResponses {
1313    /// Build introspection responses from compiled schema.
1314    ///
1315    /// This is called once at server startup and cached.
1316    #[must_use]
1317    pub fn build(schema: &CompiledSchema) -> Self {
1318        let introspection = IntrospectionBuilder::build(schema);
1319        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1320
1321        // Build __schema response
1322        let schema_response = serde_json::json!({
1323            "data": {
1324                "__schema": introspection
1325            }
1326        })
1327        .to_string();
1328
1329        // Build __type responses for each type
1330        let mut type_responses = HashMap::new();
1331        for (name, t) in type_map {
1332            let response = serde_json::json!({
1333                "data": {
1334                    "__type": t
1335                }
1336            })
1337            .to_string();
1338            type_responses.insert(name, response);
1339        }
1340
1341        Self {
1342            schema_response,
1343            type_responses,
1344        }
1345    }
1346
1347    /// Get response for `__type(name: "...")` query.
1348    #[must_use]
1349    pub fn get_type_response(&self, type_name: &str) -> String {
1350        self.type_responses.get(type_name).cloned().unwrap_or_else(|| {
1351            serde_json::json!({
1352                "data": {
1353                    "__type": null
1354                }
1355            })
1356            .to_string()
1357        })
1358    }
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363    use super::*;
1364    use crate::schema::{AutoParams, FieldType};
1365
1366    fn test_schema() -> CompiledSchema {
1367        let mut schema = CompiledSchema::new();
1368
1369        // Add a User type
1370        schema.types.push(
1371            TypeDefinition::new("User", "v_user")
1372                .with_field(FieldDefinition::new("id", FieldType::Id))
1373                .with_field(FieldDefinition::new("name", FieldType::String))
1374                .with_field(FieldDefinition::nullable("email", FieldType::String))
1375                .with_description("A user in the system"),
1376        );
1377
1378        // Add a users query
1379        schema.queries.push(QueryDefinition {
1380            name:         "users".to_string(),
1381            return_type:  "User".to_string(),
1382            returns_list: true,
1383            nullable:     false,
1384            arguments:    vec![],
1385            sql_source:   Some("v_user".to_string()),
1386            description:  Some("Get all users".to_string()),
1387            auto_params:  AutoParams::default(),
1388            deprecation:  None,
1389            jsonb_column: "data".to_string(),
1390        });
1391
1392        // Add a user query with argument
1393        schema.queries.push(QueryDefinition {
1394            name:         "user".to_string(),
1395            return_type:  "User".to_string(),
1396            returns_list: false,
1397            nullable:     true,
1398            arguments:    vec![crate::schema::ArgumentDefinition {
1399                name:          "id".to_string(),
1400                arg_type:      FieldType::Id,
1401                nullable:      false, // required
1402                default_value: None,
1403                description:   Some("User ID".to_string()),
1404                deprecation:   None,
1405            }],
1406            sql_source:   Some("v_user".to_string()),
1407            description:  Some("Get user by ID".to_string()),
1408            auto_params:  AutoParams::default(),
1409            deprecation:  None,
1410            jsonb_column: "data".to_string(),
1411        });
1412
1413        schema
1414    }
1415
1416    #[test]
1417    fn test_build_introspection_schema() {
1418        let schema = test_schema();
1419        let introspection = IntrospectionBuilder::build(&schema);
1420
1421        // Should have Query type
1422        assert_eq!(introspection.query_type.name, "Query");
1423
1424        // Should not have Mutation type (no mutations)
1425        assert!(introspection.mutation_type.is_none());
1426
1427        // Should have built-in scalars
1428        let scalar_names: Vec<_> = introspection
1429            .types
1430            .iter()
1431            .filter(|t| t.kind == TypeKind::Scalar)
1432            .filter_map(|t| t.name.as_ref())
1433            .collect();
1434        assert!(scalar_names.contains(&&"Int".to_string()));
1435        assert!(scalar_names.contains(&&"String".to_string()));
1436        assert!(scalar_names.contains(&&"Boolean".to_string()));
1437
1438        // Should have User type
1439        let user_type = introspection
1440            .types
1441            .iter()
1442            .find(|t| t.name.as_ref() == Some(&"User".to_string()));
1443        assert!(user_type.is_some());
1444        let user_type = user_type.unwrap();
1445        assert_eq!(user_type.kind, TypeKind::Object);
1446        assert!(user_type.fields.is_some());
1447        assert_eq!(user_type.fields.as_ref().unwrap().len(), 3);
1448    }
1449
1450    #[test]
1451    fn test_build_introspection_responses() {
1452        let schema = test_schema();
1453        let responses = IntrospectionResponses::build(&schema);
1454
1455        // Should have schema response
1456        assert!(responses.schema_response.contains("__schema"));
1457        assert!(responses.schema_response.contains("Query"));
1458
1459        // Should have type responses
1460        assert!(responses.type_responses.contains_key("User"));
1461        assert!(responses.type_responses.contains_key("Query"));
1462        assert!(responses.type_responses.contains_key("Int"));
1463
1464        // Unknown type should return null
1465        let unknown = responses.get_type_response("Unknown");
1466        assert!(unknown.contains("null"));
1467    }
1468
1469    #[test]
1470    fn test_query_field_introspection() {
1471        let schema = test_schema();
1472        let introspection = IntrospectionBuilder::build(&schema);
1473
1474        let query_type = introspection
1475            .types
1476            .iter()
1477            .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
1478            .unwrap();
1479
1480        let fields = query_type.fields.as_ref().unwrap();
1481
1482        // Should have 'users' query
1483        let users_field = fields.iter().find(|f| f.name == "users").unwrap();
1484        assert_eq!(users_field.field_type.kind, TypeKind::NonNull);
1485        assert!(users_field.args.is_empty());
1486
1487        // Should have 'user' query with argument
1488        let user_field = fields.iter().find(|f| f.name == "user").unwrap();
1489        assert!(!user_field.args.is_empty());
1490        assert_eq!(user_field.args[0].name, "id");
1491    }
1492
1493    #[test]
1494    fn test_field_type_non_null() {
1495        let schema = test_schema();
1496        let introspection = IntrospectionBuilder::build(&schema);
1497
1498        let user_type = introspection
1499            .types
1500            .iter()
1501            .find(|t| t.name.as_ref() == Some(&"User".to_string()))
1502            .unwrap();
1503
1504        let fields = user_type.fields.as_ref().unwrap();
1505
1506        // 'id' should be NON_NULL
1507        let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1508        assert_eq!(id_field.field_type.kind, TypeKind::NonNull);
1509
1510        // 'email' should be nullable (not wrapped in NON_NULL)
1511        let email_field = fields.iter().find(|f| f.name == "email").unwrap();
1512        assert_ne!(email_field.field_type.kind, TypeKind::NonNull);
1513    }
1514
1515    #[test]
1516    fn test_deprecated_field_introspection() {
1517        use crate::schema::DeprecationInfo;
1518
1519        // Create a schema with a deprecated field
1520        let mut schema = CompiledSchema::new();
1521        schema.types.push(TypeDefinition {
1522            name:                "Product".to_string(),
1523            sql_source:          "products".to_string(),
1524            jsonb_column:        "data".to_string(),
1525            description:         None,
1526            sql_projection_hint: None,
1527            implements:          vec![],
1528            fields:              vec![
1529                FieldDefinition::new("id", FieldType::Id),
1530                FieldDefinition {
1531                    name:           "oldSku".to_string(),
1532                    field_type:     FieldType::String,
1533                    nullable:       false,
1534                    description:    Some("Legacy SKU field".to_string()),
1535                    default_value:  None,
1536                    vector_config:  None,
1537                    alias:          None,
1538                    deprecation:    Some(DeprecationInfo {
1539                        reason: Some("Use 'sku' instead".to_string()),
1540                    }),
1541                    requires_scope: None,
1542                    encryption:     None,
1543                },
1544                FieldDefinition::new("sku", FieldType::String),
1545            ],
1546        });
1547
1548        let introspection = IntrospectionBuilder::build(&schema);
1549
1550        // Find Product type
1551        let product_type = introspection
1552            .types
1553            .iter()
1554            .find(|t| t.name.as_ref() == Some(&"Product".to_string()))
1555            .unwrap();
1556
1557        let fields = product_type.fields.as_ref().unwrap();
1558
1559        // 'oldSku' should be deprecated
1560        let old_sku_field = fields.iter().find(|f| f.name == "oldSku").unwrap();
1561        assert!(old_sku_field.is_deprecated);
1562        assert_eq!(old_sku_field.deprecation_reason, Some("Use 'sku' instead".to_string()));
1563
1564        // 'sku' should NOT be deprecated
1565        let sku_field = fields.iter().find(|f| f.name == "sku").unwrap();
1566        assert!(!sku_field.is_deprecated);
1567        assert!(sku_field.deprecation_reason.is_none());
1568
1569        // 'id' should NOT be deprecated
1570        let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1571        assert!(!id_field.is_deprecated);
1572        assert!(id_field.deprecation_reason.is_none());
1573    }
1574
1575    #[test]
1576    fn test_enum_type_introspection() {
1577        use crate::schema::{EnumDefinition, EnumValueDefinition};
1578
1579        let mut schema = CompiledSchema::new();
1580
1581        // Add an enum type with some values, one deprecated
1582        schema.enums.push(EnumDefinition {
1583            name:        "OrderStatus".to_string(),
1584            description: Some("Status of an order".to_string()),
1585            values:      vec![
1586                EnumValueDefinition {
1587                    name:        "PENDING".to_string(),
1588                    description: Some("Order is pending".to_string()),
1589                    deprecation: None,
1590                },
1591                EnumValueDefinition {
1592                    name:        "PROCESSING".to_string(),
1593                    description: None,
1594                    deprecation: None,
1595                },
1596                EnumValueDefinition {
1597                    name:        "SHIPPED".to_string(),
1598                    description: None,
1599                    deprecation: None,
1600                },
1601                EnumValueDefinition {
1602                    name:        "CANCELLED".to_string(),
1603                    description: Some("Order was cancelled".to_string()),
1604                    deprecation: Some(crate::schema::DeprecationInfo {
1605                        reason: Some("Use REFUNDED instead".to_string()),
1606                    }),
1607                },
1608            ],
1609        });
1610
1611        let introspection = IntrospectionBuilder::build(&schema);
1612
1613        // Find OrderStatus enum
1614        let order_status = introspection
1615            .types
1616            .iter()
1617            .find(|t| t.name.as_ref() == Some(&"OrderStatus".to_string()))
1618            .unwrap();
1619
1620        assert_eq!(order_status.kind, TypeKind::Enum);
1621        assert_eq!(order_status.description, Some("Status of an order".to_string()));
1622
1623        // Should have enum_values
1624        let enum_values = order_status.enum_values.as_ref().unwrap();
1625        assert_eq!(enum_values.len(), 4);
1626
1627        // Check PENDING value
1628        let pending = enum_values.iter().find(|v| v.name == "PENDING").unwrap();
1629        assert_eq!(pending.description, Some("Order is pending".to_string()));
1630        assert!(!pending.is_deprecated);
1631        assert!(pending.deprecation_reason.is_none());
1632
1633        // Check CANCELLED value (deprecated)
1634        let cancelled = enum_values.iter().find(|v| v.name == "CANCELLED").unwrap();
1635        assert!(cancelled.is_deprecated);
1636        assert_eq!(cancelled.deprecation_reason, Some("Use REFUNDED instead".to_string()));
1637
1638        // Enum should not have fields
1639        assert!(order_status.fields.is_none());
1640    }
1641
1642    #[test]
1643    fn test_input_object_introspection() {
1644        use crate::schema::{InputFieldDefinition, InputObjectDefinition};
1645
1646        let mut schema = CompiledSchema::new();
1647
1648        // Add an input object type
1649        schema.input_types.push(InputObjectDefinition {
1650            name:        "UserFilter".to_string(),
1651            description: Some("Filter for user queries".to_string()),
1652            fields:      vec![
1653                InputFieldDefinition {
1654                    name:             "name".to_string(),
1655                    field_type:       "String".to_string(),
1656                    description:      Some("Filter by name".to_string()),
1657                    default_value:    None,
1658                    deprecation:      None,
1659                    validation_rules: Vec::new(),
1660                },
1661                InputFieldDefinition {
1662                    name:             "email".to_string(),
1663                    field_type:       "String".to_string(),
1664                    description:      None,
1665                    default_value:    None,
1666                    deprecation:      None,
1667                    validation_rules: Vec::new(),
1668                },
1669                InputFieldDefinition {
1670                    name:             "limit".to_string(),
1671                    field_type:       "Int".to_string(),
1672                    description:      Some("Max results".to_string()),
1673                    default_value:    Some("10".to_string()),
1674                    deprecation:      None,
1675                    validation_rules: Vec::new(),
1676                },
1677            ],
1678            metadata:    None,
1679        });
1680
1681        let introspection = IntrospectionBuilder::build(&schema);
1682
1683        // Find UserFilter input type
1684        let user_filter = introspection
1685            .types
1686            .iter()
1687            .find(|t| t.name.as_ref() == Some(&"UserFilter".to_string()))
1688            .unwrap();
1689
1690        assert_eq!(user_filter.kind, TypeKind::InputObject);
1691        assert_eq!(user_filter.description, Some("Filter for user queries".to_string()));
1692
1693        // Should have input_fields
1694        let input_fields = user_filter.input_fields.as_ref().unwrap();
1695        assert_eq!(input_fields.len(), 3);
1696
1697        // Check name field
1698        let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
1699        assert_eq!(name_field.description, Some("Filter by name".to_string()));
1700        assert!(name_field.default_value.is_none());
1701
1702        // Check limit field with default value
1703        let limit_field = input_fields.iter().find(|f| f.name == "limit").unwrap();
1704        assert_eq!(limit_field.description, Some("Max results".to_string()));
1705        assert_eq!(limit_field.default_value, Some("10".to_string()));
1706
1707        // Input object should not have fields
1708        assert!(user_filter.fields.is_none());
1709    }
1710
1711    #[test]
1712    fn test_enum_in_type_map() {
1713        use crate::schema::EnumDefinition;
1714
1715        let mut schema = CompiledSchema::new();
1716        schema.enums.push(EnumDefinition {
1717            name:        "Status".to_string(),
1718            description: None,
1719            values:      vec![],
1720        });
1721
1722        let introspection = IntrospectionBuilder::build(&schema);
1723        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1724
1725        // Enum should be in the type map
1726        assert!(type_map.contains_key("Status"));
1727        let status = type_map.get("Status").unwrap();
1728        assert_eq!(status.kind, TypeKind::Enum);
1729    }
1730
1731    #[test]
1732    fn test_input_object_in_type_map() {
1733        use crate::schema::InputObjectDefinition;
1734
1735        let mut schema = CompiledSchema::new();
1736        schema.input_types.push(InputObjectDefinition {
1737            name:        "CreateUserInput".to_string(),
1738            description: None,
1739            fields:      vec![],
1740            metadata:    None,
1741        });
1742
1743        let introspection = IntrospectionBuilder::build(&schema);
1744        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1745
1746        // Input object should be in the type map
1747        assert!(type_map.contains_key("CreateUserInput"));
1748        let input = type_map.get("CreateUserInput").unwrap();
1749        assert_eq!(input.kind, TypeKind::InputObject);
1750    }
1751
1752    #[test]
1753    fn test_interface_introspection() {
1754        use crate::schema::InterfaceDefinition;
1755
1756        let mut schema = CompiledSchema::new();
1757
1758        // Add a Node interface
1759        schema.interfaces.push(InterfaceDefinition {
1760            name:        "Node".to_string(),
1761            description: Some("An object with a globally unique ID".to_string()),
1762            fields:      vec![FieldDefinition::new("id", FieldType::Id)],
1763        });
1764
1765        // Add types that implement the interface
1766        schema.types.push(TypeDefinition {
1767            name:                "User".to_string(),
1768            sql_source:          "users".to_string(),
1769            jsonb_column:        "data".to_string(),
1770            description:         Some("A user".to_string()),
1771            sql_projection_hint: None,
1772            implements:          vec!["Node".to_string()],
1773            fields:              vec![
1774                FieldDefinition::new("id", FieldType::Id),
1775                FieldDefinition::new("name", FieldType::String),
1776            ],
1777        });
1778
1779        schema.types.push(TypeDefinition {
1780            name:                "Post".to_string(),
1781            sql_source:          "posts".to_string(),
1782            jsonb_column:        "data".to_string(),
1783            description:         Some("A blog post".to_string()),
1784            sql_projection_hint: None,
1785            implements:          vec!["Node".to_string()],
1786            fields:              vec![
1787                FieldDefinition::new("id", FieldType::Id),
1788                FieldDefinition::new("title", FieldType::String),
1789            ],
1790        });
1791
1792        let introspection = IntrospectionBuilder::build(&schema);
1793
1794        // Find Node interface
1795        let node = introspection
1796            .types
1797            .iter()
1798            .find(|t| t.name.as_ref() == Some(&"Node".to_string()))
1799            .unwrap();
1800
1801        assert_eq!(node.kind, TypeKind::Interface);
1802        assert_eq!(node.description, Some("An object with a globally unique ID".to_string()));
1803
1804        // Interface should have fields
1805        let fields = node.fields.as_ref().unwrap();
1806        assert_eq!(fields.len(), 1);
1807        assert_eq!(fields[0].name, "id");
1808
1809        // Interface should have possible_types (implementors)
1810        let possible_types = node.possible_types.as_ref().unwrap();
1811        assert_eq!(possible_types.len(), 2);
1812        assert!(possible_types.iter().any(|t| t.name == "User"));
1813        assert!(possible_types.iter().any(|t| t.name == "Post"));
1814
1815        // Interface should not have enum_values or input_fields
1816        assert!(node.enum_values.is_none());
1817        assert!(node.input_fields.is_none());
1818    }
1819
1820    #[test]
1821    fn test_type_implements_interface() {
1822        use crate::schema::InterfaceDefinition;
1823
1824        let mut schema = CompiledSchema::new();
1825
1826        // Add interfaces
1827        schema.interfaces.push(InterfaceDefinition {
1828            name:        "Node".to_string(),
1829            description: None,
1830            fields:      vec![FieldDefinition::new("id", FieldType::Id)],
1831        });
1832
1833        schema.interfaces.push(InterfaceDefinition {
1834            name:        "Timestamped".to_string(),
1835            description: None,
1836            fields:      vec![FieldDefinition::new("createdAt", FieldType::DateTime)],
1837        });
1838
1839        // Add a type that implements both interfaces
1840        schema.types.push(TypeDefinition {
1841            name:                "Comment".to_string(),
1842            sql_source:          "comments".to_string(),
1843            jsonb_column:        "data".to_string(),
1844            description:         None,
1845            sql_projection_hint: None,
1846            implements:          vec!["Node".to_string(), "Timestamped".to_string()],
1847            fields:              vec![
1848                FieldDefinition::new("id", FieldType::Id),
1849                FieldDefinition::new("createdAt", FieldType::DateTime),
1850                FieldDefinition::new("text", FieldType::String),
1851            ],
1852        });
1853
1854        let introspection = IntrospectionBuilder::build(&schema);
1855
1856        // Find Comment type
1857        let comment = introspection
1858            .types
1859            .iter()
1860            .find(|t| t.name.as_ref() == Some(&"Comment".to_string()))
1861            .unwrap();
1862
1863        assert_eq!(comment.kind, TypeKind::Object);
1864
1865        // Type should list interfaces it implements
1866        let interfaces = comment.interfaces.as_ref().unwrap();
1867        assert_eq!(interfaces.len(), 2);
1868        assert!(interfaces.iter().any(|i| i.name == "Node"));
1869        assert!(interfaces.iter().any(|i| i.name == "Timestamped"));
1870    }
1871
1872    #[test]
1873    fn test_interface_in_type_map() {
1874        use crate::schema::InterfaceDefinition;
1875
1876        let mut schema = CompiledSchema::new();
1877        schema.interfaces.push(InterfaceDefinition {
1878            name:        "Searchable".to_string(),
1879            description: None,
1880            fields:      vec![],
1881        });
1882
1883        let introspection = IntrospectionBuilder::build(&schema);
1884        let type_map = IntrospectionBuilder::build_type_map(&introspection);
1885
1886        // Interface should be in the type map
1887        assert!(type_map.contains_key("Searchable"));
1888        let interface = type_map.get("Searchable").unwrap();
1889        assert_eq!(interface.kind, TypeKind::Interface);
1890    }
1891
1892    #[test]
1893    fn test_filter_deprecated_fields() {
1894        // Create a type with some deprecated fields
1895        let introspection_type = IntrospectionType {
1896            kind:               TypeKind::Object,
1897            name:               Some("TestType".to_string()),
1898            description:        None,
1899            fields:             Some(vec![
1900                IntrospectionField {
1901                    name:               "id".to_string(),
1902                    description:        None,
1903                    args:               vec![],
1904                    field_type:         IntrospectionBuilder::type_ref("ID"),
1905                    is_deprecated:      false,
1906                    deprecation_reason: None,
1907                },
1908                IntrospectionField {
1909                    name:               "oldField".to_string(),
1910                    description:        None,
1911                    args:               vec![],
1912                    field_type:         IntrospectionBuilder::type_ref("String"),
1913                    is_deprecated:      true,
1914                    deprecation_reason: Some("Use newField instead".to_string()),
1915                },
1916                IntrospectionField {
1917                    name:               "newField".to_string(),
1918                    description:        None,
1919                    args:               vec![],
1920                    field_type:         IntrospectionBuilder::type_ref("String"),
1921                    is_deprecated:      false,
1922                    deprecation_reason: None,
1923                },
1924            ]),
1925            interfaces:         None,
1926            possible_types:     None,
1927            enum_values:        None,
1928            input_fields:       None,
1929            of_type:            None,
1930            specified_by_u_r_l: None,
1931        };
1932
1933        // With includeDeprecated = false, should only have 2 fields
1934        let filtered = introspection_type.filter_deprecated_fields(false);
1935        let fields = filtered.fields.as_ref().unwrap();
1936        assert_eq!(fields.len(), 2);
1937        assert!(fields.iter().any(|f| f.name == "id"));
1938        assert!(fields.iter().any(|f| f.name == "newField"));
1939        assert!(!fields.iter().any(|f| f.name == "oldField"));
1940
1941        // With includeDeprecated = true, should have all 3 fields
1942        let unfiltered = introspection_type.filter_deprecated_fields(true);
1943        let fields = unfiltered.fields.as_ref().unwrap();
1944        assert_eq!(fields.len(), 3);
1945    }
1946
1947    #[test]
1948    fn test_filter_deprecated_enum_values() {
1949        // Create an enum type with some deprecated values
1950        let introspection_type = IntrospectionType {
1951            kind:               TypeKind::Enum,
1952            name:               Some("Status".to_string()),
1953            description:        None,
1954            fields:             None,
1955            interfaces:         None,
1956            possible_types:     None,
1957            enum_values:        Some(vec![
1958                IntrospectionEnumValue {
1959                    name:               "ACTIVE".to_string(),
1960                    description:        None,
1961                    is_deprecated:      false,
1962                    deprecation_reason: None,
1963                },
1964                IntrospectionEnumValue {
1965                    name:               "INACTIVE".to_string(),
1966                    description:        None,
1967                    is_deprecated:      true,
1968                    deprecation_reason: Some("Use DISABLED instead".to_string()),
1969                },
1970                IntrospectionEnumValue {
1971                    name:               "DISABLED".to_string(),
1972                    description:        None,
1973                    is_deprecated:      false,
1974                    deprecation_reason: None,
1975                },
1976            ]),
1977            input_fields:       None,
1978            of_type:            None,
1979            specified_by_u_r_l: None,
1980        };
1981
1982        // With includeDeprecated = false, should only have 2 values
1983        let filtered = introspection_type.filter_deprecated_enum_values(false);
1984        let values = filtered.enum_values.as_ref().unwrap();
1985        assert_eq!(values.len(), 2);
1986        assert!(values.iter().any(|v| v.name == "ACTIVE"));
1987        assert!(values.iter().any(|v| v.name == "DISABLED"));
1988        assert!(!values.iter().any(|v| v.name == "INACTIVE"));
1989
1990        // With includeDeprecated = true, should have all 3 values
1991        let unfiltered = introspection_type.filter_deprecated_enum_values(true);
1992        let values = unfiltered.enum_values.as_ref().unwrap();
1993        assert_eq!(values.len(), 3);
1994    }
1995
1996    #[test]
1997    fn test_specified_by_url_for_custom_scalars() {
1998        let schema = CompiledSchema::new();
1999        let introspection = IntrospectionBuilder::build(&schema);
2000
2001        // Find DateTime scalar
2002        let datetime = introspection
2003            .types
2004            .iter()
2005            .find(|t| t.name.as_ref() == Some(&"DateTime".to_string()))
2006            .unwrap();
2007
2008        assert_eq!(datetime.kind, TypeKind::Scalar);
2009        assert!(datetime.specified_by_u_r_l.is_some());
2010        assert!(datetime.specified_by_u_r_l.as_ref().unwrap().contains("date-time"));
2011
2012        // Find UUID scalar
2013        let uuid = introspection
2014            .types
2015            .iter()
2016            .find(|t| t.name.as_ref() == Some(&"UUID".to_string()))
2017            .unwrap();
2018
2019        assert_eq!(uuid.kind, TypeKind::Scalar);
2020        assert!(uuid.specified_by_u_r_l.is_some());
2021        assert!(uuid.specified_by_u_r_l.as_ref().unwrap().contains("rfc4122"));
2022
2023        // Built-in scalars (Int, String, etc.) should NOT have specifiedByURL
2024        let int = introspection
2025            .types
2026            .iter()
2027            .find(|t| t.name.as_ref() == Some(&"Int".to_string()))
2028            .unwrap();
2029
2030        assert_eq!(int.kind, TypeKind::Scalar);
2031        assert!(int.specified_by_u_r_l.is_none());
2032    }
2033
2034    #[test]
2035    fn test_deprecated_query_introspection() {
2036        use crate::schema::{ArgumentDefinition, AutoParams, DeprecationInfo};
2037
2038        let mut schema = CompiledSchema::new();
2039
2040        // Add a deprecated query
2041        schema.queries.push(QueryDefinition {
2042            name:         "oldUsers".to_string(),
2043            return_type:  "User".to_string(),
2044            returns_list: true,
2045            nullable:     false,
2046            arguments:    vec![],
2047            sql_source:   Some("v_user".to_string()),
2048            description:  Some("Old way to get users".to_string()),
2049            auto_params:  AutoParams::default(),
2050            deprecation:  Some(DeprecationInfo {
2051                reason: Some("Use 'users' instead".to_string()),
2052            }),
2053            jsonb_column: "data".to_string(),
2054        });
2055
2056        // Add a non-deprecated query with a deprecated argument
2057        schema.queries.push(QueryDefinition {
2058            name:         "users".to_string(),
2059            return_type:  "User".to_string(),
2060            returns_list: true,
2061            nullable:     false,
2062            arguments:    vec![
2063                ArgumentDefinition {
2064                    name:          "first".to_string(),
2065                    arg_type:      FieldType::Int,
2066                    nullable:      true,
2067                    default_value: None,
2068                    description:   Some("Number of results to return".to_string()),
2069                    deprecation:   None,
2070                },
2071                ArgumentDefinition {
2072                    name:          "limit".to_string(),
2073                    arg_type:      FieldType::Int,
2074                    nullable:      true,
2075                    default_value: None,
2076                    description:   Some("Old parameter for limiting results".to_string()),
2077                    deprecation:   Some(DeprecationInfo {
2078                        reason: Some("Use 'first' instead".to_string()),
2079                    }),
2080                },
2081            ],
2082            sql_source:   Some("v_user".to_string()),
2083            description:  Some("Get users with pagination".to_string()),
2084            auto_params:  AutoParams::default(),
2085            deprecation:  None,
2086            jsonb_column: "data".to_string(),
2087        });
2088
2089        let introspection = IntrospectionBuilder::build(&schema);
2090
2091        // Find Query type
2092        let query_type = introspection
2093            .types
2094            .iter()
2095            .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
2096            .unwrap();
2097
2098        let fields = query_type.fields.as_ref().unwrap();
2099
2100        // 'oldUsers' should be deprecated
2101        let old_users = fields.iter().find(|f| f.name == "oldUsers").unwrap();
2102        assert!(old_users.is_deprecated);
2103        assert_eq!(old_users.deprecation_reason, Some("Use 'users' instead".to_string()));
2104
2105        // 'users' should NOT be deprecated
2106        let users = fields.iter().find(|f| f.name == "users").unwrap();
2107        assert!(!users.is_deprecated);
2108        assert!(users.deprecation_reason.is_none());
2109
2110        // 'users' should have 2 arguments
2111        assert_eq!(users.args.len(), 2);
2112
2113        // 'first' argument should NOT be deprecated
2114        let first_arg = users.args.iter().find(|a| a.name == "first").unwrap();
2115        assert!(!first_arg.is_deprecated);
2116        assert!(first_arg.deprecation_reason.is_none());
2117
2118        // 'limit' argument should be deprecated
2119        let limit_arg = users.args.iter().find(|a| a.name == "limit").unwrap();
2120        assert!(limit_arg.is_deprecated);
2121        assert_eq!(limit_arg.deprecation_reason, Some("Use 'first' instead".to_string()));
2122    }
2123
2124    #[test]
2125    fn test_deprecated_input_field_introspection() {
2126        use crate::schema::{DeprecationInfo, InputFieldDefinition, InputObjectDefinition};
2127
2128        let mut schema = CompiledSchema::new();
2129
2130        // Add an input type with a deprecated field
2131        schema.input_types.push(InputObjectDefinition {
2132            name:        "CreateUserInput".to_string(),
2133            description: Some("Input for creating a user".to_string()),
2134            fields:      vec![
2135                InputFieldDefinition {
2136                    name:             "name".to_string(),
2137                    field_type:       "String!".to_string(),
2138                    default_value:    None,
2139                    description:      Some("User name".to_string()),
2140                    deprecation:      None,
2141                    validation_rules: Vec::new(),
2142                },
2143                InputFieldDefinition {
2144                    name:             "oldEmail".to_string(),
2145                    field_type:       "String".to_string(),
2146                    default_value:    None,
2147                    description:      Some("Legacy email field".to_string()),
2148                    deprecation:      Some(DeprecationInfo {
2149                        reason: Some("Use 'email' instead".to_string()),
2150                    }),
2151                    validation_rules: Vec::new(),
2152                },
2153            ],
2154            metadata:    None,
2155        });
2156
2157        let introspection = IntrospectionBuilder::build(&schema);
2158
2159        // Find CreateUserInput type
2160        let create_user_input = introspection
2161            .types
2162            .iter()
2163            .find(|t| t.name.as_ref() == Some(&"CreateUserInput".to_string()))
2164            .unwrap();
2165
2166        let input_fields = create_user_input.input_fields.as_ref().unwrap();
2167
2168        // 'name' should NOT be deprecated
2169        let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
2170        assert!(!name_field.is_deprecated);
2171        assert!(name_field.deprecation_reason.is_none());
2172
2173        // 'oldEmail' should be deprecated
2174        let old_email = input_fields.iter().find(|f| f.name == "oldEmail").unwrap();
2175        assert!(old_email.is_deprecated);
2176        assert_eq!(old_email.deprecation_reason, Some("Use 'email' instead".to_string()));
2177    }
2178}