1pub 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
40pub use rh_foundation::loader::{
42 LoaderConfig as PackageDownloadConfig, LoaderError, LoaderResult, PackageDist,
43 PackageLoader as PackageDownloader, PackageManifest,
44};
45
46pub 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#[derive(thiserror::Error, Debug)]
67pub enum CodegenError {
68 #[error("Invalid FHIR type: {fhir_type}")]
70 InvalidFhirType { fhir_type: String },
71
72 #[error("Missing required field: {field}")]
74 MissingField { field: String },
75
76 #[error("Code generation failed: {message}")]
78 Generation { message: String },
79
80 #[error(transparent)]
82 Loader(#[from] rh_foundation::loader::LoaderError),
83
84 #[error(transparent)]
86 Foundation(#[from] FoundationError),
87}
88
89impl 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
102pub type CodegenResult<T> = std::result::Result<T, CodegenError>;
104
105pub 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 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 generator.generate_to_organized_directories(structure_def, &base_output_dir)?;
120
121 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
130pub 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 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_file(&traits_dir, &trait_filename)?;
166 }
167 Err(e) => return Err(e),
168 }
169
170 Ok(())
171}
172
173fn 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 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 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 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 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(), 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 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 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(), 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 let result = generate_organized_directories_with_traits(
285 &mut generator,
286 &active_structure_def,
287 temp_dir.path(),
288 );
289
290 if let Err(error) = result {
292 let error_message = error.to_string();
293 assert!(!error_message.contains("retired"));
295 }
296 }
297}