Skip to main content

rh_codegen/
lib.rs

1//! FHIR code generation library
2//!
3//! This library provides functionality for generating Rust types from FHIR (Fast Healthcare
4//! Interoperability Resources) StructureDefinition files.
5//!
6//! ## Macro Call Generation
7//!
8//! The library can generate macro calls for FHIR primitive types instead of regular struct fields.
9//! This provides better handling of FHIR's primitive extension pattern where each primitive field
10//! can have an associated extension element.
11//!
12//! To enable macro call generation, set `use_macro_calls: true` in your `CodegenConfig`:
13//!
14//! ```rust
15//! use rh_codegen::CodegenConfig;
16//!
17//! let mut config = CodegenConfig::default();
18//! config.use_macro_calls = true;
19//! ```
20//!
21/// When enabled, primitive fields like `boolean`, `string`, `integer`, etc. will be generated as
22/// macro calls such as `primitive_boolean!("active", true)` instead of regular struct fields.
23/// These macros automatically generate both the primitive field and its companion extension field.
24pub use rh_foundation::{Config, FoundationError};
25
26pub mod bindings;
27mod config;
28pub mod fhir_types;
29mod generator;
30pub mod generators;
31pub mod invariants;
32pub mod macros;
33pub mod metadata;
34pub mod naming;
35pub mod quality;
36mod rust_types;
37mod type_mapper;
38pub mod value_sets;
39
40// Re-export loader types for convenience
41pub use rh_foundation::loader::{
42    LoaderConfig as PackageDownloadConfig, LoaderError, LoaderResult, PackageDist,
43    PackageLoader as PackageDownloader, PackageManifest,
44};
45
46// Re-export modular code generation types
47pub use config::CodegenConfig;
48pub use fhir_types::StructureDefinition;
49pub use generator::CodeGenerator;
50pub use generators::crate_generator::{
51    generate_crate_structure, generate_module_files, parse_package_metadata, CrateGenerationParams,
52};
53pub use generators::file_generator::{FhirTypeCategory, FileGenerator};
54pub use generators::token_generator::TokenGenerator;
55pub use generators::utils::GeneratorUtils;
56pub use naming::Naming;
57pub use quality::{format_generated_crate, QualityConfig};
58pub use rust_types::{RustEnum, RustStruct, RustTrait, RustTraitMethod, RustType};
59pub use type_mapper::TypeMapper;
60pub use value_sets::{ValueSetConcept, ValueSetManager};
61
62/// Errors specific to code generation.
63///
64/// This error type extends FoundationError with domain-specific
65/// error variants for FHIR code generation.
66#[derive(thiserror::Error, Debug)]
67pub enum CodegenError {
68    /// Invalid FHIR type encountered
69    #[error("Invalid FHIR type: {fhir_type}")]
70    InvalidFhirType { fhir_type: String },
71
72    /// Missing required field in structure definition
73    #[error("Missing required field: {field}")]
74    MissingField { field: String },
75
76    /// General code generation failure
77    #[error("Code generation failed: {message}")]
78    Generation { message: String },
79
80    /// Package loader error
81    #[error(transparent)]
82    Loader(#[from] rh_foundation::loader::LoaderError),
83
84    /// Foundation error (covers IO, JSON, etc.)
85    #[error(transparent)]
86    Foundation(#[from] FoundationError),
87}
88
89// Implement From for common types that should go through FoundationError
90impl From<std::io::Error> for CodegenError {
91    fn from(err: std::io::Error) -> Self {
92        CodegenError::Foundation(FoundationError::Io(err))
93    }
94}
95
96impl From<serde_json::Error> for CodegenError {
97    fn from(err: serde_json::Error) -> Self {
98        CodegenError::Foundation(FoundationError::Serialization(err))
99    }
100}
101
102/// Result type for codegen operations
103pub type CodegenResult<T> = std::result::Result<T, CodegenError>;
104
105/// Generate organized directory structure with traits for resources
106pub fn generate_organized_directories_with_traits<P: AsRef<std::path::Path>>(
107    generator: &mut CodeGenerator,
108    structure_def: &StructureDefinition,
109    base_output_dir: P,
110) -> CodegenResult<()> {
111    // Skip if status is retired
112    if structure_def.status == "retired" {
113        return Err(CodegenError::Generation {
114            message: format!("Skipping retired StructureDefinition '{name}' - retired StructureDefinitions are not generated as Rust types", name = structure_def.name)
115        });
116    }
117
118    // First generate the main structure
119    generator.generate_to_organized_directories(structure_def, &base_output_dir)?;
120
121    // Then generate trait if this is a resource or profile
122    let category = generator.classify_fhir_structure_def(structure_def);
123    if category == FhirTypeCategory::Resource || category == FhirTypeCategory::Profile {
124        generate_resource_trait_for_structure(generator, structure_def, &base_output_dir)?;
125    }
126
127    Ok(())
128}
129
130/// Generate Resource trait for a specific structure definition
131pub fn generate_resource_trait_for_structure<P: AsRef<std::path::Path>>(
132    generator: &mut CodeGenerator,
133    structure_def: &StructureDefinition,
134    base_output_dir: P,
135) -> CodegenResult<()> {
136    let traits_dir = base_output_dir.as_ref().join("src").join("traits");
137    std::fs::create_dir_all(&traits_dir)?;
138
139    // Generate both the generic Resource trait and the specific resource trait
140
141    // 1. Generate the generic Resource trait
142    // let generic_trait_file = traits_dir.join("resource.rs");
143    // match generator.generate_resource_trait() {
144    //     Ok(trait_content) => {
145    //         std::fs::write(&generic_trait_file, trait_content)?;
146    //     }
147    //     Err(e) => return Err(e),
148    // }
149
150    // 2. Generate the specific resource trait with choice type methods
151    // For profiles, use the struct name to ensure consistency with trait implementation references
152    // For regular resources, use the name field which may have spaces/special chars
153    let is_profile = crate::generators::type_registry::TypeRegistry::is_profile(structure_def);
154    let trait_filename = if is_profile {
155        let struct_name = crate::naming::Naming::struct_name(structure_def);
156        crate::naming::Naming::to_snake_case(&struct_name)
157    } else {
158        crate::naming::Naming::trait_module_name(&structure_def.name)
159    };
160
161    let specific_trait_file = traits_dir.join(format!("{trait_filename}.rs"));
162    match generator.generate_trait_to_file(structure_def, &specific_trait_file) {
163        Ok(()) => {
164            // Update traits/mod.rs to include both traits
165            update_traits_mod_file(&traits_dir, &trait_filename)?;
166        }
167        Err(e) => return Err(e),
168    }
169
170    Ok(())
171}
172
173/// Update the traits/mod.rs file to include the resource module
174fn update_traits_mod_file(traits_dir: &std::path::Path, resource_name: &str) -> CodegenResult<()> {
175    let mod_file = traits_dir.join("mod.rs");
176    if !mod_file.exists() {
177        // Create initial mod.rs with both generic resource and specific resource modules
178        let mod_content = format!(
179            "//! FHIR traits for common functionality
180//!
181//! This module contains traits that define common interfaces for FHIR types.
182
183pub mod resource;
184pub mod {resource_name};
185"
186        );
187        std::fs::write(&mod_file, mod_content)?;
188    } else {
189        // Check if mod.rs already contains the module declarations
190        let existing_content = std::fs::read_to_string(&mod_file)?;
191        let mut new_content = existing_content.clone();
192
193        if !existing_content.contains("pub mod resource;") {
194            new_content = new_content.trim_end().to_string() + "\npub mod resource;\n";
195        }
196
197        let specific_module_line = format!("pub mod {resource_name};");
198        if !existing_content.contains(&specific_module_line) {
199            new_content =
200                new_content.trim_end().to_string() + &format!("\npub mod {resource_name};\n");
201        }
202
203        // Only write if content changed
204        if new_content != existing_content {
205            std::fs::write(&mod_file, new_content)?;
206        }
207    }
208
209    Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::{CodeGenerator, CodegenConfig};
216    use tempfile::TempDir;
217
218    #[test]
219    fn test_skip_retired_structure_definition() {
220        let mut generator = CodeGenerator::new(CodegenConfig::default());
221        let temp_dir = TempDir::new().unwrap();
222
223        // Create a mock retired StructureDefinition
224        let retired_structure_def = StructureDefinition {
225            resource_type: "StructureDefinition".to_string(),
226            id: "test-retired".to_string(),
227            url: "http://example.org/StructureDefinition/TestRetired".to_string(),
228            version: Some("1.0.0".to_string()),
229            name: "TestRetired".to_string(),
230            title: Some("Test Retired Structure".to_string()),
231            status: "retired".to_string(), // This is the key field
232            description: Some("A retired test structure".to_string()),
233            purpose: None,
234            kind: "resource".to_string(),
235            is_abstract: false,
236            base_type: "DomainResource".to_string(),
237            base_definition: Some(
238                "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
239            ),
240            differential: None,
241            snapshot: None,
242        };
243
244        // Attempt to generate - should return an error about retired status
245        let result = generate_organized_directories_with_traits(
246            &mut generator,
247            &retired_structure_def,
248            temp_dir.path(),
249        );
250
251        assert!(result.is_err());
252        let error_message = result.unwrap_err().to_string();
253        assert!(error_message.contains("retired"));
254        assert!(error_message.contains("TestRetired"));
255    }
256
257    #[test]
258    fn test_process_active_structure_definition() {
259        let mut generator = CodeGenerator::new(CodegenConfig::default());
260        let temp_dir = TempDir::new().unwrap();
261
262        // Create a mock active StructureDefinition
263        let active_structure_def = StructureDefinition {
264            resource_type: "StructureDefinition".to_string(),
265            id: "test-active".to_string(),
266            url: "http://example.org/StructureDefinition/TestActive".to_string(),
267            version: Some("1.0.0".to_string()),
268            name: "TestActive".to_string(),
269            title: Some("Test Active Structure".to_string()),
270            status: "active".to_string(), // This should allow processing
271            description: Some("An active test structure".to_string()),
272            purpose: None,
273            kind: "resource".to_string(),
274            is_abstract: false,
275            base_type: "DomainResource".to_string(),
276            base_definition: Some(
277                "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
278            ),
279            differential: None,
280            snapshot: None,
281        };
282
283        // Attempt to generate - should succeed (even though it might fail later due to missing data)
284        let result = generate_organized_directories_with_traits(
285            &mut generator,
286            &active_structure_def,
287            temp_dir.path(),
288        );
289
290        // We expect this to either succeed or fail for a different reason (not retired status)
291        if let Err(error) = result {
292            let error_message = error.to_string();
293            // Should not be failing due to retired status
294            assert!(!error_message.contains("retired"));
295        }
296    }
297}