Skip to main content

rh_codegen/generators/file_generator/
mod.rs

1//! File generation and organization functionality
2//!
3//! This module handles writing generated code to files and organizing the output structure.
4//!
5//! ## Ergonomic Improvements for Trait Usage
6//!
7//! This module implements two key ergonomic improvements for generated FHIR resources:
8//!
9//! ### 1. Trait Re-exports in Resource Modules
10//!
11//! Each generated resource module (e.g., `resources::patient`) automatically re-exports its
12//! associated traits (`PatientMutators`, `PatientAccessors`, `PatientExistence`). This allows
13//! users to import just the resource module and get all necessary traits:
14//!
15//! ```ignore
16//! // Before: Required importing from multiple modules
17//! use hl7_fhir_r4_core::resources::patient::Patient;
18//! use hl7_fhir_r4_core::traits::patient::PatientMutators;
19//! use hl7_fhir_r4_core::traits::domain_resource::DomainResourceMutators;
20//! use hl7_fhir_r4_core::traits::resource::ResourceMutators;
21//!
22//! // After: Single import gets everything needed
23//! use hl7_fhir_r4_core::resources::patient::{Patient, PatientMutators};
24//! // Note: Parent traits (ResourceMutators, DomainResourceMutators) are trait bounds,
25//! // so they're brought into scope automatically when PatientMutators is used
26//! ```
27//!
28//! ### 2. Prelude Module
29//!
30//! A `prelude` module is generated that re-exports commonly used base traits:
31//!
32//! ```ignore
33//! use hl7_fhir_r4_core::prelude::*;
34//! use hl7_fhir_r4_core::resources::patient::{Patient, PatientMutators};
35//!
36//! // Now all base traits are in scope
37//! let patient = <Patient as PatientMutators>::new()
38//!     .set_id("example".to_string())  // from ResourceMutators
39//!     .set_active(true);               // from PatientMutators
40//! ```
41//!
42//! These improvements follow idiomatic Rust patterns used by popular crates like
43//! `serde`, `tokio`, and `diesel`, making the generated code more ergonomic and
44//! reducing the cognitive load on users.
45
46mod enum_files;
47mod trait_file;
48
49use std::collections::HashSet;
50use std::fs;
51use std::path::Path;
52
53use quote::{format_ident, quote};
54
55use crate::config::CodegenConfig;
56use crate::fhir_types::StructureDefinition;
57use crate::generators::binding_generator::BindingGenerator;
58use crate::generators::import_manager::ImportManager;
59use crate::generators::primitive_generator::PrimitiveGenerator;
60use crate::generators::token_generator::TokenGenerator;
61use crate::rust_types::{RustStruct, RustTrait};
62use crate::{CodegenError, CodegenResult};
63
64#[derive(Debug, Clone, PartialEq)]
65pub enum FhirTypeCategory {
66    Resource,
67    Profile,
68    DataType,
69    Extension,
70    Primitive,
71}
72
73pub struct FileGenerator<'a> {
74    pub(crate) config: &'a CodegenConfig,
75    pub(crate) token_generator: &'a TokenGenerator,
76}
77
78impl<'a> FileGenerator<'a> {
79    pub fn new(config: &'a CodegenConfig, token_generator: &'a TokenGenerator) -> Self {
80        Self {
81            config,
82            token_generator,
83        }
84    }
85
86    pub fn generate_macros_file<P: AsRef<Path>>(&self, output_path: P) -> CodegenResult<()> {
87        let macros_content = include_str!("../../macros.rs");
88
89        let syntax_tree =
90            syn::parse_file(macros_content).map_err(|e| CodegenError::Generation {
91                message: format!("Failed to parse macros file: {e}"),
92            })?;
93
94        let formatted_code = prettyplease::unparse(&syntax_tree);
95
96        fs::write(output_path, formatted_code)?;
97
98        Ok(())
99    }
100
101    pub fn generate_lib_file<P: AsRef<Path>>(&self, output_path: P) -> CodegenResult<()> {
102        let lib_tokens = quote! {
103            //! Generated FHIR Rust bindings
104            //!
105            //! This crate contains Rust types and traits for FHIR resources and data types.
106            //! It includes macros for primitive field generation and maintains FHIR compliance.
107
108            // Allow clippy lint for derivable Default implementations
109            //
110            // TODO: Future optimization - derive Default when possible instead of manual impl
111            //
112            // Currently, we generate explicit Default implementations for all structs.
113            // Many of these could use #[derive(Default)] instead, which would be more idiomatic.
114            //
115            // Pros of deriving Default:
116            // - More idiomatic Rust code
117            // - Less generated code (no manual impl blocks)
118            // - Clearer intent (all fields use Default::default())
119            //
120            // Cons of current approach (manual impl):
121            // - Clippy warns about 1,100+ derivable implementations
122            // - More verbose generated code
123            //
124            // Pros of current approach:
125            // - Explicit and predictable behavior
126            // - Handles mixed initialization patterns consistently
127            // - Simpler code generation logic
128            //
129            // To implement derive-based approach would require:
130            // 1. Analyze all field types to ensure they implement Default
131            // 2. Detect required fields with non-Default initializations (String::new(), Vec::new(), etc.)
132            // 3. Add "Default" to struct derives only when ALL fields can use Default::default()
133            // 4. Skip manual impl generation for those structs
134            //
135            #![allow(clippy::derivable_impls)]
136
137            pub mod macros;
138            pub mod primitives;
139            pub mod datatypes;
140            pub mod extensions;
141            pub mod resources;
142            pub mod traits;
143            pub mod bindings;
144
145            // Re-export macros and serde traits for convenience
146            pub use macros::*;
147            pub use serde::{Deserialize, Serialize};
148        };
149
150        let syntax_tree = syn::parse2(lib_tokens).map_err(|e| CodegenError::Generation {
151            message: format!("Failed to parse generated lib tokens: {e}"),
152        })?;
153
154        let formatted_code = prettyplease::unparse(&syntax_tree);
155
156        fs::write(output_path, formatted_code)?;
157
158        Ok(())
159    }
160
161    pub fn generate_module_file<P: AsRef<Path>>(
162        &self,
163        module_dir: P,
164        module_names: &[String],
165    ) -> CodegenResult<()> {
166        let module_dir = module_dir.as_ref();
167        let mod_file_path = module_dir.join("mod.rs");
168
169        let mut mod_tokens = proc_macro2::TokenStream::new();
170
171        for module_name in module_names {
172            let mod_ident = format_ident!("{}", module_name);
173            mod_tokens.extend(quote! {
174                pub mod #mod_ident;
175            });
176        }
177
178        let syntax_tree = syn::parse2(mod_tokens).map_err(|e| CodegenError::Generation {
179            message: format!("Failed to parse generated mod tokens: {e}"),
180        })?;
181
182        let formatted_code = prettyplease::unparse(&syntax_tree);
183
184        fs::write(mod_file_path, formatted_code)?;
185
186        Ok(())
187    }
188
189    pub fn generate_combined_primitives_file<P: AsRef<Path>>(
190        &self,
191        primitive_structure_defs: &[StructureDefinition],
192        output_path: P,
193    ) -> CodegenResult<()> {
194        let mut all_tokens = proc_macro2::TokenStream::new();
195
196        let doc_comment = quote! {
197            //! FHIR Primitive Types
198            //!
199            //! This module contains type aliases for all FHIR primitive types.
200            //! Companion elements for primitive fields use the base Element type.
201        };
202        all_tokens.extend(doc_comment);
203
204        let mut type_cache = std::collections::HashMap::new();
205        let primitive_generator = PrimitiveGenerator::new(self.config, &mut type_cache);
206        let type_aliases =
207            primitive_generator.generate_all_primitive_type_aliases(primitive_structure_defs)?;
208
209        for type_alias in type_aliases {
210            let type_alias_tokens = self.token_generator.generate_type_alias(&type_alias);
211            all_tokens.extend(type_alias_tokens);
212        }
213
214        let syntax_tree = syn::parse2(all_tokens).map_err(|e| CodegenError::Generation {
215            message: format!("Failed to parse generated primitive tokens: {e}"),
216        })?;
217
218        let formatted_code = prettyplease::unparse(&syntax_tree);
219
220        fs::write(output_path, formatted_code)?;
221
222        Ok(())
223    }
224
225    pub fn generate_to_organized_directories<P: AsRef<Path>>(
226        &self,
227        structure_def: &StructureDefinition,
228        base_output_dir: P,
229        rust_struct: &RustStruct,
230        nested_structs: &[RustStruct],
231    ) -> CodegenResult<()> {
232        let base_dir = base_output_dir.as_ref();
233
234        let mut category = self.classify_fhir_structure_def(structure_def);
235        if category != FhirTypeCategory::Extension && Self::has_extension_base(rust_struct) {
236            category = FhirTypeCategory::Extension;
237        }
238
239        let target_dir = match category {
240            FhirTypeCategory::Resource => base_dir.join("src").join("resource"),
241            FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
242            FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
243            FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
244            FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
245        };
246
247        std::fs::create_dir_all(&target_dir)?;
248
249        let mut embedded_nested: Vec<RustStruct> = Vec::new();
250        let mut external_extensions: Vec<RustStruct> = Vec::new();
251
252        for nested in nested_structs {
253            if Self::has_extension_base(nested) {
254                external_extensions.push(nested.clone());
255            } else {
256                embedded_nested.push(nested.clone());
257            }
258        }
259
260        embedded_nested.sort_by(|left, right| left.name.cmp(&right.name));
261        external_extensions.sort_by(|left, right| left.name.cmp(&right.name));
262
263        let filename = crate::naming::Naming::filename(structure_def);
264        let output_path = target_dir.join(filename);
265
266        let result =
267            self.generate_to_file(structure_def, output_path, rust_struct, &embedded_nested);
268
269        if !external_extensions.is_empty() {
270            let extensions_dir = base_dir.join("src").join("extensions");
271            std::fs::create_dir_all(&extensions_dir)?;
272
273            for ext in external_extensions {
274                self.write_struct_only_file(&ext, &extensions_dir)?;
275            }
276        }
277
278        result
279    }
280
281    pub fn generate_trait_to_organized_directory<P: AsRef<Path>>(
282        &self,
283        structure_def: &StructureDefinition,
284        base_output_dir: P,
285        rust_trait: &RustTrait,
286    ) -> CodegenResult<()> {
287        let traits_dir = base_output_dir.as_ref().join("src").join("traits");
288
289        std::fs::create_dir_all(&traits_dir)?;
290
291        let struct_name = crate::naming::Naming::struct_name(structure_def);
292        let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
293        let filename = format!("{snake_case_name}.rs");
294        let output_path = traits_dir.join(filename);
295
296        self.generate_trait_to_file(structure_def, output_path, rust_trait)
297    }
298
299    fn has_extension_base(rust_struct: &RustStruct) -> bool {
300        rust_struct.fields.iter().any(|field| {
301            field.name == "base" && matches!(&field.field_type, crate::rust_types::RustType::Custom(type_name) if type_name == "Extension")
302        })
303    }
304
305    pub fn classify_fhir_structure_def(
306        &self,
307        structure_def: &StructureDefinition,
308    ) -> FhirTypeCategory {
309        if crate::generators::type_registry::TypeRegistry::is_profile(structure_def) {
310            return FhirTypeCategory::Profile;
311        }
312
313        if structure_def.kind == "primitive-type" {
314            return FhirTypeCategory::Primitive;
315        }
316
317        if crate::generators::utils::GeneratorUtils::is_fhir_datatype(&structure_def.name)
318            || structure_def.base_type == "Element"
319            || structure_def.base_type == "BackboneElement"
320            || structure_def.base_type == "DataType"
321            || structure_def.name == "Extension"
322        {
323            return FhirTypeCategory::DataType;
324        }
325
326        if structure_def.base_type == "Extension" {
327            return FhirTypeCategory::Extension;
328        }
329
330        if structure_def.kind == "resource"
331            || structure_def.base_type == "Resource"
332            || structure_def.base_type == "DomainResource"
333        {
334            return FhirTypeCategory::Resource;
335        }
336
337        if structure_def.kind == "complex-type" {
338            return FhirTypeCategory::DataType;
339        }
340
341        FhirTypeCategory::Resource
342    }
343
344    pub fn generate_to_file<P: AsRef<Path>>(
345        &self,
346        structure_def: &StructureDefinition,
347        output_path: P,
348        rust_struct: &RustStruct,
349        nested_structs: &[RustStruct],
350    ) -> CodegenResult<()> {
351        let mut imports = HashSet::new();
352
353        if self.config.with_serde && structure_def.kind != "primitive-type" {
354            imports.insert("serde::{Deserialize, Serialize}".to_string());
355        }
356
357        let has_macro_calls = rust_struct
358            .fields
359            .iter()
360            .any(|field| field.macro_call.is_some())
361            || nested_structs
362                .iter()
363                .any(|s| s.fields.iter().any(|field| field.macro_call.is_some()));
364
365        if has_macro_calls {
366            imports.insert("crate::{primitive_string, primitive_boolean, primitive_integer, primitive_decimal, primitive_datetime, primitive_date, primitive_time, primitive_uri, primitive_canonical, primitive_base64binary, primitive_instant, primitive_positiveint, primitive_unsignedint, primitive_id, primitive_oid, primitive_uuid, primitive_code, primitive_markdown, primitive_url}".to_string());
367        }
368
369        let mut all_tokens = proc_macro2::TokenStream::new();
370
371        if structure_def.kind == "primitive-type" {
372            let mut type_cache = std::collections::HashMap::new();
373            let primitive_generator = PrimitiveGenerator::new(self.config, &mut type_cache);
374            let type_alias = primitive_generator.generate_primitive_type_alias(structure_def)?;
375            let type_alias_tokens = self.token_generator.generate_type_alias(&type_alias);
376            all_tokens.extend(type_alias_tokens);
377        } else {
378            let mut all_structs = vec![rust_struct.clone()];
379            all_structs.extend(nested_structs.iter().cloned());
380
381            let structs_in_file: HashSet<String> =
382                all_structs.iter().map(|s| s.name.clone()).collect();
383
384            for struct_def in &all_structs {
385                ImportManager::collect_custom_types_from_struct(
386                    struct_def,
387                    &mut imports,
388                    &structs_in_file,
389                );
390            }
391
392            for struct_def in all_structs {
393                let struct_tokens = self.token_generator.generate_struct(&struct_def);
394                all_tokens.extend(struct_tokens);
395            }
396        }
397
398        let mut import_tokens = proc_macro2::TokenStream::new();
399        let mut sorted_imports: Vec<_> = imports.iter().collect();
400        sorted_imports.sort();
401        for import in sorted_imports {
402            let import_token: proc_macro2::TokenStream = format!("use {import};")
403                .parse()
404                .expect("codegen bug: invalid import statement in generated file imports");
405            import_tokens.extend(import_token);
406        }
407
408        let mut final_tokens = proc_macro2::TokenStream::new();
409        final_tokens.extend(import_tokens);
410        final_tokens.extend(all_tokens);
411
412        let syntax_tree = syn::parse2(final_tokens).map_err(|e| CodegenError::Generation {
413            message: format!("Failed to parse generated tokens: {e}"),
414        })?;
415
416        let mut formatted_code = prettyplease::unparse(&syntax_tree);
417
418        if structure_def.kind == "resource" || structure_def.kind == "complex-type" {
419            let default_impl = self.generate_default_implementation(structure_def, rust_struct);
420            if !default_impl.is_empty() {
421                formatted_code.push_str("\n\n");
422                formatted_code.push_str(&default_impl);
423            }
424
425            let mut sorted_nested_structs = nested_structs.to_vec();
426            sorted_nested_structs.sort_by(|left, right| left.name.cmp(&right.name));
427
428            for nested in &sorted_nested_structs {
429                let nested_default_impl =
430                    self.generate_nested_struct_default_implementation(structure_def, nested);
431                if !nested_default_impl.is_empty() {
432                    formatted_code.push_str("\n\n");
433                    formatted_code.push_str(&nested_default_impl);
434                }
435            }
436        }
437
438        if structure_def.kind == "resource" || structure_def.kind == "complex-type" {
439            let invariants_const =
440                crate::generators::InvariantGenerator::generate_invariants_constant(structure_def);
441            if !invariants_const.is_empty() {
442                formatted_code.push_str("\n\n");
443                formatted_code.push_str(&invariants_const);
444            }
445        }
446
447        if structure_def.kind == "resource" || structure_def.kind == "complex-type" {
448            let bindings_const = BindingGenerator::generate_bindings_constant(structure_def);
449            if !bindings_const.is_empty() {
450                formatted_code.push_str("\n\n");
451                formatted_code.push_str(&bindings_const);
452            }
453        }
454
455        if structure_def.kind == "resource" || structure_def.kind == "complex-type" {
456            let cardinalities_const =
457                crate::generators::cardinality_generator::CardinalityGenerator::generate_cardinalities_constant(
458                    structure_def,
459                );
460            if !cardinalities_const.is_empty() {
461                formatted_code.push_str("\n\n");
462                formatted_code.push_str(&cardinalities_const);
463            }
464        }
465
466        if structure_def.kind == "resource" {
467            formatted_code.push_str("\n\n");
468            formatted_code.push_str(&self.generate_trait_implementations(structure_def));
469        }
470
471        if structure_def.kind == "resource" || structure_def.kind == "complex-type" {
472            let validation_impl =
473                crate::generators::ValidationTraitGenerator::generate_trait_impl(structure_def);
474            if !validation_impl.is_empty() {
475                formatted_code.push_str("\n\n");
476                formatted_code.push_str(&validation_impl);
477            }
478        }
479
480        if structure_def.kind == "resource" {
481            formatted_code.push_str("\n\n");
482            formatted_code.push_str(&self.generate_trait_reexports(structure_def));
483        }
484
485        if structure_def.name == "Resource" {
486            formatted_code.push_str("\n\n");
487        }
488
489        if output_path.as_ref().exists() {
490            eprintln!(
491                "Warning: File '{}' already exists and will be overwritten. This may indicate a naming collision between FHIR StructureDefinitions.",
492                output_path.as_ref().display()
493            );
494        }
495
496        fs::write(output_path.as_ref(), formatted_code)?;
497
498        Ok(())
499    }
500
501    fn write_struct_only_file<P: AsRef<Path>>(
502        &self,
503        rust_struct: &RustStruct,
504        dir: P,
505    ) -> CodegenResult<()> {
506        let dir = dir.as_ref();
507
508        let mut imports = HashSet::new();
509        if self.config.with_serde {
510            imports.insert("serde::{Deserialize, Serialize}".to_string());
511        }
512
513        let mut structs_in_file = HashSet::new();
514        structs_in_file.insert(rust_struct.name.clone());
515        ImportManager::collect_custom_types_from_struct(
516            rust_struct,
517            &mut imports,
518            &structs_in_file,
519        );
520
521        let mut all_tokens = proc_macro2::TokenStream::new();
522
523        for import in &imports {
524            let import_token: proc_macro2::TokenStream = format!("use {import};")
525                .parse()
526                .expect("codegen bug: invalid import statement in struct-only file");
527            all_tokens.extend(import_token);
528        }
529
530        all_tokens.extend(self.token_generator.generate_struct(rust_struct));
531
532        let syntax_tree = syn::parse2(all_tokens).map_err(|e| CodegenError::Generation {
533            message: format!(
534                "Failed to parse generated tokens for {}: {e}",
535                rust_struct.name
536            ),
537        })?;
538
539        let formatted_code = prettyplease::unparse(&syntax_tree);
540
541        let filename = format!(
542            "{}.rs",
543            crate::naming::Naming::to_snake_case(&rust_struct.name)
544        );
545        let output_path = dir.join(filename);
546
547        std::fs::write(output_path, formatted_code)?;
548
549        Ok(())
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use crate::config::CodegenConfig;
557    use crate::generators::token_generator::TokenGenerator;
558    use std::fs;
559    use tempfile::TempDir;
560
561    #[test]
562    fn test_generate_macros_file() {
563        let temp_dir = TempDir::new().unwrap();
564        let macros_path = temp_dir.path().join("macros.rs");
565
566        let config = CodegenConfig::default();
567        let token_generator = TokenGenerator::new();
568        let file_generator = FileGenerator::new(&config, &token_generator);
569
570        file_generator.generate_macros_file(&macros_path).unwrap();
571
572        assert!(macros_path.exists());
573        let content = fs::read_to_string(&macros_path).unwrap();
574
575        assert!(content.contains("macro_rules! primitive_string"));
576        assert!(content.contains("macro_rules! primitive_boolean"));
577        assert!(content.contains("macro_rules! primitive_id"));
578    }
579
580    #[test]
581    fn test_generate_lib_file() {
582        let temp_dir = TempDir::new().unwrap();
583        let lib_path = temp_dir.path().join("lib.rs");
584
585        let config = CodegenConfig::default();
586        let token_generator = TokenGenerator::new();
587        let file_generator = FileGenerator::new(&config, &token_generator);
588
589        file_generator.generate_lib_file(&lib_path).unwrap();
590
591        assert!(lib_path.exists());
592        let content = fs::read_to_string(&lib_path).unwrap();
593
594        assert!(content.contains("pub mod macros;"));
595        assert!(content.contains("pub mod primitives;"));
596        assert!(content.contains("pub mod datatypes;"));
597        assert!(content.contains("pub mod resources;"));
598        assert!(content.contains("pub mod traits;"));
599        assert!(content.contains("pub mod bindings;"));
600
601        assert!(content.contains("pub use macros::*;"));
602        assert!(content.contains("pub use serde::{Deserialize, Serialize};"));
603
604        assert!(!content.contains("pub use primitives::*;"));
605        assert!(!content.contains("pub use datatypes::*;"));
606        assert!(!content.contains("pub use resource::*;"));
607        assert!(!content.contains("pub use traits::*;"));
608        assert!(!content.contains("pub use bindings::*;"));
609    }
610
611    #[test]
612    fn test_generate_complete_crate() {
613        let temp_dir = TempDir::new().unwrap();
614        let crate_path = temp_dir.path().join("test-crate");
615
616        let config = CodegenConfig::default();
617        let token_generator = TokenGenerator::new();
618        let file_generator = FileGenerator::new(&config, &token_generator);
619
620        file_generator
621            .generate_complete_crate(
622                &crate_path,
623                "test-crate",
624                &[], // Empty structure definitions
625            )
626            .unwrap();
627
628        assert!(crate_path.join("Cargo.toml").exists());
629        assert!(crate_path.join("src").is_dir());
630        assert!(crate_path.join("src/lib.rs").exists());
631        assert!(crate_path.join("src/macros.rs").exists());
632        assert!(crate_path.join("src/primitives").is_dir());
633        assert!(crate_path.join("src/primitives/mod.rs").exists());
634        assert!(crate_path.join("src/datatypes").is_dir());
635        assert!(crate_path.join("src/datatypes/mod.rs").exists());
636        assert!(crate_path.join("src/resource").is_dir());
637        assert!(crate_path.join("src/resource/mod.rs").exists());
638        assert!(crate_path.join("src/traits").is_dir());
639        assert!(crate_path.join("src/traits/mod.rs").exists());
640
641        let cargo_content = fs::read_to_string(crate_path.join("Cargo.toml")).unwrap();
642        assert!(cargo_content.contains("name = \"test-crate\""));
643        assert!(cargo_content.contains("edition = \"2021\""));
644        assert!(cargo_content.contains("serde"));
645        assert!(!cargo_content.contains("paste"));
646    }
647}