rh_codegen/generators/file_generator/
mod.rs1mod 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 #![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 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 };
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(¯os_path).unwrap();
571
572 assert!(macros_path.exists());
573 let content = fs::read_to_string(¯os_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 &[], )
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}