Skip to main content

rh_codegen/
generator.rs

1//! FHIR type generation functionality
2//!
3//! This module contains the main code generator that orchestrates the generation of Rust types
4//! from FHIR StructureDefinitions using specialized sub-generators.
5
6#![allow(dead_code)] // Allow unused code during refactoring transition
7
8use std::collections::HashMap;
9use std::path::Path;
10
11use crate::config::CodegenConfig;
12use crate::fhir_types::StructureDefinition;
13use crate::generators::token_generator::TokenGenerator;
14#[cfg(test)]
15use crate::generators::ImportManager;
16use crate::generators::{
17    EnumGenerator, FieldGenerator, FileGenerator, FileIoManager, NestedStructGenerator,
18    PrimitiveGenerator, StructGenerator, TraitGenerator, TypeRegistry, TypeUtilities,
19};
20use crate::rust_types::{RustEnum, RustStruct, RustTrait};
21use crate::value_sets::ValueSetManager;
22use crate::CodegenResult;
23
24// Re-export from file_generator for backward compatibility
25pub use crate::generators::file_generator::FhirTypeCategory;
26
27/// Main code generator struct that orchestrates specialized generators
28pub struct CodeGenerator {
29    config: CodegenConfig,
30    /// Cache of previously generated types to avoid regenerating the same struct
31    type_cache: HashMap<String, RustStruct>,
32    /// Cache of generated enums for value set bindings
33    enum_cache: HashMap<String, RustEnum>,
34    /// ValueSet manager for handling ValueSet operations
35    value_set_manager: ValueSetManager,
36    /// Token generator for generating Rust code
37    token_generator: TokenGenerator,
38}
39
40impl CodeGenerator {
41    /// Create a new code generator with the given configuration
42    pub fn new(config: CodegenConfig) -> Self {
43        let value_set_manager = ValueSetManager::new();
44        let token_generator = TokenGenerator::new();
45
46        Self {
47            config,
48            type_cache: HashMap::new(),
49            enum_cache: HashMap::new(),
50            value_set_manager,
51            token_generator,
52        }
53    }
54
55    /// Create a new code generator with a ValueSet directory
56    pub fn new_with_value_set_directory<P: AsRef<Path>>(
57        config: CodegenConfig,
58        value_set_dir: P,
59    ) -> Self {
60        let value_set_manager = ValueSetManager::new_with_directory(value_set_dir);
61        let token_generator = TokenGenerator::new();
62
63        Self {
64            config,
65            type_cache: HashMap::new(),
66            enum_cache: HashMap::new(),
67            value_set_manager,
68            token_generator,
69        }
70    }
71
72    /// Load and parse a FHIR StructureDefinition from a JSON file
73    pub fn load_structure_definition<P: AsRef<Path>>(
74        &self,
75        path: P,
76    ) -> CodegenResult<StructureDefinition> {
77        FileIoManager::load_structure_definition(path)
78    }
79
80    /// Generate a Rust struct from a FHIR StructureDefinition
81    pub fn generate_struct(
82        &mut self,
83        structure_def: &StructureDefinition,
84    ) -> CodegenResult<RustStruct> {
85        // Register the type in the TypeRegistry based on its StructureDefinition
86        TypeRegistry::register_from_structure_definition(structure_def);
87
88        let mut struct_generator = StructGenerator::new(
89            &self.config,
90            &mut self.type_cache,
91            &mut self.value_set_manager,
92        );
93        let rust_struct = struct_generator.generate_struct(structure_def)?;
94
95        Ok(rust_struct)
96    }
97
98    /// Generate traits for a structure definition
99    pub fn generate_trait(
100        &mut self,
101        structure_def: &StructureDefinition,
102    ) -> CodegenResult<Vec<RustTrait>> {
103        let crate_lib_name = self
104            .config
105            .crate_name
106            .as_deref()
107            .map(|n| n.replace('-', "_"))
108            .unwrap_or_else(|| "hl7_fhir_r4_core".to_string());
109        let mut trait_generator = TraitGenerator::new_with_crate_name(crate_lib_name);
110        let mut traits = Vec::new();
111        let categories = ["Accessors", "Mutators", "Existence"];
112
113        for category in &categories {
114            let rust_trait = trait_generator.generate_trait(structure_def, category)?;
115            traits.push(rust_trait);
116        }
117
118        Ok(traits)
119    }
120
121    /// Generate a primitive type struct with special FHIR primitive type semantics
122    fn generate_primitive_type_struct(
123        &mut self,
124        structure_def: &StructureDefinition,
125        rust_struct: RustStruct,
126    ) -> CodegenResult<RustStruct> {
127        let mut primitive_generator = PrimitiveGenerator::new(&self.config, &mut self.type_cache);
128        primitive_generator.generate_primitive_type_struct(structure_def, rust_struct)
129    }
130
131    /// Generate a type alias for primitive types
132    fn generate_primitive_type_alias(
133        &self,
134        structure_def: &StructureDefinition,
135    ) -> CodegenResult<crate::rust_types::RustTypeAlias> {
136        let mut temp_cache = HashMap::new();
137        let primitive_generator = PrimitiveGenerator::new(&self.config, &mut temp_cache);
138        primitive_generator.generate_primitive_type_alias(structure_def)
139    }
140
141    /// Generate the companion Element struct for a primitive type
142    fn generate_primitive_element_struct(
143        &mut self,
144        structure_def: &StructureDefinition,
145    ) -> CodegenResult<RustStruct> {
146        let mut primitive_generator = PrimitiveGenerator::new(&self.config, &mut self.type_cache);
147        primitive_generator.generate_primitive_element_struct(structure_def)
148    }
149
150    /// Generate a nested struct for BackboneElements
151    fn generate_nested_struct(
152        &mut self,
153        parent_struct_name: &str,
154        nested_field_name: &str,
155        nested_elements: &[crate::fhir_types::ElementDefinition],
156        parent_structure_def: &StructureDefinition,
157    ) -> CodegenResult<Option<crate::rust_types::RustStruct>> {
158        let mut nested_struct_generator = NestedStructGenerator::new(
159            &self.config,
160            &mut self.type_cache,
161            &mut self.value_set_manager,
162        );
163        nested_struct_generator.generate_nested_struct(
164            parent_struct_name,
165            nested_field_name,
166            nested_elements,
167            parent_structure_def,
168        )
169    }
170
171    /// Create a RustField from an ElementDefinition
172    fn create_field_from_element(
173        &mut self,
174        element: &crate::fhir_types::ElementDefinition,
175    ) -> CodegenResult<Option<crate::rust_types::RustField>> {
176        let mut field_generator =
177            FieldGenerator::new(&self.config, &self.type_cache, &mut self.value_set_manager);
178        field_generator.create_field_from_element(element)
179    }
180
181    /// Convert a FHIR field name to a valid Rust field name
182    fn to_rust_field_name(&self, name: &str) -> String {
183        crate::naming::Naming::field_name(name)
184    }
185
186    /// Generate a Rust struct and write it to the appropriate directory based on FHIR type classification
187    pub fn generate_to_organized_directories<P: AsRef<Path>>(
188        &mut self,
189        structure_def: &StructureDefinition,
190        base_output_dir: P,
191    ) -> CodegenResult<()> {
192        let rust_struct = self.generate_struct(structure_def)?;
193        let nested_structs =
194            FileIoManager::collect_nested_structs(&rust_struct.name, &self.type_cache);
195
196        let file_io_manager = FileIoManager::new(&self.config, &self.token_generator);
197        file_io_manager.generate_to_organized_directories(
198            structure_def,
199            base_output_dir,
200            &rust_struct,
201            &nested_structs,
202        )
203    }
204
205    /// Generate traits and write them to the traits directory
206    pub fn generate_trait_to_organized_directory<P: AsRef<Path>>(
207        &mut self,
208        structure_def: &StructureDefinition,
209        base_output_dir: P,
210    ) -> CodegenResult<()> {
211        let rust_traits = self.generate_trait(structure_def)?;
212
213        let file_io_manager = FileIoManager::new(&self.config, &self.token_generator);
214        file_io_manager.generate_traits_to_organized_directory(
215            structure_def,
216            base_output_dir.as_ref(),
217            &rust_traits,
218        )
219    }
220
221    /// Classify a FHIR StructureDefinition into the appropriate category
222    pub fn classify_fhir_structure_def(
223        &self,
224        structure_def: &StructureDefinition,
225    ) -> FhirTypeCategory {
226        let file_generator = FileGenerator::new(&self.config, &self.token_generator);
227        file_generator.classify_fhir_structure_def(structure_def)
228    }
229
230    /// Check if a type name represents a known FHIR data type
231    fn is_fhir_datatype(&self, name: &str) -> bool {
232        TypeUtilities::is_fhir_datatype(name)
233    }
234
235    /// Generate a Rust struct and write it to a file
236    pub fn generate_to_file<P: AsRef<Path>>(
237        &mut self,
238        structure_def: &StructureDefinition,
239        output_path: P,
240    ) -> CodegenResult<()> {
241        if structure_def.kind == "primitive-type" {
242            // For primitive types, use empty placeholder values
243            let empty_struct = RustStruct::new("".to_string());
244            let nested_structs = vec![];
245            let file_io_manager = FileIoManager::new(&self.config, &self.token_generator);
246            file_io_manager.generate_to_file(
247                structure_def,
248                output_path,
249                &empty_struct,
250                &nested_structs,
251            )
252        } else {
253            // Generate the main struct for non-primitive types
254            let rust_struct = self.generate_struct(structure_def)?;
255            let nested_structs =
256                FileIoManager::collect_nested_structs(&rust_struct.name, &self.type_cache);
257            let file_io_manager = FileIoManager::new(&self.config, &self.token_generator);
258
259            file_io_manager.generate_to_file(
260                structure_def,
261                output_path,
262                &rust_struct,
263                &nested_structs,
264            )
265        }
266    }
267
268    /// Generate a Rust trait and write it to a file
269    pub fn generate_trait_to_file<P: AsRef<Path>>(
270        &mut self,
271        structure_def: &StructureDefinition,
272        output_path: P,
273    ) -> CodegenResult<()> {
274        // Generate the trait first
275        let rust_traits = self.generate_trait(structure_def)?;
276
277        // Create FileIoManager and delegate
278        let file_io_manager = FileIoManager::new(&self.config, &self.token_generator);
279
280        // Use generate_traits_to_file to write all traits to the same file
281        file_io_manager.generate_traits_to_file(
282            structure_def,
283            output_path.as_ref(),
284            &rust_traits,
285        )?;
286
287        Ok(())
288    }
289
290    /// Pre-scan and register all ValueSet enums in the TypeRegistry
291    /// This should be called before processing resources to ensure correct import paths
292    pub fn pre_register_value_set_enums<P: AsRef<Path>>(
293        &mut self,
294        package_dir: P,
295    ) -> CodegenResult<()> {
296        let package_path = package_dir.as_ref();
297
298        // Scan for ValueSet JSON files in the package directory
299        if !package_path.exists() {
300            return Ok(()); // Nothing to scan
301        }
302
303        let entries = match std::fs::read_dir(package_path) {
304            Ok(entries) => entries,
305            Err(_) => return Ok(()), // Can't read directory, skip
306        };
307
308        for entry in entries {
309            let entry = match entry {
310                Ok(entry) => entry,
311                Err(_) => continue, // Skip problematic entries
312            };
313
314            let path = entry.path();
315            if !path.is_file() || path.extension().is_none_or(|ext| ext != "json") {
316                continue;
317            }
318
319            // Try to read and parse as ValueSet
320            let content = match std::fs::read_to_string(&path) {
321                Ok(content) => content,
322                Err(_) => continue, // Skip unreadable files
323            };
324
325            let json_value: serde_json::Value = match serde_json::from_str(&content) {
326                Ok(value) => value,
327                Err(_) => continue, // Skip invalid JSON
328            };
329
330            // Check if this is a ValueSet resource
331            if json_value.get("resourceType").and_then(|v| v.as_str()) != Some("ValueSet") {
332                continue;
333            }
334
335            // Get the ValueSet URL to generate enum name
336            if let Some(url) = json_value.get("url").and_then(|v| v.as_str()) {
337                // Generate enum name using the same logic as the enum generator
338                let enum_name = self.value_set_manager.generate_enum_name(url);
339
340                // Pre-register this enum in the TypeRegistry
341                crate::generators::type_registry::TypeRegistry::register_type_classification_only(
342                    &enum_name,
343                    crate::generators::type_registry::TypeClassification::ValueSetEnum,
344                );
345            }
346        }
347
348        Ok(())
349    }
350
351    /// Generate all ValueSet enums to separate files in the specified directory
352    pub fn generate_enum_files<P: AsRef<Path>>(&mut self, enums_dir: P) -> CodegenResult<()> {
353        let enum_generator = EnumGenerator::new(&mut self.value_set_manager, &mut self.enum_cache);
354        let token_generator = crate::generators::token_generator::TokenGenerator::new();
355        let file_generator = FileGenerator::new(&self.config, &token_generator);
356
357        file_generator.generate_enum_files(enums_dir, &enum_generator)
358    }
359
360    /// Generate a mod.rs file that re-exports all the enum modules
361    pub fn generate_enums_mod_file<P: AsRef<Path>>(&self, enums_dir: P) -> CodegenResult<()> {
362        // Create an EnumGenerator with access to the cached enums
363        let mut value_set_manager = self.value_set_manager.clone(); // Need to clone for borrow checker
364        let mut enum_cache = self.enum_cache.clone();
365        let enum_generator = EnumGenerator::new(&mut value_set_manager, &mut enum_cache);
366
367        // Create FileGenerator and delegate
368        let file_generator = FileGenerator::new(&self.config, &self.token_generator);
369        file_generator.generate_enums_mod_file(enums_dir, &enum_generator)
370    }
371
372    /// Generate an enum for a value set binding
373    pub fn generate_enum_for_value_set(
374        &mut self,
375        value_set_url: &str,
376    ) -> CodegenResult<Option<RustEnum>> {
377        let mut enum_generator =
378            EnumGenerator::new(&mut self.value_set_manager, &mut self.enum_cache);
379        let result = enum_generator.generate_enum_for_value_set(value_set_url)?;
380
381        Ok(result)
382    }
383
384    /// Check if any ValueSet enums have been generated
385    pub fn has_cached_enums(&self) -> bool {
386        TypeUtilities::has_cached_enums(&self.value_set_manager)
387    }
388
389    /// Convert a FHIR resource type name to filename using snake_case
390    pub fn to_filename(&self, structure_def: &StructureDefinition) -> String {
391        crate::naming::Naming::filename(structure_def)
392    }
393
394    /// Generate a trait file directly from a RustTrait object
395    pub fn generate_trait_file_from_trait<P: AsRef<Path>>(
396        &self,
397        rust_trait: &RustTrait,
398        output_path: P,
399    ) -> CodegenResult<()> {
400        // Create FileGenerator and delegate
401        let file_generator = FileGenerator::new(&self.config, &self.token_generator);
402        file_generator.generate_trait_file_from_trait(rust_trait, output_path)
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn test_to_valid_rust_identifier_conversion() {
412        // Test FHIR resource names that should preserve original case
413        assert_eq!(
414            crate::naming::Naming::to_rust_identifier("StructureDefinition"),
415            "StructureDefinition"
416        );
417        assert_eq!(
418            crate::naming::Naming::to_rust_identifier("Patient"),
419            "Patient"
420        );
421        assert_eq!(
422            crate::naming::Naming::to_rust_identifier("Observation"),
423            "Observation"
424        );
425        assert_eq!(
426            crate::naming::Naming::to_rust_identifier("CodeSystem"),
427            "CodeSystem"
428        );
429
430        // Test names that need conversion due to special characters
431        assert_eq!(
432            crate::naming::Naming::to_rust_identifier("patient"),
433            "patient"
434        );
435
436        // Test names with spaces
437        assert_eq!(
438            crate::naming::Naming::to_rust_identifier("Relative Date Criteria"),
439            "RelativeDateCriteria"
440        );
441        assert_eq!(
442            crate::naming::Naming::to_rust_identifier("Care Plan"),
443            "CarePlan"
444        );
445
446        // Test names with dashes and underscores
447        assert_eq!(
448            crate::naming::Naming::to_rust_identifier("patient-name"),
449            "PatientName"
450        );
451        assert_eq!(
452            crate::naming::Naming::to_rust_identifier("patient_name"),
453            "patient_name"
454        );
455
456        // Test mixed separators
457        assert_eq!(
458            crate::naming::Naming::to_rust_identifier("some-complex_name with.spaces"),
459            "SomeComplexNameWithSpaces"
460        );
461
462        // Test empty and edge cases
463        assert_eq!(crate::naming::Naming::to_rust_identifier(""), "_");
464        assert_eq!(crate::naming::Naming::to_rust_identifier("   "), "_");
465        assert_eq!(crate::naming::Naming::to_rust_identifier("a"), "a");
466    }
467
468    #[test]
469    fn test_logical_model_skipping() {
470        use crate::fhir_types::StructureDefinition;
471
472        let config = CodegenConfig::default();
473        let mut generator = CodeGenerator::new(config);
474
475        // Create a test LogicalModel StructureDefinition
476        let logical_model = StructureDefinition {
477            resource_type: "StructureDefinition".to_string(),
478            id: "test-logical-model".to_string(),
479            url: "http://example.org/fhir/StructureDefinition/test-logical-model".to_string(),
480            name: "test-logical-model".to_string(),
481            title: Some("Test Logical Model".to_string()),
482            status: "active".to_string(),
483            kind: "logical".to_string(),
484            is_abstract: false,
485            description: Some("A test logical model".to_string()),
486            purpose: None,
487            base_type: "Base".to_string(),
488            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Base".to_string()),
489            version: None,
490            differential: None,
491            snapshot: None,
492        };
493
494        // Test that LogicalModels are rejected
495        let result = generator.generate_struct(&logical_model);
496        assert!(result.is_err());
497
498        if let Err(crate::CodegenError::Generation { message }) = result {
499            assert!(message.contains("Skipping LogicalModel"));
500            assert!(message.contains("test-logical-model"));
501        } else {
502            panic!("Expected CodegenError::Generation for LogicalModel");
503        }
504    }
505
506    #[test]
507    fn test_nested_struct_generation() {
508        use crate::fhir_types::{
509            ElementDefinition, ElementType, StructureDefinition, StructureDefinitionDifferential,
510        };
511
512        let config = CodegenConfig::default();
513        let mut generator = CodeGenerator::new(config);
514
515        // Create a simplified Bundle StructureDefinition with nested entry
516        let bundle_structure = StructureDefinition {
517            resource_type: "StructureDefinition".to_string(),
518            id: "Bundle".to_string(),
519            url: "http://hl7.org/fhir/StructureDefinition/Bundle".to_string(),
520            name: "Bundle".to_string(),
521            title: Some("Bundle".to_string()),
522            status: "active".to_string(),
523            kind: "resource".to_string(),
524            is_abstract: false,
525            description: Some("A container for a collection of resources".to_string()),
526            purpose: None,
527            base_type: "Bundle".to_string(),
528            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Resource".to_string()),
529            version: None,
530            differential: Some(StructureDefinitionDifferential {
531                element: vec![
532                    ElementDefinition {
533                        id: Some("Bundle.entry".to_string()),
534                        path: "Bundle.entry".to_string(),
535                        short: Some("Entry in the bundle".to_string()),
536                        definition: None,
537                        min: Some(0),
538                        max: Some("*".to_string()),
539                        element_type: Some(vec![ElementType {
540                            code: Some("BackboneElement".to_string()),
541                            target_profile: None,
542                        }]),
543                        fixed: None,
544                        pattern: None,
545                        binding: None,
546                        constraint: None,
547                    },
548                    ElementDefinition {
549                        id: Some("Bundle.entry.resource".to_string()),
550                        path: "Bundle.entry.resource".to_string(),
551                        short: Some("A resource in the bundle".to_string()),
552                        definition: None,
553                        min: Some(0),
554                        max: Some("1".to_string()),
555                        element_type: Some(vec![ElementType {
556                            code: Some("Resource".to_string()),
557                            target_profile: None,
558                        }]),
559                        fixed: None,
560                        pattern: None,
561                        binding: None,
562                        constraint: None,
563                    },
564                ],
565            }),
566            snapshot: None,
567        };
568
569        // Generate the struct
570        let result = generator.generate_struct(&bundle_structure);
571        assert!(result.is_ok());
572
573        let bundle_struct = result.unwrap();
574
575        // Check that the main Bundle struct was generated
576        assert_eq!(bundle_struct.name, "Bundle");
577
578        // Check that an entry field exists
579        let entry_field = bundle_struct.fields.iter().find(|f| f.name == "entry");
580        assert!(entry_field.is_some(), "Bundle should have an entry field");
581
582        // Check that the nested BundleEntry struct was generated and cached
583        assert!(
584            generator.type_cache.contains_key("BundleEntry"),
585            "BundleEntry struct should be generated"
586        );
587
588        let bundle_entry_struct = generator.type_cache.get("BundleEntry").unwrap();
589        assert_eq!(bundle_entry_struct.name, "BundleEntry");
590
591        // Check that BundleEntry has the expected fields
592        let resource_field = bundle_entry_struct
593            .fields
594            .iter()
595            .find(|f| f.name == "resource");
596        assert!(
597            resource_field.is_some(),
598            "BundleEntry should have a resource field"
599        );
600    }
601
602    #[test]
603    fn test_primitive_type_naming() {
604        use crate::fhir_types::StructureDefinition;
605
606        // Test primitive type - should not be capitalized
607        let primitive_structure = StructureDefinition {
608            resource_type: "StructureDefinition".to_string(),
609            id: "string".to_string(),
610            url: "http://hl7.org/fhir/StructureDefinition/string".to_string(),
611            name: "string".to_string(),
612            title: Some("string".to_string()),
613            status: "active".to_string(),
614            kind: "primitive-type".to_string(),
615            is_abstract: false,
616            description: Some("A sequence of Unicode characters".to_string()),
617            purpose: None,
618            base_type: "string".to_string(),
619            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Element".to_string()),
620            version: None,
621            differential: None,
622            snapshot: None,
623        };
624
625        // Test that primitive types are not capitalized
626        let struct_name = crate::naming::Naming::struct_name(&primitive_structure);
627        assert_eq!(
628            struct_name, "string",
629            "Primitive type 'string' should not be capitalized"
630        );
631
632        let filename = crate::naming::Naming::filename(&primitive_structure);
633        assert_eq!(
634            filename, "string.rs",
635            "Primitive type filename should not be capitalized"
636        );
637
638        // Test another primitive type
639        let boolean_structure = StructureDefinition {
640            resource_type: "StructureDefinition".to_string(),
641            id: "boolean".to_string(),
642            url: "http://hl7.org/fhir/StructureDefinition/boolean".to_string(),
643            name: "boolean".to_string(),
644            title: Some("boolean".to_string()),
645            status: "active".to_string(),
646            kind: "primitive-type".to_string(),
647            is_abstract: false,
648            description: Some("Value of 'true' or 'false'".to_string()),
649            purpose: None,
650            base_type: "boolean".to_string(),
651            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Element".to_string()),
652            version: None,
653            differential: None,
654            snapshot: None,
655        };
656
657        let struct_name = crate::naming::Naming::struct_name(&boolean_structure);
658        assert_eq!(
659            struct_name, "boolean",
660            "Primitive type 'boolean' should not be capitalized"
661        );
662
663        // Test complex type - should be capitalized
664        let complex_structure = StructureDefinition {
665            resource_type: "StructureDefinition".to_string(),
666            id: "Period".to_string(),
667            url: "http://hl7.org/fhir/StructureDefinition/Period".to_string(),
668            name: "Period".to_string(),
669            title: Some("Period".to_string()),
670            status: "active".to_string(),
671            kind: "complex-type".to_string(),
672            is_abstract: false,
673            description: Some("A time period defined by a start and end date".to_string()),
674            purpose: None,
675            base_type: "Period".to_string(),
676            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Element".to_string()),
677            version: None,
678            differential: None,
679            snapshot: None,
680        };
681
682        let struct_name = crate::naming::Naming::struct_name(&complex_structure);
683        assert_eq!(
684            struct_name, "Period",
685            "Complex type 'Period' should be capitalized"
686        );
687    }
688
689    #[test]
690    fn test_primitive_type_generation() {
691        use crate::fhir_types::StructureDefinition;
692        use crate::rust_types::RustType;
693
694        let config = CodegenConfig::default();
695        let mut generator = CodeGenerator::new(config);
696
697        // Test primitive type generation
698        let uri_structure = StructureDefinition {
699            resource_type: "StructureDefinition".to_string(),
700            id: "uri".to_string(),
701            url: "http://hl7.org/fhir/StructureDefinition/uri".to_string(),
702            name: "uri".to_string(),
703            title: Some("uri".to_string()),
704            status: "active".to_string(),
705            kind: "primitive-type".to_string(),
706            is_abstract: false,
707            description: Some(
708                "String of characters used to identify a name or a resource".to_string(),
709            ),
710            purpose: None,
711            base_type: "uri".to_string(),
712            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Element".to_string()),
713            version: None,
714            differential: None,
715            snapshot: None,
716        };
717
718        // Test that primitive type alias is generated correctly
719        let type_alias_result = generator.generate_primitive_type_alias(&uri_structure);
720        assert!(
721            type_alias_result.is_ok(),
722            "Should generate primitive type alias successfully"
723        );
724
725        let uri_type_alias = type_alias_result.unwrap();
726
727        // Check that the type alias name follows the new PascalCase Type suffix convention
728        assert_eq!(
729            uri_type_alias.name, "UriType",
730            "Primitive type alias should use PascalCase with Type suffix"
731        );
732
733        // Check that the type alias target is String for uri
734        if let RustType::String = uri_type_alias.target_type {
735            // Expected
736        } else {
737            panic!(
738                "URI primitive type alias should target String, got: {:?}",
739                uri_type_alias.target_type
740            );
741        }
742
743        // Test boolean primitive type
744        let boolean_structure = StructureDefinition {
745            resource_type: "StructureDefinition".to_string(),
746            id: "boolean".to_string(),
747            url: "http://hl7.org/fhir/StructureDefinition/boolean".to_string(),
748            name: "boolean".to_string(),
749            title: Some("boolean".to_string()),
750            status: "active".to_string(),
751            kind: "primitive-type".to_string(),
752            is_abstract: false,
753            description: Some("Value of 'true' or 'false'".to_string()),
754            purpose: None,
755            base_type: "boolean".to_string(),
756            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Element".to_string()),
757            version: None,
758            differential: None,
759            snapshot: None,
760        };
761
762        let type_alias_result = generator.generate_primitive_type_alias(&boolean_structure);
763        assert!(
764            type_alias_result.is_ok(),
765            "Should generate boolean primitive type alias successfully"
766        );
767
768        let boolean_type_alias = type_alias_result.unwrap();
769
770        // Check that the boolean type alias targets bool
771        if let RustType::Boolean = boolean_type_alias.target_type {
772            // Expected
773        } else {
774            panic!(
775                "Boolean primitive type alias should target bool, got: {:?}",
776                boolean_type_alias.target_type
777            );
778        }
779
780        // Test that the companion Element struct is generated
781        let element_struct = generator.generate_primitive_element_struct(&uri_structure);
782        assert!(
783            element_struct.is_ok(),
784            "Should generate companion Element struct successfully"
785        );
786
787        let element_struct = element_struct.unwrap();
788        assert_eq!(
789            element_struct.name, "_uri",
790            "Companion Element struct should be named '_uri'"
791        );
792        assert_eq!(
793            element_struct.base_definition,
794            Some("Element".to_string()),
795            "Companion Element struct should inherit from Element"
796        );
797    }
798
799    #[test]
800    fn test_trait_generation() {
801        use crate::fhir_types::{
802            ElementDefinition, ElementType, StructureDefinition, StructureDefinitionDifferential,
803        };
804
805        let config = CodegenConfig::default();
806        let mut generator = CodeGenerator::new(config);
807
808        // Create a simplified Patient StructureDefinition for trait generation
809        let patient_structure = StructureDefinition {
810            resource_type: "StructureDefinition".to_string(),
811            id: "Patient".to_string(),
812            url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
813            name: "Patient".to_string(),
814            title: Some("Patient".to_string()),
815            status: "active".to_string(),
816            kind: "resource".to_string(),
817            is_abstract: false,
818            description: Some("Demographics and other administrative information about an individual receiving care.".to_string()),
819            purpose: None,
820            base_type: "DomainResource".to_string(),
821            base_definition: Some("http://hl7.org/fhir/StructureDefinition/DomainResource".to_string()),
822            version: None,
823            differential: Some(StructureDefinitionDifferential {
824                element: vec![
825                    ElementDefinition {
826                        id: Some("Patient.active".to_string()),
827                        path: "Patient.active".to_string(),
828                        short: Some("Whether this patient record is in active use".to_string()),
829                        definition: Some("Whether this patient record is in active use".to_string()),
830                        min: Some(0),
831                        max: Some("1".to_string()),
832                        element_type: Some(vec![ElementType {
833                            code: Some("boolean".to_string()),
834                            target_profile: None,
835                        }]),
836                        fixed: None,
837                        pattern: None,
838                        binding: None,
839                        constraint: None,
840                    },
841                    ElementDefinition {
842                        id: Some("Patient.name".to_string()),
843                        path: "Patient.name".to_string(),
844                        short: Some("A name associated with the patient".to_string()),
845                        definition: Some("A name associated with the patient".to_string()),
846                        min: Some(0),
847                        max: Some("*".to_string()),
848                        element_type: Some(vec![ElementType {
849                            code: Some("HumanName".to_string()),
850                            target_profile: None,
851                        }]),
852                        fixed: None,
853                        pattern: None,
854                        binding: None,
855                        constraint: None,
856                    },
857                ],
858            }),
859            snapshot: None,
860        };
861
862        // Generate the trait
863        let result = generator.generate_trait(&patient_structure);
864        assert!(result.is_ok(), "Should generate Patient trait successfully");
865
866        let patient_traits = result.unwrap();
867        let patient_trait = patient_traits
868            .iter()
869            .find(|t| t.name == "PatientAccessors")
870            .expect("PatientAccessors trait not found");
871
872        assert_eq!(
873            patient_trait.name, "PatientAccessors",
874            "Trait should be named 'PatientAccessors'"
875        );
876
877        // Check that the Patient trait properly inherits from DomainResource
878        assert!(
879            patient_trait
880                .super_traits
881                .contains(&"DomainResourceAccessors".to_string()),
882            "Patient trait should inherit from DomainResourceAccessors"
883        );
884
885        // The Patient trait should NOT have methods that are inherited from parent traits
886        let has_extensions = patient_trait.methods.iter().any(|m| m.name == "extensions");
887        assert!(
888            !has_extensions,
889            "Patient trait should NOT have extensions method - it should be inherited from Resource"
890        );
891
892        let has_narrative = patient_trait.methods.iter().any(|m| m.name == "narrative");
893        assert!(
894            !has_narrative,
895            "Patient trait should NOT have narrative method - it should be inherited from DomainResource"
896        );
897
898        let has_id = patient_trait.methods.iter().any(|m| m.name == "id");
899        assert!(
900            !has_id,
901            "Patient trait should NOT have id method - it should be inherited from Resource"
902        );
903
904        // Note: The new trait generator focuses on resource-level methods rather than
905        // field-specific methods like 'active' and 'name', which are handled by struct implementations
906    }
907
908    #[test]
909    fn test_filename_generation() {
910        // Test PascalCase struct names generate snake_case filenames
911        let patient_structure = StructureDefinition {
912            resource_type: "StructureDefinition".to_string(),
913            id: "Patient".to_string(),
914            url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
915            name: "Patient".to_string(),
916            title: Some("Patient".to_string()),
917            status: "active".to_string(),
918            kind: "resource".to_string(),
919            is_abstract: false,
920            description: Some("A patient resource".to_string()),
921            purpose: None,
922            base_type: "DomainResource".to_string(),
923            base_definition: Some(
924                "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
925            ),
926            version: None,
927            differential: None,
928            snapshot: None,
929        };
930
931        let observation_structure = StructureDefinition {
932            resource_type: "StructureDefinition".to_string(),
933            id: "Observation".to_string(),
934            url: "http://hl7.org/fhir/StructureDefinition/Observation".to_string(),
935            name: "Observation".to_string(),
936            title: Some("Observation".to_string()),
937            status: "active".to_string(),
938            kind: "resource".to_string(),
939            is_abstract: false,
940            description: Some("An observation resource".to_string()),
941            purpose: None,
942            base_type: "DomainResource".to_string(),
943            base_definition: Some(
944                "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
945            ),
946            version: None,
947            differential: None,
948            snapshot: None,
949        };
950
951        // Test that struct names remain PascalCase
952        let patient_struct_name = crate::naming::Naming::struct_name(&patient_structure);
953        assert_eq!(patient_struct_name, "Patient");
954
955        let observation_struct_name = crate::naming::Naming::struct_name(&observation_structure);
956        assert_eq!(observation_struct_name, "Observation");
957
958        // Test that filenames are snake_case
959        let patient_filename = crate::naming::Naming::filename(&patient_structure);
960        assert_eq!(patient_filename, "patient.rs");
961
962        let observation_filename = crate::naming::Naming::filename(&observation_structure);
963        assert_eq!(observation_filename, "observation.rs");
964
965        // Test more complex PascalCase names
966        let structure_definition = StructureDefinition {
967            resource_type: "StructureDefinition".to_string(),
968            id: "StructureDefinition".to_string(),
969            url: "http://hl7.org/fhir/StructureDefinition/StructureDefinition".to_string(),
970            name: "StructureDefinition".to_string(),
971            title: Some("StructureDefinition".to_string()),
972            status: "active".to_string(),
973            kind: "resource".to_string(),
974            is_abstract: false,
975            description: Some("A structure definition".to_string()),
976            purpose: None,
977            base_type: "DomainResource".to_string(),
978            base_definition: Some(
979                "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
980            ),
981            version: None,
982            differential: None,
983            snapshot: None,
984        };
985
986        let struct_def_struct_name = crate::naming::Naming::struct_name(&structure_definition);
987        assert_eq!(struct_def_struct_name, "StructureDefinition");
988
989        let struct_def_filename = crate::naming::Naming::filename(&structure_definition);
990        assert_eq!(struct_def_filename, "structure_definition.rs");
991
992        // Test enum filename generation
993        let enum_filename = crate::naming::Naming::enum_filename("AdministrativeGender");
994        assert_eq!(enum_filename, "administrative_gender.rs");
995
996        let enum_module_name = crate::naming::Naming::module_name("AdministrativeGender");
997        assert_eq!(enum_module_name, "administrative_gender");
998    }
999
1000    #[test]
1001    fn test_import_classification() {
1002        // Test resource classification
1003        assert!(ImportManager::is_fhir_resource_type("DomainResource"));
1004        assert!(ImportManager::is_fhir_resource_type("Patient"));
1005        assert!(ImportManager::is_fhir_resource_type("ActivityDefinition"));
1006        assert!(!ImportManager::is_fhir_resource_type("Identifier"));
1007
1008        // Test datatype classification
1009        assert!(ImportManager::is_fhir_datatype("Identifier"));
1010        assert!(ImportManager::is_fhir_datatype("CodeableConcept"));
1011        assert!(ImportManager::is_fhir_datatype("Reference"));
1012        assert!(!ImportManager::is_fhir_datatype("DomainResource"));
1013
1014        // Test import path generation
1015        assert_eq!(
1016            ImportManager::get_import_path_for_type("DomainResource"),
1017            "crate::resources::domain_resource::DomainResource"
1018        );
1019        assert_eq!(
1020            ImportManager::get_import_path_for_type("Identifier"),
1021            "crate::datatypes::identifier::Identifier"
1022        );
1023        assert_eq!(
1024            ImportManager::get_import_path_for_type("PublicationStatus"),
1025            "crate::bindings::publication_status::PublicationStatus"
1026        );
1027    }
1028}