Skip to main content

fraiseql_cli/schema/
converter.rs

1//! Schema Converter
2//!
3//! Converts `IntermediateSchema` (language-agnostic) to `CompiledSchema` (Rust-specific)
4
5use std::collections::HashSet;
6
7use anyhow::{Context, Result};
8use fraiseql_core::schema::{
9    ArgumentDefinition, AutoParams, CompiledSchema, DirectiveDefinition, DirectiveLocationKind,
10    EnumDefinition, EnumValueDefinition, FieldDefinition, FieldType, InputFieldDefinition,
11    InputObjectDefinition, InterfaceDefinition, MutationDefinition, MutationOperation,
12    QueryDefinition, SubscriptionDefinition, SubscriptionFilter, TypeDefinition, UnionDefinition,
13};
14use fraiseql_core::validation::{CustomTypeDef, CustomTypeRegistry};
15use tracing::{info, warn};
16
17use super::{
18    intermediate::{
19        IntermediateArgument, IntermediateAutoParams, IntermediateDirective, IntermediateEnum,
20        IntermediateEnumValue, IntermediateField, IntermediateInputField, IntermediateInputObject,
21        IntermediateInterface, IntermediateMutation, IntermediateQuery, IntermediateSchema,
22        IntermediateScalar, IntermediateSubscription, IntermediateType, IntermediateUnion,
23    },
24    rich_filters::{RichFilterConfig, compile_rich_filters},
25};
26
27/// Converts intermediate format to compiled format
28pub struct SchemaConverter;
29
30impl SchemaConverter {
31    /// Convert `IntermediateSchema` to `CompiledSchema`
32    ///
33    /// This performs:
34    /// 1. Type conversion (intermediate types → compiled types)
35    /// 2. Field name normalization (type → `field_type`)
36    /// 3. Validation (type references, circular refs, etc.)
37    /// 4. Optimization (for future phases)
38    ///
39    /// # Panics
40    ///
41    /// Panics if fact table metadata serialization fails (which should never happen
42    /// for valid `FactTable` structures).
43    pub fn convert(intermediate: IntermediateSchema) -> Result<CompiledSchema> {
44        info!("Converting intermediate schema to compiled format");
45
46        // Convert types
47        let types = intermediate
48            .types
49            .into_iter()
50            .map(Self::convert_type)
51            .collect::<Result<Vec<_>>>()
52            .context("Failed to convert types")?;
53
54        // Convert queries
55        let queries = intermediate
56            .queries
57            .into_iter()
58            .map(Self::convert_query)
59            .collect::<Result<Vec<_>>>()
60            .context("Failed to convert queries")?;
61
62        // Convert mutations
63        let mutations = intermediate
64            .mutations
65            .into_iter()
66            .map(Self::convert_mutation)
67            .collect::<Result<Vec<_>>>()
68            .context("Failed to convert mutations")?;
69
70        // Convert enums
71        let enums = intermediate.enums.into_iter().map(Self::convert_enum).collect::<Vec<_>>();
72
73        // Convert input types
74        let input_types = intermediate
75            .input_types
76            .into_iter()
77            .map(Self::convert_input_object)
78            .collect::<Vec<_>>();
79
80        // Convert interfaces
81        let interfaces = intermediate
82            .interfaces
83            .into_iter()
84            .map(Self::convert_interface)
85            .collect::<Result<Vec<_>>>()
86            .context("Failed to convert interfaces")?;
87
88        // Convert unions
89        let unions = intermediate.unions.into_iter().map(Self::convert_union).collect::<Vec<_>>();
90
91        // Convert subscriptions
92        let subscriptions = intermediate
93            .subscriptions
94            .into_iter()
95            .map(Self::convert_subscription)
96            .collect::<Result<Vec<_>>>()
97            .context("Failed to convert subscriptions")?;
98
99        // Convert custom directives
100        let directives = intermediate
101            .directives
102            .unwrap_or_default()
103            .into_iter()
104            .map(Self::convert_directive)
105            .collect::<Result<Vec<_>>>()
106            .context("Failed to convert directives")?;
107
108        // Convert fact tables from Vec to HashMap<String, serde_json::Value>
109        let fact_tables = intermediate
110            .fact_tables
111            .unwrap_or_default()
112            .into_iter()
113            .map(|ft| {
114                let metadata =
115                    serde_json::to_value(&ft).expect("Failed to serialize fact table metadata");
116                (ft.table_name, metadata)
117            })
118            .collect();
119
120        let mut compiled = CompiledSchema {
121            types,
122            enums,
123            input_types,
124            interfaces,
125            unions,
126            queries,
127            mutations,
128            subscriptions,
129            directives,
130            fact_tables, // Analytics metadata
131            observers: Vec::new(), /* Observer definitions (populated from
132                          * IntermediateSchema) */
133            federation: None,                // Federation metadata
134            security: intermediate.security, // Security configuration from TOML
135            schema_sdl: None,                // Raw GraphQL SDL
136            custom_scalars: CustomTypeRegistry::default(), // Custom scalar registry
137        };
138
139        // Populate custom scalars from intermediate schema
140        if let Some(custom_scalars_vec) = intermediate.custom_scalars {
141            for scalar_def in custom_scalars_vec {
142                let custom_type = Self::convert_custom_scalar(scalar_def)?;
143                compiled.custom_scalars.register(
144                    custom_type.name.clone(),
145                    custom_type,
146                ).context("Failed to register custom scalar")?;
147            }
148        }
149
150        // Compile rich filter types (EmailAddress, VIN, IBAN, etc.)
151        let rich_filter_config = RichFilterConfig::default();
152        compile_rich_filters(&mut compiled, &rich_filter_config)
153            .context("Failed to compile rich filter types")?;
154
155        // Validate the compiled schema
156        Self::validate(&compiled)?;
157
158        info!("Schema conversion successful");
159        Ok(compiled)
160    }
161
162    /// Convert `IntermediateType` to `TypeDefinition`
163    fn convert_type(intermediate: IntermediateType) -> Result<TypeDefinition> {
164        let fields = intermediate
165            .fields
166            .into_iter()
167            .map(Self::convert_field)
168            .collect::<Result<Vec<_>>>()
169            .context(format!("Failed to convert type '{}'", intermediate.name))?;
170
171        Ok(TypeDefinition {
172            name: intermediate.name,
173            fields,
174            description: intermediate.description,
175            sql_source: String::new(), // Not used for regular types (empty string)
176            jsonb_column: String::new(), // Not used for regular types (empty string)
177            sql_projection_hint: None, // Will be populated by optimizer in
178            implements: intermediate.implements,
179        })
180    }
181
182    /// Convert `IntermediateEnum` to `EnumDefinition`
183    fn convert_enum(intermediate: IntermediateEnum) -> EnumDefinition {
184        let values = intermediate.values.into_iter().map(Self::convert_enum_value).collect();
185
186        EnumDefinition {
187            name: intermediate.name,
188            values,
189            description: intermediate.description,
190        }
191    }
192
193    /// Convert `IntermediateEnumValue` to `EnumValueDefinition`
194    fn convert_enum_value(intermediate: IntermediateEnumValue) -> EnumValueDefinition {
195        let deprecation = intermediate
196            .deprecated
197            .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
198
199        EnumValueDefinition {
200            name: intermediate.name,
201            description: intermediate.description,
202            deprecation,
203        }
204    }
205
206    /// Convert `IntermediateScalar` to `CustomTypeDef`
207    fn convert_custom_scalar(intermediate: IntermediateScalar) -> Result<CustomTypeDef> {
208        Ok(CustomTypeDef {
209            name: intermediate.name,
210            description: intermediate.description,
211            specified_by_url: intermediate.specified_by_url,
212            validation_rules: intermediate.validation_rules,
213            elo_expression: None,
214            base_type: intermediate.base_type,
215        })
216    }
217
218    /// Convert `IntermediateInputObject` to `InputObjectDefinition`
219    fn convert_input_object(intermediate: IntermediateInputObject) -> InputObjectDefinition {
220        let fields = intermediate.fields.into_iter().map(Self::convert_input_field).collect();
221
222        InputObjectDefinition {
223            name: intermediate.name,
224            fields,
225            description: intermediate.description,
226            metadata: None,
227        }
228    }
229
230    /// Convert `IntermediateInputField` to `InputFieldDefinition`
231    fn convert_input_field(intermediate: IntermediateInputField) -> InputFieldDefinition {
232        let deprecation = intermediate
233            .deprecated
234            .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
235
236        // Convert default value to JSON string if present
237        let default_value = intermediate.default.map(|v| v.to_string());
238
239        InputFieldDefinition {
240            name: intermediate.name,
241            field_type: intermediate.field_type,
242            description: intermediate.description,
243            default_value,
244            deprecation,
245            validation_rules: Vec::new(),
246        }
247    }
248
249    /// Convert `IntermediateInterface` to `InterfaceDefinition`
250    fn convert_interface(intermediate: IntermediateInterface) -> Result<InterfaceDefinition> {
251        let fields = intermediate
252            .fields
253            .into_iter()
254            .map(Self::convert_field)
255            .collect::<Result<Vec<_>>>()
256            .context(format!("Failed to convert interface '{}'", intermediate.name))?;
257
258        Ok(InterfaceDefinition {
259            name: intermediate.name,
260            fields,
261            description: intermediate.description,
262        })
263    }
264
265    /// Convert `IntermediateUnion` to `UnionDefinition`
266    fn convert_union(intermediate: IntermediateUnion) -> UnionDefinition {
267        let mut union_def =
268            UnionDefinition::new(&intermediate.name).with_members(intermediate.member_types);
269        if let Some(desc) = intermediate.description {
270            union_def = union_def.with_description(&desc);
271        }
272        union_def
273    }
274
275    /// Convert `IntermediateField` to `FieldDefinition`
276    ///
277    /// **Key normalization**: `type` → `field_type`
278    fn convert_field(intermediate: IntermediateField) -> Result<FieldDefinition> {
279        let field_type = Self::parse_field_type(&intermediate.field_type)?;
280
281        // Extract deprecation info from @deprecated directive if present
282        let deprecation = intermediate.directives.as_ref().and_then(|directives| {
283            directives.iter().find(|d| d.name == "deprecated").map(|d| {
284                let reason = d
285                    .arguments
286                    .as_ref()
287                    .and_then(|args| args.get("reason").and_then(|v| v.as_str()).map(String::from));
288                fraiseql_core::schema::DeprecationInfo { reason }
289            })
290        });
291
292        Ok(FieldDefinition {
293            name: intermediate.name,
294            field_type,
295            nullable: intermediate.nullable,
296            default_value: None,
297            description: intermediate.description,
298            vector_config: None,
299            alias: None,
300            deprecation,
301            requires_scope: intermediate.requires_scope,
302        })
303    }
304
305    /// Parse string type name to `FieldType` enum
306    ///
307    /// Handles built-in scalars and custom object types
308    fn parse_field_type(type_name: &str) -> Result<FieldType> {
309        match type_name {
310            "String" => Ok(FieldType::String),
311            "Int" => Ok(FieldType::Int),
312            "Float" => Ok(FieldType::Float),
313            "Boolean" => Ok(FieldType::Boolean),
314            "ID" => Ok(FieldType::Id),
315            "DateTime" => Ok(FieldType::DateTime),
316            "Date" => Ok(FieldType::Date),
317            "Time" => Ok(FieldType::Time),
318            "Json" => Ok(FieldType::Json),
319            "UUID" => Ok(FieldType::Uuid),
320            "Decimal" => Ok(FieldType::Decimal),
321            "Vector" => Ok(FieldType::Vector),
322            // Custom object types (User, Post, etc.)
323            custom => Ok(FieldType::Object(custom.to_string())),
324        }
325    }
326
327    /// Convert `IntermediateQuery` to `QueryDefinition`
328    fn convert_query(intermediate: IntermediateQuery) -> Result<QueryDefinition> {
329        let arguments = intermediate
330            .arguments
331            .into_iter()
332            .map(Self::convert_argument)
333            .collect::<Result<Vec<_>>>()
334            .context(format!("Failed to convert query '{}'", intermediate.name))?;
335
336        let auto_params =
337            intermediate.auto_params.map(Self::convert_auto_params).unwrap_or_default();
338
339        let deprecation = intermediate
340            .deprecated
341            .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
342
343        Ok(QueryDefinition {
344            name: intermediate.name,
345            return_type: intermediate.return_type,
346            returns_list: intermediate.returns_list,
347            nullable: intermediate.nullable,
348            arguments,
349            sql_source: intermediate.sql_source,
350            description: intermediate.description,
351            auto_params,
352            deprecation,
353            jsonb_column: intermediate.jsonb_column.unwrap_or_else(|| "data".to_string()),
354        })
355    }
356
357    /// Convert `IntermediateMutation` to `MutationDefinition`
358    fn convert_mutation(intermediate: IntermediateMutation) -> Result<MutationDefinition> {
359        let arguments = intermediate
360            .arguments
361            .into_iter()
362            .map(Self::convert_argument)
363            .collect::<Result<Vec<_>>>()
364            .context(format!("Failed to convert mutation '{}'", intermediate.name))?;
365
366        let operation = Self::parse_mutation_operation(
367            intermediate.operation.as_deref(),
368            intermediate.sql_source.as_deref(),
369        )?;
370
371        let deprecation = intermediate
372            .deprecated
373            .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
374
375        Ok(MutationDefinition {
376            name: intermediate.name,
377            return_type: intermediate.return_type,
378            arguments,
379            description: intermediate.description,
380            operation,
381            deprecation,
382        })
383    }
384
385    /// Parse mutation operation from string
386    ///
387    /// Converts intermediate format operation string to `MutationOperation` enum
388    fn parse_mutation_operation(
389        operation: Option<&str>,
390        sql_source: Option<&str>,
391    ) -> Result<MutationOperation> {
392        match operation {
393            Some("CREATE" | "INSERT") => {
394                // Extract table name from sql_source or use empty for Custom
395                let table = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
396                Ok(MutationOperation::Insert { table })
397            },
398            Some("UPDATE") => {
399                let table = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
400                Ok(MutationOperation::Update { table })
401            },
402            Some("DELETE") => {
403                let table = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
404                Ok(MutationOperation::Delete { table })
405            },
406            Some("FUNCTION") => {
407                let name = sql_source.map(std::string::ToString::to_string).unwrap_or_default();
408                Ok(MutationOperation::Function { name })
409            },
410            Some("CUSTOM") | None => Ok(MutationOperation::Custom),
411            Some(op) => {
412                anyhow::bail!("Unknown mutation operation: {op}")
413            },
414        }
415    }
416
417    /// Convert `IntermediateArgument` to `ArgumentDefinition`
418    fn convert_argument(intermediate: IntermediateArgument) -> Result<ArgumentDefinition> {
419        let arg_type = Self::parse_field_type(&intermediate.arg_type)?;
420
421        let deprecation = intermediate
422            .deprecated
423            .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
424
425        Ok(ArgumentDefinition {
426            name: intermediate.name,
427            arg_type,
428            nullable: intermediate.nullable,
429            default_value: intermediate.default,
430            description: None,
431            deprecation,
432        })
433    }
434
435    /// Convert `IntermediateAutoParams` to `AutoParams`
436    const fn convert_auto_params(intermediate: IntermediateAutoParams) -> AutoParams {
437        AutoParams {
438            has_limit:    intermediate.limit,
439            has_offset:   intermediate.offset,
440            has_where:    intermediate.where_clause,
441            has_order_by: intermediate.order_by,
442        }
443    }
444
445    /// Convert `IntermediateSubscription` to `SubscriptionDefinition`
446    fn convert_subscription(
447        intermediate: IntermediateSubscription,
448    ) -> Result<SubscriptionDefinition> {
449        let arguments = intermediate
450            .arguments
451            .into_iter()
452            .map(Self::convert_argument)
453            .collect::<Result<Vec<_>>>()
454            .context(format!("Failed to convert subscription '{}'", intermediate.name))?;
455
456        // Convert filter conditions to SubscriptionFilter
457        let filter = intermediate.filter.map(|f| {
458            let argument_paths = f.conditions.into_iter().map(|c| (c.argument, c.path)).collect();
459            SubscriptionFilter {
460                argument_paths,
461                static_filters: Vec::new(),
462            }
463        });
464
465        // Convert deprecation
466        let deprecation = intermediate
467            .deprecated
468            .map(|d| fraiseql_core::schema::DeprecationInfo { reason: d.reason });
469
470        Ok(SubscriptionDefinition {
471            name: intermediate.name,
472            return_type: intermediate.return_type,
473            arguments,
474            description: intermediate.description,
475            topic: intermediate.topic,
476            filter,
477            fields: intermediate.fields,
478            deprecation,
479        })
480    }
481
482    /// Convert `IntermediateDirective` to `DirectiveDefinition`
483    fn convert_directive(intermediate: IntermediateDirective) -> Result<DirectiveDefinition> {
484        let arguments = intermediate
485            .arguments
486            .into_iter()
487            .map(Self::convert_argument)
488            .collect::<Result<Vec<_>>>()
489            .context(format!("Failed to convert directive '{}'", intermediate.name))?;
490
491        // Parse directive locations
492        let locations = intermediate
493            .locations
494            .into_iter()
495            .filter_map(|loc| Self::parse_directive_location(&loc))
496            .collect();
497
498        Ok(DirectiveDefinition {
499            name: intermediate.name,
500            description: intermediate.description,
501            locations,
502            arguments,
503            is_repeatable: intermediate.repeatable,
504        })
505    }
506
507    /// Parse directive location string to `DirectiveLocationKind` enum
508    fn parse_directive_location(location: &str) -> Option<DirectiveLocationKind> {
509        match location {
510            // Type System Directive Locations
511            "SCHEMA" => Some(DirectiveLocationKind::Schema),
512            "SCALAR" => Some(DirectiveLocationKind::Scalar),
513            "OBJECT" => Some(DirectiveLocationKind::Object),
514            "FIELD_DEFINITION" => Some(DirectiveLocationKind::FieldDefinition),
515            "ARGUMENT_DEFINITION" => Some(DirectiveLocationKind::ArgumentDefinition),
516            "INTERFACE" => Some(DirectiveLocationKind::Interface),
517            "UNION" => Some(DirectiveLocationKind::Union),
518            "ENUM" => Some(DirectiveLocationKind::Enum),
519            "ENUM_VALUE" => Some(DirectiveLocationKind::EnumValue),
520            "INPUT_OBJECT" => Some(DirectiveLocationKind::InputObject),
521            "INPUT_FIELD_DEFINITION" => Some(DirectiveLocationKind::InputFieldDefinition),
522            // Executable Directive Locations
523            "QUERY" => Some(DirectiveLocationKind::Query),
524            "MUTATION" => Some(DirectiveLocationKind::Mutation),
525            "SUBSCRIPTION" => Some(DirectiveLocationKind::Subscription),
526            "FIELD" => Some(DirectiveLocationKind::Field),
527            "FRAGMENT_DEFINITION" => Some(DirectiveLocationKind::FragmentDefinition),
528            "FRAGMENT_SPREAD" => Some(DirectiveLocationKind::FragmentSpread),
529            "INLINE_FRAGMENT" => Some(DirectiveLocationKind::InlineFragment),
530            "VARIABLE_DEFINITION" => Some(DirectiveLocationKind::VariableDefinition),
531            _ => {
532                warn!("Unknown directive location: {}", location);
533                None
534            },
535        }
536    }
537
538    /// Validate compiled schema
539    ///
540    /// Checks:
541    /// - All type references exist
542    /// - No circular references
543    /// - Queries have valid return types
544    /// - Mutations have valid return types
545    /// - Interface implementations are valid
546    fn validate(schema: &CompiledSchema) -> Result<()> {
547        info!("Validating compiled schema");
548
549        // Build type registry
550        let mut type_names = HashSet::new();
551        for type_def in &schema.types {
552            type_names.insert(type_def.name.clone());
553        }
554
555        // Build interface registry
556        let mut interface_names = HashSet::new();
557        for interface_def in &schema.interfaces {
558            interface_names.insert(interface_def.name.clone());
559        }
560
561        // Add built-in scalars
562        type_names.insert("Int".to_string());
563        type_names.insert("Float".to_string());
564        type_names.insert("String".to_string());
565        type_names.insert("Boolean".to_string());
566        type_names.insert("ID".to_string());
567
568        // Validate queries
569        for query in &schema.queries {
570            if !type_names.contains(&query.return_type) {
571                warn!("Query '{}' references unknown type: {}", query.name, query.return_type);
572                anyhow::bail!(
573                    "Query '{}' references unknown type '{}'",
574                    query.name,
575                    query.return_type
576                );
577            }
578
579            // Validate argument types
580            for arg in &query.arguments {
581                let type_name = Self::extract_type_name(&arg.arg_type);
582                if !type_names.contains(&type_name) {
583                    anyhow::bail!(
584                        "Query '{}' argument '{}' references unknown type '{}'",
585                        query.name,
586                        arg.name,
587                        type_name
588                    );
589                }
590            }
591        }
592
593        // Validate mutations
594        for mutation in &schema.mutations {
595            if !type_names.contains(&mutation.return_type) {
596                anyhow::bail!(
597                    "Mutation '{}' references unknown type '{}'",
598                    mutation.name,
599                    mutation.return_type
600                );
601            }
602
603            // Validate argument types
604            for arg in &mutation.arguments {
605                let type_name = Self::extract_type_name(&arg.arg_type);
606                if !type_names.contains(&type_name) {
607                    anyhow::bail!(
608                        "Mutation '{}' argument '{}' references unknown type '{}'",
609                        mutation.name,
610                        arg.name,
611                        type_name
612                    );
613                }
614            }
615        }
616
617        // Validate interface implementations
618        for type_def in &schema.types {
619            for interface_name in &type_def.implements {
620                if !interface_names.contains(interface_name) {
621                    anyhow::bail!(
622                        "Type '{}' implements unknown interface '{}'",
623                        type_def.name,
624                        interface_name
625                    );
626                }
627
628                // Validate that the type has all fields required by the interface
629                if let Some(interface) = schema.find_interface(interface_name) {
630                    for interface_field in &interface.fields {
631                        let type_has_field = type_def.fields.iter().any(|f| {
632                            f.name == interface_field.name
633                                && f.field_type == interface_field.field_type
634                        });
635                        if !type_has_field {
636                            anyhow::bail!(
637                                "Type '{}' implements interface '{}' but is missing field '{}'",
638                                type_def.name,
639                                interface_name,
640                                interface_field.name
641                            );
642                        }
643                    }
644                }
645            }
646        }
647
648        info!("Schema validation passed");
649        Ok(())
650    }
651
652    /// Extract type name from `FieldType` for validation
653    ///
654    /// Built-in types return their scalar name, Object types return the object name
655    fn extract_type_name(field_type: &FieldType) -> String {
656        match field_type {
657            FieldType::String => "String".to_string(),
658            FieldType::Int => "Int".to_string(),
659            FieldType::Float => "Float".to_string(),
660            FieldType::Boolean => "Boolean".to_string(),
661            FieldType::Id => "ID".to_string(),
662            FieldType::DateTime => "DateTime".to_string(),
663            FieldType::Date => "Date".to_string(),
664            FieldType::Time => "Time".to_string(),
665            FieldType::Json => "Json".to_string(),
666            FieldType::Uuid => "UUID".to_string(),
667            FieldType::Decimal => "Decimal".to_string(),
668            FieldType::Vector => "Vector".to_string(),
669            FieldType::Scalar(name) => name.clone(),
670            FieldType::Object(name) => name.clone(),
671            FieldType::Enum(name) => name.clone(),
672            FieldType::Input(name) => name.clone(),
673            FieldType::Interface(name) => name.clone(),
674            FieldType::Union(name) => name.clone(),
675            FieldType::List(inner) => Self::extract_type_name(inner),
676        }
677    }
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683
684    #[test]
685    fn test_convert_minimal_schema() {
686        let intermediate = IntermediateSchema {
687            security:          None,
688            version:           "2.0.0".to_string(),
689            types:             vec![],
690            enums:             vec![],
691            input_types:       vec![],
692            interfaces:        vec![],
693            unions:            vec![],
694            queries:           vec![],
695            mutations:         vec![],
696            subscriptions:     vec![],
697            fragments:         None,
698            directives:        None,
699            fact_tables:       None,
700            aggregate_queries: None,
701            observers:         None,
702            custom_scalars: None,
703        };
704
705        let compiled = SchemaConverter::convert(intermediate).unwrap();
706        assert_eq!(compiled.types.len(), 0);
707        assert_eq!(compiled.queries.len(), 0);
708        assert_eq!(compiled.mutations.len(), 0);
709    }
710
711    #[test]
712    fn test_convert_type_with_fields() {
713        let intermediate = IntermediateSchema {
714            security:          None,
715            version:           "2.0.0".to_string(),
716            types:             vec![IntermediateType {
717                name:        "User".to_string(),
718                fields:      vec![
719                    IntermediateField {
720                        name:           "id".to_string(),
721                        field_type:     "Int".to_string(),
722                        nullable:       false,
723                        description:    None,
724                        directives:     None,
725                        requires_scope: None,
726                    },
727                    IntermediateField {
728                        name:           "name".to_string(),
729                        field_type:     "String".to_string(),
730                        nullable:       false,
731                        description:    None,
732                        directives:     None,
733                        requires_scope: None,
734                    },
735                ],
736                description: Some("User type".to_string()),
737                implements:  vec![],
738            }],
739            enums:             vec![],
740            input_types:       vec![],
741            interfaces:        vec![],
742            unions:            vec![],
743            queries:           vec![],
744            mutations:         vec![],
745            subscriptions:     vec![],
746            fragments:         None,
747            directives:        None,
748            fact_tables:       None,
749            aggregate_queries: None,
750            observers:         None,
751            custom_scalars: None,
752        };
753
754        let compiled = SchemaConverter::convert(intermediate).unwrap();
755        assert_eq!(compiled.types.len(), 1);
756        assert_eq!(compiled.types[0].name, "User");
757        assert_eq!(compiled.types[0].fields.len(), 2);
758        assert_eq!(compiled.types[0].fields[0].field_type, FieldType::Int);
759        assert_eq!(compiled.types[0].fields[1].field_type, FieldType::String);
760    }
761
762    #[test]
763    fn test_validate_unknown_type_reference() {
764        let intermediate = IntermediateSchema {
765            security:          None,
766            version:           "2.0.0".to_string(),
767            types:             vec![],
768            enums:             vec![],
769            input_types:       vec![],
770            interfaces:        vec![],
771            unions:            vec![],
772            queries:           vec![IntermediateQuery {
773                name:         "users".to_string(),
774                return_type:  "UnknownType".to_string(),
775                returns_list: true,
776                nullable:     false,
777                arguments:    vec![],
778                description:  None,
779                sql_source:   Some("v_user".to_string()),
780                auto_params:  None,
781                deprecated:   None,
782                jsonb_column: None,
783            }],
784            mutations:         vec![],
785            subscriptions:     vec![],
786            fragments:         None,
787            directives:        None,
788            fact_tables:       None,
789            aggregate_queries: None,
790            observers:         None,
791            custom_scalars: None,
792        };
793
794        let result = SchemaConverter::convert(intermediate);
795        assert!(result.is_err());
796        assert!(result.unwrap_err().to_string().contains("unknown type 'UnknownType'"));
797    }
798
799    #[test]
800    fn test_convert_query_with_arguments() {
801        let intermediate = IntermediateSchema {
802            security:          None,
803            version:           "2.0.0".to_string(),
804            types:             vec![IntermediateType {
805                name:        "User".to_string(),
806                fields:      vec![],
807                description: None,
808                implements:  vec![],
809            }],
810            enums:             vec![],
811            input_types:       vec![],
812            interfaces:        vec![],
813            unions:            vec![],
814            queries:           vec![IntermediateQuery {
815                name:         "users".to_string(),
816                return_type:  "User".to_string(),
817                returns_list: true,
818                nullable:     false,
819                arguments:    vec![IntermediateArgument {
820                    name:       "limit".to_string(),
821                    arg_type:   "Int".to_string(),
822                    nullable:   false,
823                    default:    Some(serde_json::json!(10)),
824                    deprecated: None,
825                }],
826                description:  Some("Get users".to_string()),
827                sql_source:   Some("v_user".to_string()),
828                auto_params:  Some(IntermediateAutoParams {
829                    limit:        true,
830                    offset:       true,
831                    where_clause: false,
832                    order_by:     false,
833                }),
834                deprecated:   None,
835                jsonb_column: None,
836            }],
837            mutations:         vec![],
838            subscriptions:     vec![],
839            fragments:         None,
840            directives:        None,
841            fact_tables:       None,
842            aggregate_queries: None,
843            observers:         None,
844            custom_scalars: None,
845        };
846
847        let compiled = SchemaConverter::convert(intermediate).unwrap();
848        assert_eq!(compiled.queries.len(), 1);
849        assert_eq!(compiled.queries[0].arguments.len(), 1);
850        assert_eq!(compiled.queries[0].arguments[0].arg_type, FieldType::Int);
851        assert!(compiled.queries[0].auto_params.has_limit);
852    }
853
854    #[test]
855    fn test_convert_field_with_deprecated_directive() {
856        use crate::schema::intermediate::IntermediateAppliedDirective;
857
858        let intermediate = IntermediateSchema {
859            security:          None,
860            version:           "2.0.0".to_string(),
861            types:             vec![IntermediateType {
862                name:        "User".to_string(),
863                fields:      vec![
864                    IntermediateField {
865                        name:           "oldId".to_string(),
866                        field_type:     "Int".to_string(),
867                        nullable:       false,
868                        description:    None,
869                        directives:     Some(vec![IntermediateAppliedDirective {
870                            name:      "deprecated".to_string(),
871                            arguments: Some(serde_json::json!({"reason": "Use 'id' instead"})),
872                        }]),
873                        requires_scope: None,
874                    },
875                    IntermediateField {
876                        name:           "id".to_string(),
877                        field_type:     "Int".to_string(),
878                        nullable:       false,
879                        description:    None,
880                        directives:     None,
881                        requires_scope: None,
882                    },
883                ],
884                description: None,
885                implements:  vec![],
886            }],
887            enums:             vec![],
888            input_types:       vec![],
889            interfaces:        vec![],
890            unions:            vec![],
891            queries:           vec![],
892            mutations:         vec![],
893            subscriptions:     vec![],
894            fragments:         None,
895            directives:        None,
896            fact_tables:       None,
897            aggregate_queries: None,
898            observers:         None,
899            custom_scalars: None,
900        };
901
902        let compiled = SchemaConverter::convert(intermediate).unwrap();
903        assert_eq!(compiled.types.len(), 1);
904        assert_eq!(compiled.types[0].fields.len(), 2);
905
906        // Check deprecated field
907        let old_id_field = &compiled.types[0].fields[0];
908        assert_eq!(old_id_field.name, "oldId");
909        assert!(old_id_field.is_deprecated());
910        assert_eq!(old_id_field.deprecation_reason(), Some("Use 'id' instead"));
911
912        // Check non-deprecated field
913        let id_field = &compiled.types[0].fields[1];
914        assert_eq!(id_field.name, "id");
915        assert!(!id_field.is_deprecated());
916        assert_eq!(id_field.deprecation_reason(), None);
917    }
918
919    #[test]
920    fn test_convert_enum() {
921        use crate::schema::intermediate::{
922            IntermediateDeprecation, IntermediateEnum, IntermediateEnumValue,
923        };
924
925        let intermediate = IntermediateSchema {
926            security:          None,
927            version:           "2.0.0".to_string(),
928            types:             vec![],
929            enums:             vec![IntermediateEnum {
930                name:        "OrderStatus".to_string(),
931                values:      vec![
932                    IntermediateEnumValue {
933                        name:        "PENDING".to_string(),
934                        description: None,
935                        deprecated:  None,
936                    },
937                    IntermediateEnumValue {
938                        name:        "PROCESSING".to_string(),
939                        description: Some("Currently being processed".to_string()),
940                        deprecated:  None,
941                    },
942                    IntermediateEnumValue {
943                        name:        "CANCELLED".to_string(),
944                        description: None,
945                        deprecated:  Some(IntermediateDeprecation {
946                            reason: Some("Use VOIDED instead".to_string()),
947                        }),
948                    },
949                ],
950                description: Some("Order status enum".to_string()),
951            }],
952            input_types:       vec![],
953            interfaces:        vec![],
954            unions:            vec![],
955            queries:           vec![],
956            mutations:         vec![],
957            subscriptions:     vec![],
958            fragments:         None,
959            directives:        None,
960            fact_tables:       None,
961            aggregate_queries: None,
962            observers:         None,
963            custom_scalars: None,
964        };
965
966        let compiled = SchemaConverter::convert(intermediate).unwrap();
967        assert_eq!(compiled.enums.len(), 1);
968
969        let status_enum = &compiled.enums[0];
970        assert_eq!(status_enum.name, "OrderStatus");
971        assert_eq!(status_enum.description, Some("Order status enum".to_string()));
972        assert_eq!(status_enum.values.len(), 3);
973
974        // Check PENDING value
975        assert_eq!(status_enum.values[0].name, "PENDING");
976        assert!(!status_enum.values[0].is_deprecated());
977
978        // Check PROCESSING value with description
979        assert_eq!(status_enum.values[1].name, "PROCESSING");
980        assert_eq!(
981            status_enum.values[1].description,
982            Some("Currently being processed".to_string())
983        );
984
985        // Check CANCELLED deprecated value
986        assert_eq!(status_enum.values[2].name, "CANCELLED");
987        assert!(status_enum.values[2].is_deprecated());
988    }
989
990    #[test]
991    fn test_convert_input_object() {
992        use crate::schema::intermediate::{
993            IntermediateDeprecation, IntermediateInputField, IntermediateInputObject,
994        };
995
996        let intermediate = IntermediateSchema {
997            security:          None,
998            version:           "2.0.0".to_string(),
999            types:             vec![],
1000            enums:             vec![],
1001            input_types:       vec![IntermediateInputObject {
1002                name:        "UserFilter".to_string(),
1003                fields:      vec![
1004                    IntermediateInputField {
1005                        name:        "name".to_string(),
1006                        field_type:  "String".to_string(),
1007                        nullable:    true,
1008                        description: None,
1009                        default:     None,
1010                        deprecated:  None,
1011                    },
1012                    IntermediateInputField {
1013                        name:        "active".to_string(),
1014                        field_type:  "Boolean".to_string(),
1015                        nullable:    true,
1016                        description: Some("Filter by active status".to_string()),
1017                        default:     Some(serde_json::json!(true)),
1018                        deprecated:  None,
1019                    },
1020                    IntermediateInputField {
1021                        name:        "oldField".to_string(),
1022                        field_type:  "String".to_string(),
1023                        nullable:    true,
1024                        description: None,
1025                        default:     None,
1026                        deprecated:  Some(IntermediateDeprecation {
1027                            reason: Some("Use newField instead".to_string()),
1028                        }),
1029                    },
1030                ],
1031                description: Some("User filter input".to_string()),
1032            }],
1033            interfaces:        vec![],
1034            unions:            vec![],
1035            queries:           vec![],
1036            mutations:         vec![],
1037            subscriptions:     vec![],
1038            fragments:         None,
1039            directives:        None,
1040            fact_tables:       None,
1041            aggregate_queries: None,
1042            observers:         None,
1043            custom_scalars: None,
1044        };
1045
1046        let compiled = SchemaConverter::convert(intermediate).unwrap();
1047        // 1 user-defined input type + 49 rich type WhereInput types
1048        assert_eq!(compiled.input_types.len(), 50);
1049
1050        // Find the UserFilter type (rich types are added at the end)
1051        let filter = compiled.input_types.iter().find(|t| t.name == "UserFilter").unwrap();
1052        assert_eq!(filter.name, "UserFilter");
1053        assert_eq!(filter.description, Some("User filter input".to_string()));
1054        assert_eq!(filter.fields.len(), 3);
1055
1056        // Check name field
1057        let name_field = filter.find_field("name").unwrap();
1058        assert_eq!(name_field.field_type, "String");
1059        assert!(!name_field.is_deprecated());
1060
1061        // Check active field with default value
1062        let active_field = filter.find_field("active").unwrap();
1063        assert_eq!(active_field.field_type, "Boolean");
1064        assert_eq!(active_field.default_value, Some("true".to_string()));
1065        assert_eq!(active_field.description, Some("Filter by active status".to_string()));
1066
1067        // Check deprecated field
1068        let old_field = filter.find_field("oldField").unwrap();
1069        assert!(old_field.is_deprecated());
1070    }
1071
1072    #[test]
1073    fn test_rich_filter_types_generated() {
1074        let intermediate = IntermediateSchema {
1075            security:          None,
1076            version:           "2.0.0".to_string(),
1077            types:             vec![],
1078            enums:             vec![],
1079            input_types:       vec![],
1080            interfaces:        vec![],
1081            unions:            vec![],
1082            queries:           vec![],
1083            mutations:         vec![],
1084            subscriptions:     vec![],
1085            fragments:         None,
1086            directives:        None,
1087            fact_tables:       None,
1088            aggregate_queries: None,
1089            observers:         None,
1090            custom_scalars: None,
1091        };
1092
1093        let compiled = SchemaConverter::convert(intermediate).unwrap();
1094
1095        // Should have 49 rich type WhereInput types
1096        assert_eq!(compiled.input_types.len(), 49);
1097
1098        // Check that EmailAddressWhereInput exists
1099        let email_where = compiled
1100            .input_types
1101            .iter()
1102            .find(|t| t.name == "EmailAddressWhereInput")
1103            .expect("EmailAddressWhereInput should be generated");
1104
1105        // Should have standard operators (eq, neq, in, nin, contains, isnull) + rich operators
1106        assert!(email_where.fields.len() > 6);
1107        assert!(email_where.fields.iter().any(|f| f.name == "eq"));
1108        assert!(email_where.fields.iter().any(|f| f.name == "neq"));
1109        assert!(email_where.fields.iter().any(|f| f.name == "contains"));
1110        assert!(email_where.fields.iter().any(|f| f.name == "isnull"));
1111
1112        // Check that VINWhereInput exists
1113        let vin_where = compiled
1114            .input_types
1115            .iter()
1116            .find(|t| t.name == "VINWhereInput")
1117            .expect("VINWhereInput should be generated");
1118
1119        assert!(vin_where.fields.len() > 6);
1120        assert!(vin_where.fields.iter().any(|f| f.name == "eq"));
1121    }
1122
1123    #[test]
1124    fn test_rich_filter_types_have_sql_templates() {
1125        let intermediate = IntermediateSchema {
1126            security:          None,
1127            version:           "2.0.0".to_string(),
1128            types:             vec![],
1129            enums:             vec![],
1130            input_types:       vec![],
1131            interfaces:        vec![],
1132            unions:            vec![],
1133            queries:           vec![],
1134            mutations:         vec![],
1135            subscriptions:     vec![],
1136            fragments:         None,
1137            directives:        None,
1138            fact_tables:       None,
1139            aggregate_queries: None,
1140            observers:         None,
1141            custom_scalars: None,
1142        };
1143
1144        let compiled = SchemaConverter::convert(intermediate).unwrap();
1145
1146        // Check that EmailAddressWhereInput has SQL template metadata
1147        let email_where = compiled
1148            .input_types
1149            .iter()
1150            .find(|t| t.name == "EmailAddressWhereInput")
1151            .expect("EmailAddressWhereInput should be generated");
1152
1153        // Verify metadata exists and contains operators
1154        assert!(
1155            email_where.metadata.is_some(),
1156            "Metadata should exist for EmailAddressWhereInput"
1157        );
1158        let metadata = email_where.metadata.as_ref().unwrap();
1159        assert!(
1160            metadata.get("operators").is_some(),
1161            "Operators should be in metadata: {metadata:?}"
1162        );
1163
1164        let operators = metadata["operators"].as_object().unwrap();
1165        // Should have templates for email-specific operators
1166        assert!(!operators.is_empty(), "Operators map should not be empty: {operators:?}");
1167        assert!(
1168            operators.contains_key("domainEq"),
1169            "Missing domainEq in operators: {:?}",
1170            operators.keys().collect::<Vec<_>>()
1171        );
1172
1173        // Verify domainEq has templates for all 4 databases
1174        let email_domain_eq = operators["domainEq"].as_object().unwrap();
1175        assert!(email_domain_eq.contains_key("postgres"));
1176        assert!(email_domain_eq.contains_key("mysql"));
1177        assert!(email_domain_eq.contains_key("sqlite"));
1178        assert!(email_domain_eq.contains_key("sqlserver"));
1179
1180        // Verify PostgreSQL template is correct
1181        let postgres_template = email_domain_eq["postgres"].as_str().unwrap();
1182        assert!(postgres_template.contains("SPLIT_PART"));
1183        assert!(postgres_template.contains("$field"));
1184    }
1185
1186    #[test]
1187    fn test_lookup_data_embedded_in_schema() {
1188        let intermediate = IntermediateSchema {
1189            security:          None,
1190            version:           "2.0.0".to_string(),
1191            types:             vec![],
1192            enums:             vec![],
1193            input_types:       vec![],
1194            interfaces:        vec![],
1195            unions:            vec![],
1196            queries:           vec![],
1197            mutations:         vec![],
1198            subscriptions:     vec![],
1199            fragments:         None,
1200            directives:        None,
1201            fact_tables:       None,
1202            aggregate_queries: None,
1203            observers:         None,
1204            custom_scalars: None,
1205        };
1206
1207        let compiled = SchemaConverter::convert(intermediate).unwrap();
1208
1209        // Verify lookup data is embedded in schema.security
1210        assert!(compiled.security.is_some(), "Security section should exist");
1211        let security = compiled.security.as_ref().unwrap();
1212        assert!(
1213            security.get("lookup_data").is_some(),
1214            "Lookup data should be in security section"
1215        );
1216
1217        let lookup_data = security["lookup_data"].as_object().unwrap();
1218
1219        // Verify all lookup tables are present
1220        assert!(lookup_data.contains_key("countries"), "Countries lookup should be present");
1221        assert!(lookup_data.contains_key("currencies"), "Currencies lookup should be present");
1222        assert!(lookup_data.contains_key("timezones"), "Timezones lookup should be present");
1223        assert!(lookup_data.contains_key("languages"), "Languages lookup should be present");
1224
1225        // Verify countries data
1226        let countries = lookup_data["countries"].as_object().unwrap();
1227        assert!(countries.contains_key("US"), "US should be in countries");
1228        assert!(countries.contains_key("FR"), "France should be in countries");
1229        assert!(countries.contains_key("GB"), "UK should be in countries");
1230
1231        // Verify US data
1232        let us = countries["US"].as_object().unwrap();
1233        assert_eq!(us["continent"].as_str().unwrap(), "North America");
1234        assert!(!us["in_eu"].as_bool().unwrap());
1235
1236        // Verify France is EU and Schengen
1237        let fr = countries["FR"].as_object().unwrap();
1238        assert!(fr["in_eu"].as_bool().unwrap());
1239        assert!(fr["in_schengen"].as_bool().unwrap());
1240
1241        // Verify currencies data
1242        let currencies = lookup_data["currencies"].as_object().unwrap();
1243        assert!(currencies.contains_key("USD"));
1244        assert!(currencies.contains_key("EUR"));
1245        let usd = currencies["USD"].as_object().unwrap();
1246        assert_eq!(usd["symbol"].as_str().unwrap(), "$");
1247        assert_eq!(usd["decimal_places"].as_i64().unwrap(), 2);
1248
1249        // Verify timezones data
1250        let timezones = lookup_data["timezones"].as_object().unwrap();
1251        assert!(timezones.contains_key("UTC"));
1252        assert!(timezones.contains_key("EST"));
1253        let est = timezones["EST"].as_object().unwrap();
1254        assert_eq!(est["offset_minutes"].as_i64().unwrap(), -300);
1255        assert!(est["has_dst"].as_bool().unwrap());
1256    }
1257
1258    #[test]
1259    fn test_convert_interface() {
1260        use crate::schema::intermediate::{IntermediateField, IntermediateInterface};
1261
1262        let intermediate = IntermediateSchema {
1263            security:          None,
1264            version:           "2.0.0".to_string(),
1265            types:             vec![],
1266            enums:             vec![],
1267            input_types:       vec![],
1268            interfaces:        vec![IntermediateInterface {
1269                name:        "Node".to_string(),
1270                fields:      vec![IntermediateField {
1271                    name:           "id".to_string(),
1272                    field_type:     "ID".to_string(),
1273                    nullable:       false,
1274                    description:    None,
1275                    directives:     None,
1276                    requires_scope: None,
1277                }],
1278                description: Some("An object with a globally unique ID".to_string()),
1279            }],
1280            unions:            vec![],
1281            queries:           vec![],
1282            mutations:         vec![],
1283            subscriptions:     vec![],
1284            fragments:         None,
1285            directives:        None,
1286            fact_tables:       None,
1287            aggregate_queries: None,
1288            observers:         None,
1289            custom_scalars: None,
1290        };
1291
1292        let compiled = SchemaConverter::convert(intermediate).unwrap();
1293        assert_eq!(compiled.interfaces.len(), 1);
1294
1295        let interface = &compiled.interfaces[0];
1296        assert_eq!(interface.name, "Node");
1297        assert_eq!(interface.description, Some("An object with a globally unique ID".to_string()));
1298        assert_eq!(interface.fields.len(), 1);
1299        assert_eq!(interface.fields[0].name, "id");
1300        assert_eq!(interface.fields[0].field_type, FieldType::Id);
1301    }
1302
1303    #[test]
1304    fn test_convert_type_implements_interface() {
1305        use crate::schema::intermediate::{
1306            IntermediateField, IntermediateInterface, IntermediateType,
1307        };
1308
1309        let intermediate = IntermediateSchema {
1310            security:          None,
1311            version:           "2.0.0".to_string(),
1312            types:             vec![IntermediateType {
1313                name:        "User".to_string(),
1314                fields:      vec![
1315                    IntermediateField {
1316                        name:           "id".to_string(),
1317                        field_type:     "ID".to_string(),
1318                        nullable:       false,
1319                        description:    None,
1320                        directives:     None,
1321                        requires_scope: None,
1322                    },
1323                    IntermediateField {
1324                        name:           "name".to_string(),
1325                        field_type:     "String".to_string(),
1326                        nullable:       false,
1327                        description:    None,
1328                        directives:     None,
1329                        requires_scope: None,
1330                    },
1331                ],
1332                description: None,
1333                implements:  vec!["Node".to_string()],
1334            }],
1335            enums:             vec![],
1336            input_types:       vec![],
1337            interfaces:        vec![IntermediateInterface {
1338                name:        "Node".to_string(),
1339                fields:      vec![IntermediateField {
1340                    name:           "id".to_string(),
1341                    field_type:     "ID".to_string(),
1342                    nullable:       false,
1343                    description:    None,
1344                    directives:     None,
1345                    requires_scope: None,
1346                }],
1347                description: None,
1348            }],
1349            unions:            vec![],
1350            queries:           vec![],
1351            mutations:         vec![],
1352            subscriptions:     vec![],
1353            fragments:         None,
1354            directives:        None,
1355            fact_tables:       None,
1356            aggregate_queries: None,
1357            observers:         None,
1358            custom_scalars: None,
1359        };
1360
1361        let compiled = SchemaConverter::convert(intermediate).unwrap();
1362
1363        // Check type implements interface
1364        assert_eq!(compiled.types.len(), 1);
1365        assert_eq!(compiled.types[0].implements, vec!["Node"]);
1366
1367        // Check interface exists
1368        assert_eq!(compiled.interfaces.len(), 1);
1369        assert_eq!(compiled.interfaces[0].name, "Node");
1370    }
1371
1372    #[test]
1373    fn test_validate_unknown_interface() {
1374        use crate::schema::intermediate::{IntermediateField, IntermediateType};
1375
1376        let intermediate = IntermediateSchema {
1377            security:          None,
1378            version:           "2.0.0".to_string(),
1379            types:             vec![IntermediateType {
1380                name:        "User".to_string(),
1381                fields:      vec![IntermediateField {
1382                    name:           "id".to_string(),
1383                    field_type:     "ID".to_string(),
1384                    nullable:       false,
1385                    description:    None,
1386                    directives:     None,
1387                    requires_scope: None,
1388                }],
1389                description: None,
1390                implements:  vec!["UnknownInterface".to_string()],
1391            }],
1392            enums:             vec![],
1393            input_types:       vec![],
1394            interfaces:        vec![], // No interface defined!
1395            unions:            vec![],
1396            queries:           vec![],
1397            mutations:         vec![],
1398            subscriptions:     vec![],
1399            fragments:         None,
1400            directives:        None,
1401            fact_tables:       None,
1402            aggregate_queries: None,
1403            observers:         None,
1404            custom_scalars: None,
1405        };
1406
1407        let result = SchemaConverter::convert(intermediate);
1408        assert!(result.is_err());
1409        assert!(result.unwrap_err().to_string().contains("unknown interface"));
1410    }
1411
1412    #[test]
1413    fn test_validate_missing_interface_field() {
1414        use crate::schema::intermediate::{
1415            IntermediateField, IntermediateInterface, IntermediateType,
1416        };
1417
1418        let intermediate = IntermediateSchema {
1419            security:          None,
1420            version:           "2.0.0".to_string(),
1421            types:             vec![IntermediateType {
1422                name:        "User".to_string(),
1423                fields:      vec![
1424                    // Missing the required 'id' field from Node interface!
1425                    IntermediateField {
1426                        name:           "name".to_string(),
1427                        field_type:     "String".to_string(),
1428                        nullable:       false,
1429                        description:    None,
1430                        directives:     None,
1431                        requires_scope: None,
1432                    },
1433                ],
1434                description: None,
1435                implements:  vec!["Node".to_string()],
1436            }],
1437            enums:             vec![],
1438            input_types:       vec![],
1439            interfaces:        vec![IntermediateInterface {
1440                name:        "Node".to_string(),
1441                fields:      vec![IntermediateField {
1442                    name:           "id".to_string(),
1443                    field_type:     "ID".to_string(),
1444                    nullable:       false,
1445                    description:    None,
1446                    directives:     None,
1447                    requires_scope: None,
1448                }],
1449                description: None,
1450            }],
1451            unions:            vec![],
1452            queries:           vec![],
1453            mutations:         vec![],
1454            subscriptions:     vec![],
1455            fragments:         None,
1456            directives:        None,
1457            fact_tables:       None,
1458            aggregate_queries: None,
1459            observers:         None,
1460            custom_scalars: None,
1461        };
1462
1463        let result = SchemaConverter::convert(intermediate);
1464        assert!(result.is_err());
1465        assert!(result.unwrap_err().to_string().contains("missing field 'id'"));
1466    }
1467
1468    #[test]
1469    fn test_convert_union() {
1470        use crate::schema::intermediate::{IntermediateField, IntermediateType, IntermediateUnion};
1471
1472        let intermediate = IntermediateSchema {
1473            security:          None,
1474            version:           "2.0.0".to_string(),
1475            types:             vec![
1476                IntermediateType {
1477                    name:        "User".to_string(),
1478                    fields:      vec![IntermediateField {
1479                        name:           "id".to_string(),
1480                        field_type:     "ID".to_string(),
1481                        nullable:       false,
1482                        description:    None,
1483                        directives:     None,
1484                        requires_scope: None,
1485                    }],
1486                    description: None,
1487                    implements:  vec![],
1488                },
1489                IntermediateType {
1490                    name:        "Post".to_string(),
1491                    fields:      vec![IntermediateField {
1492                        name:           "id".to_string(),
1493                        field_type:     "ID".to_string(),
1494                        nullable:       false,
1495                        description:    None,
1496                        directives:     None,
1497                        requires_scope: None,
1498                    }],
1499                    description: None,
1500                    implements:  vec![],
1501                },
1502            ],
1503            enums:             vec![],
1504            input_types:       vec![],
1505            interfaces:        vec![],
1506            unions:            vec![IntermediateUnion {
1507                name:         "SearchResult".to_string(),
1508                member_types: vec!["User".to_string(), "Post".to_string()],
1509                description:  Some("Result from a search query".to_string()),
1510            }],
1511            queries:           vec![],
1512            mutations:         vec![],
1513            subscriptions:     vec![],
1514            fragments:         None,
1515            directives:        None,
1516            fact_tables:       None,
1517            aggregate_queries: None,
1518            observers:         None,
1519            custom_scalars: None,
1520        };
1521
1522        let compiled = SchemaConverter::convert(intermediate).unwrap();
1523
1524        // Check union exists
1525        assert_eq!(compiled.unions.len(), 1);
1526        let union_def = &compiled.unions[0];
1527        assert_eq!(union_def.name, "SearchResult");
1528        assert_eq!(union_def.member_types, vec!["User", "Post"]);
1529        assert_eq!(union_def.description, Some("Result from a search query".to_string()));
1530    }
1531
1532    #[test]
1533    fn test_convert_field_requires_scope() {
1534        use crate::schema::intermediate::{IntermediateField, IntermediateType};
1535
1536        let intermediate = IntermediateSchema {
1537            security:          None,
1538            version:           "2.0.0".to_string(),
1539            types:             vec![IntermediateType {
1540                name:        "Employee".to_string(),
1541                fields:      vec![
1542                    IntermediateField {
1543                        name:           "id".to_string(),
1544                        field_type:     "ID".to_string(),
1545                        nullable:       false,
1546                        description:    None,
1547                        directives:     None,
1548                        requires_scope: None,
1549                    },
1550                    IntermediateField {
1551                        name:           "name".to_string(),
1552                        field_type:     "String".to_string(),
1553                        nullable:       false,
1554                        description:    None,
1555                        directives:     None,
1556                        requires_scope: None,
1557                    },
1558                    IntermediateField {
1559                        name:           "salary".to_string(),
1560                        field_type:     "Float".to_string(),
1561                        nullable:       false,
1562                        description:    Some("Employee salary - protected field".to_string()),
1563                        directives:     None,
1564                        requires_scope: Some("read:Employee.salary".to_string()),
1565                    },
1566                    IntermediateField {
1567                        name:           "ssn".to_string(),
1568                        field_type:     "String".to_string(),
1569                        nullable:       true,
1570                        description:    Some(
1571                            "Social Security Number - highly protected".to_string(),
1572                        ),
1573                        directives:     None,
1574                        requires_scope: Some("admin".to_string()),
1575                    },
1576                ],
1577                description: None,
1578                implements:  vec![],
1579            }],
1580            enums:             vec![],
1581            input_types:       vec![],
1582            interfaces:        vec![],
1583            unions:            vec![],
1584            queries:           vec![],
1585            mutations:         vec![],
1586            subscriptions:     vec![],
1587            fragments:         None,
1588            directives:        None,
1589            fact_tables:       None,
1590            aggregate_queries: None,
1591            observers:         None,
1592            custom_scalars: None,
1593        };
1594
1595        let compiled = SchemaConverter::convert(intermediate).unwrap();
1596
1597        assert_eq!(compiled.types.len(), 1);
1598        let employee_type = &compiled.types[0];
1599        assert_eq!(employee_type.name, "Employee");
1600        assert_eq!(employee_type.fields.len(), 4);
1601
1602        // id field - no scope required
1603        assert_eq!(employee_type.fields[0].name, "id");
1604        assert!(employee_type.fields[0].requires_scope.is_none());
1605
1606        // name field - no scope required
1607        assert_eq!(employee_type.fields[1].name, "name");
1608        assert!(employee_type.fields[1].requires_scope.is_none());
1609
1610        // salary field - requires specific scope
1611        assert_eq!(employee_type.fields[2].name, "salary");
1612        assert_eq!(
1613            employee_type.fields[2].requires_scope,
1614            Some("read:Employee.salary".to_string())
1615        );
1616
1617        // ssn field - requires admin scope
1618        assert_eq!(employee_type.fields[3].name, "ssn");
1619        assert_eq!(employee_type.fields[3].requires_scope, Some("admin".to_string()));
1620    }
1621}