Skip to main content

fraiseql_core/compiler/
codegen.rs

1//! Code generator - produces final CompiledSchema JSON.
2//!
3//! # Overview
4//!
5//! Takes validated IR and SQL templates, produces runtime-optimized
6//! CompiledSchema ready for execution.
7
8use std::collections::HashSet;
9
10use super::{
11    ir::{
12        AuthoringIR, IRArgument, IREnum, IREnumValue, IRField, IRInputField, IRInputType,
13        IRInterface, IRMutation, IRUnion,
14    },
15    lowering::SqlTemplate,
16};
17use crate::{
18    error::Result,
19    schema::{
20        ArgumentDefinition, AutoParams as SchemaAutoParams, CompiledSchema, DeprecationInfo,
21        EnumDefinition, EnumValueDefinition, FieldDefinition, FieldType, InputFieldDefinition,
22        InputObjectDefinition, InterfaceDefinition, MutationDefinition, QueryDefinition,
23        SubscriptionDefinition, TypeDefinition, UnionDefinition,
24    },
25    validation::{CustomTypeDef, CustomTypeRegistry, CustomTypeRegistryConfig},
26};
27
28/// Code generator.
29pub struct CodeGenerator {
30    optimize: bool,
31}
32
33impl CodeGenerator {
34    /// Create new code generator.
35    #[must_use]
36    pub fn new(optimize: bool) -> Self {
37        Self { optimize }
38    }
39
40    /// Generate a compiled schema from the intermediate representation.
41    ///
42    /// # Architecture: Schema vs. Templates vs. Metadata
43    ///
44    /// FraiseQL separates schema definition from execution artifacts:
45    ///
46    /// ## What This Function Generates
47    ///
48    /// The `CompiledSchema` contains the **schema definition** - types, fields, enums,
49    /// interfaces, unions, and query/mutation/subscription signatures. This is what
50    /// GraphQL introspection tools query and what the runtime uses for query validation.
51    ///
52    /// ## What This Function Does NOT Generate
53    ///
54    /// ### 1. SQL Templates
55    ///
56    /// SQL templates are managed separately by the compilation pipeline.
57    /// They are:
58    /// - Generated in a separate compiler pass after schema validation
59    /// - Kept separate to allow schema reuse across database backends
60    /// - Passed to the runtime executor independently
61    ///
62    /// **Why**: Allows updating SQL generation strategy without recompiling schema
63    /// definitions. For example, you could optimize SQL templates without changing
64    /// type definitions.
65    ///
66    /// ### 2. Fact Tables
67    ///
68    /// Fact table metadata is populated by the compiler from `ir.fact_tables` in a
69    /// separate initialization pass. This maintains:
70    /// - Clean separation of concerns (schema def vs. analytics metadata)
71    /// - Ability to update fact table configuration independently
72    /// - Clear data flow through compilation pipeline
73    ///
74    /// **Why**: Fact tables are configuration-driven metadata. Keeping them separate
75    /// allows analytics tuning without affecting core schema.
76    ///
77    /// # Parameters
78    ///
79    /// * `ir` - The intermediate representation generated by the compiler
80    /// * `_templates` - SQL templates (currently unused; kept for API compatibility)
81    ///
82    /// # Errors
83    ///
84    /// Returns a `Result` with compilation errors if schema validation fails.
85    ///
86    /// # Examples
87    ///
88    /// ```ignore
89    /// use fraiseql_core::compiler::*;
90    ///
91    /// // Given an AuthoringIR from the compiler pipeline:
92    /// let codegen = CodeGenerator::new(true);
93    /// let compiled_schema = codegen.generate(&authoring_ir, &[])?;
94    ///
95    /// // Runtime uses compiled_schema for query execution
96    /// ```
97    ///
98    /// # See Also
99    ///
100    /// - (Compilation): SQL template generation
101    /// - Compiler module documentation for pipeline overview
102    pub fn generate(&self, ir: &AuthoringIR, _templates: &[SqlTemplate]) -> Result<CompiledSchema> {
103        // Build set of known type names for field type parsing
104        let known_types: HashSet<String> = ir.types.iter().map(|t| t.name.clone()).collect();
105
106        let types = ir
107            .types
108            .iter()
109            .map(|t| {
110                TypeDefinition {
111                    name:                t.name.clone(),
112                    sql_source:          t.sql_source.clone().unwrap_or_else(|| t.name.clone()),
113                    jsonb_column:        "data".to_string(),
114                    fields:              Self::map_fields(&t.fields, &known_types),
115                    description:         t.description.clone(),
116                    sql_projection_hint: None, // Populated during optimization pass
117                    implements:          Vec::new(), /* Note: IR doesn't have interface
118                                                * implementation yet */
119                    is_error:            false,
120                }
121            })
122            .collect();
123
124        let queries = ir
125            .queries
126            .iter()
127            .map(|q| {
128                QueryDefinition {
129                    name:         q.name.clone(),
130                    return_type:  q.return_type.clone(),
131                    returns_list: q.returns_list,
132                    nullable:     q.nullable,
133                    arguments:    Self::map_arguments(&q.arguments, &known_types),
134                    sql_source:   q.sql_source.clone(),
135                    description:  q.description.clone(),
136                    auto_params:  SchemaAutoParams {
137                        has_where:    q.auto_params.has_where,
138                        has_order_by: q.auto_params.has_order_by,
139                        has_limit:    q.auto_params.has_limit,
140                        has_offset:   q.auto_params.has_offset,
141                    },
142                    deprecation:  None, // Note: IR doesn't have deprecation info yet
143                    jsonb_column: "data".to_string(), // Default to "data" column
144                }
145            })
146            .collect();
147
148        let mutations = ir.mutations.iter().map(|m| Self::map_mutation(m, &known_types)).collect();
149
150        let subscriptions = ir
151            .subscriptions
152            .iter()
153            .map(|s| {
154                SubscriptionDefinition {
155                    name:        s.name.clone(),
156                    return_type: s.return_type.clone(),
157                    arguments:   Self::map_arguments(&s.arguments, &known_types),
158                    description: s.description.clone(),
159                    topic:       None, // Populated from decorator topic binding
160                    filter:      None, // Populated from decorator filters
161                    fields:      Vec::new(), // Populated from decorator field selection
162                    deprecation: None, // Note: IR subscriptions don't have deprecation yet
163                }
164            })
165            .collect();
166
167        // Map enums
168        let enums = ir.enums.iter().map(|e| Self::map_enum(e)).collect();
169
170        // Map interfaces
171        let interfaces =
172            ir.interfaces.iter().map(|i| Self::map_interface(i, &known_types)).collect();
173
174        // Map unions
175        let unions = ir.unions.iter().map(|u| Self::map_union(u)).collect();
176
177        // Map input types
178        let input_types =
179            ir.input_types.iter().map(|i| Self::map_input_type(i, &known_types)).collect();
180
181        // Build custom type registry from IRScalars
182        let custom_scalars = Self::build_custom_type_registry(&ir.scalars)?;
183
184        Ok(CompiledSchema {
185            types,
186            enums,
187            input_types,
188            interfaces,
189            unions,
190            queries,
191            mutations,
192            subscriptions,
193            // Directives are custom decorators applied to schema elements. The IR doesn't currently
194            // have custom directive definitions. They will be populated when decorator support is
195            // added.
196            directives: Vec::new(),
197            // Fact tables are populated by the compiler from ir.fact_tables in a separate pass.
198            // This maintains clear separation between schema definition (type system) and analytics
199            // metadata (fact table configuration). Empty initially; populated during post-codegen
200            // pass.
201            fact_tables: std::collections::HashMap::new(),
202            // Observers are populated from ir.observers. Currently empty; will be populated
203            // when observer support is added to the IR layer.
204            observers: Vec::new(),
205            // Federation metadata: populated if Apollo Federation is configured
206            federation: None,
207            // Security configuration: populated from fraiseql.toml
208            security: None,
209            // Raw schema SDL: populated for federation _service query
210            observers_config: None,
211            schema_sdl: None,
212            // Custom scalar types registry
213            custom_scalars,
214        })
215    }
216
217    /// Map IR fields to compiled schema fields.
218    fn map_fields(ir_fields: &[IRField], known_types: &HashSet<String>) -> Vec<FieldDefinition> {
219        ir_fields
220            .iter()
221            .map(|f| {
222                let field_type = FieldType::parse(&f.field_type, known_types);
223                FieldDefinition {
224                    name: f.name.clone(),
225                    field_type,
226                    nullable: f.nullable,
227                    description: f.description.clone(),
228                    default_value: None,  // Fields don't have defaults in GraphQL
229                    vector_config: None,  // Would be set if field_type is Vector
230                    alias: None,          // Aliases come from query, not schema
231                    deprecation: None,    // Note: IR fields don't have deprecation yet
232                    requires_scope: None, // Note: IR fields don't have scope requirements yet
233                    encryption: None,
234                }
235            })
236            .collect()
237    }
238
239    /// Map IR arguments to compiled schema arguments.
240    fn map_arguments(
241        ir_args: &[IRArgument],
242        known_types: &HashSet<String>,
243    ) -> Vec<ArgumentDefinition> {
244        ir_args
245            .iter()
246            .map(|a| {
247                let arg_type = FieldType::parse(&a.arg_type, known_types);
248                ArgumentDefinition {
249                    name: a.name.clone(),
250                    arg_type,
251                    nullable: a.nullable,
252                    default_value: a.default_value.clone(),
253                    description: a.description.clone(),
254                    deprecation: None, // Note: IR args don't have deprecation yet
255                }
256            })
257            .collect()
258    }
259
260    /// Map IR mutation to compiled schema mutation.
261    fn map_mutation(m: &IRMutation, known_types: &HashSet<String>) -> MutationDefinition {
262        use super::ir::MutationOperation as IRMutationOp;
263        use crate::schema::MutationOperation;
264
265        // The compiled schema MutationOperation needs a table name for Insert/Update/Delete
266        // Since IR doesn't have this, we use Custom as default or derive from return type
267        let operation = match m.operation {
268            IRMutationOp::Create => MutationOperation::Insert {
269                table: m.return_type.to_lowercase(), // Infer table from return type
270            },
271            IRMutationOp::Update => MutationOperation::Update {
272                table: m.return_type.to_lowercase(),
273            },
274            IRMutationOp::Delete => MutationOperation::Delete {
275                table: m.return_type.to_lowercase(),
276            },
277            IRMutationOp::Custom => MutationOperation::Custom,
278        };
279
280        MutationDefinition {
281            name:        m.name.clone(),
282            return_type: m.return_type.clone(),
283            arguments:   Self::map_arguments(&m.arguments, known_types),
284            description: m.description.clone(),
285            operation,
286            deprecation: None, // Note: IR mutations don't have deprecation yet
287            sql_source:  None,
288        }
289    }
290
291    /// Map IR enum to compiled schema enum.
292    fn map_enum(e: &IREnum) -> EnumDefinition {
293        EnumDefinition {
294            name:        e.name.clone(),
295            values:      e.values.iter().map(|v| Self::map_enum_value(v)).collect(),
296            description: e.description.clone(),
297        }
298    }
299
300    /// Map IR enum value to compiled schema enum value.
301    fn map_enum_value(v: &IREnumValue) -> EnumValueDefinition {
302        EnumValueDefinition {
303            name:        v.name.clone(),
304            description: v.description.clone(),
305            deprecation: v.deprecation_reason.as_ref().map(|reason| DeprecationInfo {
306                reason: Some(reason.clone()),
307            }),
308        }
309    }
310
311    /// Map IR interface to compiled schema interface.
312    fn map_interface(i: &IRInterface, known_types: &HashSet<String>) -> InterfaceDefinition {
313        InterfaceDefinition {
314            name:        i.name.clone(),
315            fields:      Self::map_fields(&i.fields, known_types),
316            description: i.description.clone(),
317        }
318    }
319
320    /// Map IR union to compiled schema union.
321    fn map_union(u: &IRUnion) -> UnionDefinition {
322        UnionDefinition {
323            name:         u.name.clone(),
324            member_types: u.types.clone(),
325            description:  u.description.clone(),
326        }
327    }
328
329    /// Map IR input type to compiled schema input object.
330    fn map_input_type(i: &IRInputType, known_types: &HashSet<String>) -> InputObjectDefinition {
331        InputObjectDefinition {
332            name:        i.name.clone(),
333            fields:      Self::map_input_fields(&i.fields, known_types),
334            description: i.description.clone(),
335            metadata:    None,
336        }
337    }
338
339    /// Map IR input fields to compiled schema input fields.
340    fn map_input_fields(
341        ir_fields: &[IRInputField],
342        _known_types: &HashSet<String>,
343    ) -> Vec<InputFieldDefinition> {
344        ir_fields
345            .iter()
346            .map(|f| {
347                InputFieldDefinition {
348                    name:             f.name.clone(),
349                    field_type:       f.field_type.clone(), /* InputFieldDefinition uses String,
350                                                             * not FieldType */
351                    description:      f.description.clone(),
352                    default_value:    f.default_value.as_ref().map(|v| v.to_string()),
353                    deprecation:      None, // Note: IR input fields don't have deprecation yet
354                    validation_rules: Vec::new(), /* Validation rules set separately from @validate
355                                             * directives */
356                }
357            })
358            .collect()
359    }
360
361    /// Build custom type registry from IRScalars.
362    fn build_custom_type_registry(
363        ir_scalars: &[super::ir::IRScalar],
364    ) -> Result<CustomTypeRegistry> {
365        let registry = CustomTypeRegistry::new(CustomTypeRegistryConfig::default());
366
367        for ir_scalar in ir_scalars {
368            let def = CustomTypeDef {
369                name:             ir_scalar.name.clone(),
370                description:      ir_scalar.description.clone(),
371                specified_by_url: ir_scalar.specified_by_url.clone(),
372                validation_rules: ir_scalar.validation_rules.clone(),
373                elo_expression:   None, // ELO expressions are handled separately
374                base_type:        ir_scalar.base_type.clone(),
375            };
376
377            registry.register(ir_scalar.name.clone(), def)?;
378        }
379
380        Ok(registry)
381    }
382
383    /// Check if optimization is enabled.
384    #[must_use]
385    pub const fn optimize(&self) -> bool {
386        self.optimize
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::{
393        super::ir::{AutoParams, IRArgument, IRField, IRQuery, IRSubscription, IRType},
394        *,
395    };
396
397    #[test]
398    fn test_code_generator_new() {
399        let generator = CodeGenerator::new(true);
400        assert!(generator.optimize());
401
402        let generator = CodeGenerator::new(false);
403        assert!(!generator.optimize());
404    }
405
406    #[test]
407    fn test_generate_empty_schema() {
408        let generator = CodeGenerator::new(true);
409        let ir = AuthoringIR::new();
410        let templates = Vec::new();
411
412        let result = generator.generate(&ir, &templates);
413        assert!(result.is_ok());
414
415        let schema = result.unwrap();
416        assert!(schema.types.is_empty());
417        assert!(schema.queries.is_empty());
418    }
419
420    #[test]
421    fn test_generate_types_with_fields() {
422        let generator = CodeGenerator::new(true);
423        let mut ir = AuthoringIR::new();
424
425        ir.types.push(IRType {
426            name:        "User".to_string(),
427            fields:      vec![
428                IRField {
429                    name:        "id".to_string(),
430                    field_type:  "ID!".to_string(),
431                    nullable:    false,
432                    description: Some("User ID".to_string()),
433                    sql_column:  Some("id".to_string()),
434                },
435                IRField {
436                    name:        "name".to_string(),
437                    field_type:  "String".to_string(),
438                    nullable:    true,
439                    description: None,
440                    sql_column:  None,
441                },
442                IRField {
443                    name:        "age".to_string(),
444                    field_type:  "Int".to_string(),
445                    nullable:    true,
446                    description: None,
447                    sql_column:  None,
448                },
449            ],
450            sql_source:  Some("v_user".to_string()),
451            description: Some("User type".to_string()),
452        });
453
454        let result = generator.generate(&ir, &[]);
455        assert!(result.is_ok());
456
457        let schema = result.unwrap();
458        assert_eq!(schema.types.len(), 1);
459
460        let user_type = &schema.types[0];
461        assert_eq!(user_type.name, "User");
462        assert_eq!(user_type.sql_source, "v_user");
463        assert_eq!(user_type.fields.len(), 3);
464
465        // Check field types were parsed correctly
466        assert_eq!(user_type.fields[0].name, "id");
467        assert_eq!(user_type.fields[0].field_type, FieldType::Id);
468        assert!(!user_type.fields[0].nullable);
469
470        assert_eq!(user_type.fields[1].name, "name");
471        assert_eq!(user_type.fields[1].field_type, FieldType::String);
472
473        assert_eq!(user_type.fields[2].name, "age");
474        assert_eq!(user_type.fields[2].field_type, FieldType::Int);
475    }
476
477    #[test]
478    fn test_generate_queries_with_arguments() {
479        let generator = CodeGenerator::new(true);
480        let mut ir = AuthoringIR::new();
481
482        ir.types.push(IRType {
483            name:        "User".to_string(),
484            fields:      vec![],
485            sql_source:  None,
486            description: None,
487        });
488
489        ir.queries.push(IRQuery {
490            name:         "user".to_string(),
491            return_type:  "User".to_string(),
492            returns_list: false,
493            nullable:     true,
494            arguments:    vec![IRArgument {
495                name:          "id".to_string(),
496                arg_type:      "ID!".to_string(),
497                nullable:      false,
498                default_value: None,
499                description:   Some("User ID to fetch".to_string()),
500            }],
501            sql_source:   Some("v_user".to_string()),
502            description:  Some("Fetch a single user".to_string()),
503            auto_params:  AutoParams::default(),
504        });
505
506        ir.queries.push(IRQuery {
507            name:         "users".to_string(),
508            return_type:  "User".to_string(),
509            returns_list: true,
510            nullable:     false,
511            arguments:    vec![IRArgument {
512                name:          "limit".to_string(),
513                arg_type:      "Int".to_string(),
514                nullable:      true,
515                default_value: Some(serde_json::json!(10)),
516                description:   None,
517            }],
518            sql_source:   Some("v_user".to_string()),
519            description:  None,
520            auto_params:  AutoParams {
521                has_where:    true,
522                has_order_by: true,
523                has_limit:    true,
524                has_offset:   true,
525            },
526        });
527
528        let result = generator.generate(&ir, &[]);
529        assert!(result.is_ok());
530
531        let schema = result.unwrap();
532        assert_eq!(schema.queries.len(), 2);
533
534        // Check single user query
535        let user_query = &schema.queries[0];
536        assert_eq!(user_query.name, "user");
537        assert!(!user_query.returns_list);
538        assert!(user_query.nullable);
539        assert_eq!(user_query.arguments.len(), 1);
540        assert_eq!(user_query.arguments[0].name, "id");
541        assert_eq!(user_query.arguments[0].arg_type, FieldType::Id);
542
543        // Check users query with auto_params
544        let users_query = &schema.queries[1];
545        assert_eq!(users_query.name, "users");
546        assert!(users_query.returns_list);
547        assert!(users_query.auto_params.has_where);
548        assert!(users_query.auto_params.has_order_by);
549        assert_eq!(users_query.arguments[0].default_value, Some(serde_json::json!(10)));
550    }
551
552    #[test]
553    fn test_generate_mutations() {
554        use super::super::ir::MutationOperation as IRMutationOp;
555
556        let generator = CodeGenerator::new(true);
557        let mut ir = AuthoringIR::new();
558
559        ir.mutations.push(IRMutation {
560            name:        "createUser".to_string(),
561            return_type: "User".to_string(),
562            nullable:    false,
563            arguments:   vec![IRArgument {
564                name:          "name".to_string(),
565                arg_type:      "String!".to_string(),
566                nullable:      false,
567                default_value: None,
568                description:   None,
569            }],
570            description: Some("Create a new user".to_string()),
571            operation:   IRMutationOp::Create,
572        });
573
574        let result = generator.generate(&ir, &[]);
575        assert!(result.is_ok());
576
577        let schema = result.unwrap();
578        assert_eq!(schema.mutations.len(), 1);
579
580        let mutation = &schema.mutations[0];
581        assert_eq!(mutation.name, "createUser");
582        // Insert operation should be inferred from Create
583        assert!(matches!(
584            &mutation.operation,
585            crate::schema::MutationOperation::Insert { table } if table == "user"
586        ));
587        assert_eq!(mutation.arguments.len(), 1);
588    }
589
590    #[test]
591    fn test_generate_subscriptions() {
592        let generator = CodeGenerator::new(true);
593        let mut ir = AuthoringIR::new();
594
595        ir.subscriptions.push(IRSubscription {
596            name:        "userCreated".to_string(),
597            return_type: "User".to_string(),
598            arguments:   vec![IRArgument {
599                name:          "tenantId".to_string(),
600                arg_type:      "ID!".to_string(),
601                nullable:      false,
602                default_value: None,
603                description:   None,
604            }],
605            description: Some("Subscribe to user creation events".to_string()),
606        });
607
608        let result = generator.generate(&ir, &[]);
609        assert!(result.is_ok());
610
611        let schema = result.unwrap();
612        assert_eq!(schema.subscriptions.len(), 1);
613
614        let subscription = &schema.subscriptions[0];
615        assert_eq!(subscription.name, "userCreated");
616        assert_eq!(subscription.return_type, "User");
617        assert_eq!(subscription.arguments.len(), 1);
618        assert_eq!(subscription.arguments[0].name, "tenantId");
619    }
620
621    #[test]
622    fn test_field_type_parsing_list_types() {
623        let generator = CodeGenerator::new(true);
624        let mut ir = AuthoringIR::new();
625
626        ir.types.push(IRType {
627            name:        "Post".to_string(),
628            fields:      vec![
629                IRField {
630                    name:        "tags".to_string(),
631                    field_type:  "[String]".to_string(),
632                    nullable:    true,
633                    description: None,
634                    sql_column:  None,
635                },
636                IRField {
637                    name:        "comments".to_string(),
638                    field_type:  "[Comment!]!".to_string(),
639                    nullable:    false,
640                    description: None,
641                    sql_column:  None,
642                },
643            ],
644            sql_source:  None,
645            description: None,
646        });
647
648        let result = generator.generate(&ir, &[]);
649        assert!(result.is_ok());
650
651        let schema = result.unwrap();
652        let post_type = &schema.types[0];
653
654        // [String] -> List(String)
655        assert!(matches!(
656            &post_type.fields[0].field_type,
657            FieldType::List(inner) if **inner == FieldType::String
658        ));
659
660        // [Comment!]! -> List(Object("Comment"))
661        assert!(matches!(
662            &post_type.fields[1].field_type,
663            FieldType::List(inner) if matches!(**inner, FieldType::Object(ref name) if name == "Comment")
664        ));
665    }
666
667    #[test]
668    fn test_generate_enums() {
669        use super::super::ir::{IREnum, IREnumValue};
670
671        let generator = CodeGenerator::new(true);
672        let mut ir = AuthoringIR::new();
673
674        ir.enums.push(IREnum {
675            name:        "OrderStatus".to_string(),
676            values:      vec![
677                IREnumValue {
678                    name:               "PENDING".to_string(),
679                    description:        Some("Order is pending".to_string()),
680                    deprecation_reason: None,
681                },
682                IREnumValue {
683                    name:               "COMPLETED".to_string(),
684                    description:        None,
685                    deprecation_reason: None,
686                },
687                IREnumValue {
688                    name:               "CANCELLED".to_string(),
689                    description:        None,
690                    deprecation_reason: Some("Use REJECTED instead".to_string()),
691                },
692            ],
693            description: Some("Possible order statuses".to_string()),
694        });
695
696        let result = generator.generate(&ir, &[]);
697        assert!(result.is_ok());
698
699        let schema = result.unwrap();
700        assert_eq!(schema.enums.len(), 1);
701
702        let order_status = &schema.enums[0];
703        assert_eq!(order_status.name, "OrderStatus");
704        assert_eq!(order_status.values.len(), 3);
705        assert_eq!(order_status.values[0].name, "PENDING");
706        assert_eq!(order_status.values[0].description, Some("Order is pending".to_string()));
707        assert!(order_status.values[2].deprecation.is_some());
708    }
709
710    #[test]
711    fn test_generate_interfaces() {
712        use super::super::ir::IRInterface;
713
714        let generator = CodeGenerator::new(true);
715        let mut ir = AuthoringIR::new();
716
717        ir.interfaces.push(IRInterface {
718            name:        "Node".to_string(),
719            fields:      vec![IRField {
720                name:        "id".to_string(),
721                field_type:  "ID!".to_string(),
722                nullable:    false,
723                description: Some("Unique identifier".to_string()),
724                sql_column:  None,
725            }],
726            description: Some("An object with an ID".to_string()),
727        });
728
729        let result = generator.generate(&ir, &[]);
730        assert!(result.is_ok());
731
732        let schema = result.unwrap();
733        assert_eq!(schema.interfaces.len(), 1);
734
735        let node = &schema.interfaces[0];
736        assert_eq!(node.name, "Node");
737        assert_eq!(node.fields.len(), 1);
738        assert_eq!(node.fields[0].name, "id");
739        assert_eq!(node.fields[0].field_type, FieldType::Id);
740    }
741
742    #[test]
743    fn test_generate_unions() {
744        use super::super::ir::IRUnion;
745
746        let generator = CodeGenerator::new(true);
747        let mut ir = AuthoringIR::new();
748
749        ir.unions.push(IRUnion {
750            name:        "SearchResult".to_string(),
751            types:       vec![
752                "User".to_string(),
753                "Post".to_string(),
754                "Comment".to_string(),
755            ],
756            description: Some("Possible search result types".to_string()),
757        });
758
759        let result = generator.generate(&ir, &[]);
760        assert!(result.is_ok());
761
762        let schema = result.unwrap();
763        assert_eq!(schema.unions.len(), 1);
764
765        let search_result = &schema.unions[0];
766        assert_eq!(search_result.name, "SearchResult");
767        assert_eq!(search_result.member_types.len(), 3);
768        assert_eq!(search_result.member_types[0], "User");
769    }
770
771    #[test]
772    fn test_generate_input_types() {
773        use super::super::ir::{IRInputField, IRInputType};
774
775        let generator = CodeGenerator::new(true);
776        let mut ir = AuthoringIR::new();
777
778        ir.input_types.push(IRInputType {
779            name:        "CreateUserInput".to_string(),
780            fields:      vec![
781                IRInputField {
782                    name:          "name".to_string(),
783                    field_type:    "String!".to_string(),
784                    nullable:      false,
785                    default_value: None,
786                    description:   Some("User's name".to_string()),
787                },
788                IRInputField {
789                    name:          "age".to_string(),
790                    field_type:    "Int".to_string(),
791                    nullable:      true,
792                    default_value: Some(serde_json::json!(18)),
793                    description:   None,
794                },
795            ],
796            description: Some("Input for creating a user".to_string()),
797        });
798
799        let result = generator.generate(&ir, &[]);
800        assert!(result.is_ok());
801
802        let schema = result.unwrap();
803        assert_eq!(schema.input_types.len(), 1);
804
805        let create_user = &schema.input_types[0];
806        assert_eq!(create_user.name, "CreateUserInput");
807        assert_eq!(create_user.fields.len(), 2);
808        assert_eq!(create_user.fields[0].name, "name");
809        assert_eq!(create_user.fields[1].default_value, Some("18".to_string()));
810    }
811
812    #[test]
813    fn test_generate_with_empty_scalars() {
814        let generator = CodeGenerator::new(true);
815        let mut ir = AuthoringIR::new();
816        ir.scalars = vec![];
817
818        let result = generator.generate(&ir, &[]);
819        assert!(result.is_ok());
820
821        let schema = result.unwrap();
822        assert_eq!(schema.custom_scalars.count(), 0);
823    }
824
825    #[test]
826    fn test_generate_with_single_custom_scalar() {
827        use super::super::ir::IRScalar;
828
829        let generator = CodeGenerator::new(true);
830        let mut ir = AuthoringIR::new();
831
832        ir.scalars.push(IRScalar {
833            name:             "LibraryCode".to_string(),
834            description:      Some("Unique library book identifier".to_string()),
835            specified_by_url: None,
836            validation_rules: vec![],
837            base_type:        Some("String".to_string()),
838        });
839
840        let result = generator.generate(&ir, &[]);
841        assert!(result.is_ok());
842
843        let schema = result.unwrap();
844        assert_eq!(schema.custom_scalars.count(), 1);
845        assert!(schema.custom_scalars.exists("LibraryCode"));
846
847        let def = schema.custom_scalars.get("LibraryCode").unwrap();
848        assert_eq!(def.name, "LibraryCode");
849        assert_eq!(def.description, Some("Unique library book identifier".to_string()));
850        assert_eq!(def.base_type, Some("String".to_string()));
851    }
852
853    #[test]
854    fn test_generate_with_multiple_custom_scalars() {
855        use super::super::ir::IRScalar;
856
857        let generator = CodeGenerator::new(true);
858        let mut ir = AuthoringIR::new();
859
860        ir.scalars.push(IRScalar {
861            name:             "LibraryCode".to_string(),
862            description:      None,
863            specified_by_url: None,
864            validation_rules: vec![],
865            base_type:        Some("String".to_string()),
866        });
867
868        ir.scalars.push(IRScalar {
869            name:             "StudentID".to_string(),
870            description:      Some("University student identifier".to_string()),
871            specified_by_url: None,
872            validation_rules: vec![],
873            base_type:        None,
874        });
875
876        ir.scalars.push(IRScalar {
877            name:             "PatientID".to_string(),
878            description:      Some("Hospital patient identifier".to_string()),
879            specified_by_url: Some("https://hl7.org/".to_string()),
880            validation_rules: vec![],
881            base_type:        Some("String".to_string()),
882        });
883
884        let result = generator.generate(&ir, &[]);
885        assert!(result.is_ok());
886
887        let schema = result.unwrap();
888        assert_eq!(schema.custom_scalars.count(), 3);
889
890        // Verify all scalars are registered
891        assert!(schema.custom_scalars.exists("LibraryCode"));
892        assert!(schema.custom_scalars.exists("StudentID"));
893        assert!(schema.custom_scalars.exists("PatientID"));
894
895        // Verify metadata preserved
896        let patient_id = schema.custom_scalars.get("PatientID").unwrap();
897        assert_eq!(patient_id.specified_by_url, Some("https://hl7.org/".to_string()));
898    }
899
900    #[test]
901    fn test_generate_preserves_scalar_description() {
902        use super::super::ir::IRScalar;
903
904        let generator = CodeGenerator::new(true);
905        let mut ir = AuthoringIR::new();
906
907        ir.scalars.push(IRScalar {
908            name:             "Email".to_string(),
909            description:      Some("RFC 5322 compliant email address".to_string()),
910            specified_by_url: Some("https://tools.ietf.org/html/rfc5322".to_string()),
911            validation_rules: vec![],
912            base_type:        Some("String".to_string()),
913        });
914
915        let result = generator.generate(&ir, &[]);
916        assert!(result.is_ok());
917
918        let schema = result.unwrap();
919        let email_def = schema.custom_scalars.get("Email").unwrap();
920
921        assert_eq!(email_def.description, Some("RFC 5322 compliant email address".to_string()));
922        assert_eq!(
923            email_def.specified_by_url,
924            Some("https://tools.ietf.org/html/rfc5322".to_string())
925        );
926    }
927
928    #[test]
929    fn test_generate_scalars_with_validation_rules() {
930        use super::super::ir::IRScalar;
931        use crate::validation::ValidationRule;
932
933        let generator = CodeGenerator::new(true);
934        let mut ir = AuthoringIR::new();
935
936        ir.scalars.push(IRScalar {
937            name:             "StudentID".to_string(),
938            description:      None,
939            specified_by_url: None,
940            validation_rules: vec![ValidationRule::Length {
941                min: Some(5),
942                max: Some(15),
943            }],
944            base_type:        Some("String".to_string()),
945        });
946
947        let result = generator.generate(&ir, &[]);
948        assert!(result.is_ok());
949
950        let schema = result.unwrap();
951        let student_id = schema.custom_scalars.get("StudentID").unwrap();
952
953        assert_eq!(student_id.validation_rules.len(), 1);
954    }
955}