1use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10use crate::config::CodegenConfig;
11use crate::fhir_types::StructureDefinition;
12use crate::generators::{FileGenerator, TokenGenerator};
13use crate::rust_types::{RustStruct, RustTrait};
14use crate::CodegenResult;
15
16pub use crate::generators::file_generator::FhirTypeCategory;
18
19pub struct FileIoManager<'a> {
21 config: &'a CodegenConfig,
22 token_generator: &'a TokenGenerator,
23}
24
25impl<'a> FileIoManager<'a> {
26 pub fn new(config: &'a CodegenConfig, token_generator: &'a TokenGenerator) -> Self {
28 Self {
29 config,
30 token_generator,
31 }
32 }
33
34 pub fn load_structure_definition<P: AsRef<Path>>(
36 path: P,
37 ) -> CodegenResult<StructureDefinition> {
38 let content = fs::read_to_string(&path)?;
39
40 let structure_def: StructureDefinition = serde_json::from_str(&content)?;
41
42 Ok(structure_def)
43 }
44
45 pub fn generate_to_organized_directories<P: AsRef<Path>>(
47 &self,
48 structure_def: &StructureDefinition,
49 base_output_dir: P,
50 rust_struct: &RustStruct,
51 nested_structs: &[RustStruct],
52 ) -> CodegenResult<()> {
53 let base_dir = base_output_dir.as_ref();
54
55 let target_dir = match self.classify_fhir_structure_def(structure_def) {
57 FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
58 FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
59 FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
60 FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
61 FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
62 };
63
64 std::fs::create_dir_all(&target_dir)?;
66
67 let filename = crate::naming::Naming::filename(structure_def);
69 let output_path = target_dir.join(filename);
70
71 self.generate_to_file(structure_def, output_path, rust_struct, nested_structs)
72 }
73
74 pub fn generate_traits_to_organized_directory<P: AsRef<Path>>(
76 &self,
77 structure_def: &StructureDefinition,
78 base_output_dir: P,
79 rust_traits: &[RustTrait],
80 ) -> CodegenResult<()> {
81 let traits_dir = base_output_dir.as_ref().join("src").join("traits");
82
83 std::fs::create_dir_all(&traits_dir)?;
85
86 let struct_name = crate::naming::Naming::struct_name(structure_def);
88 let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
89 let filename = format!("{snake_case_name}.rs");
90 let output_path = traits_dir.join(filename);
91
92 let rust_trait_refs: Vec<&RustTrait> = rust_traits.iter().collect();
94 let file_generator = FileGenerator::new(self.config, self.token_generator);
95 file_generator.generate_traits_to_file(structure_def, output_path, &rust_trait_refs)
96 }
97
98 pub fn generate_trait_to_organized_directory<P: AsRef<Path>>(
100 &self,
101 structure_def: &StructureDefinition,
102 base_output_dir: P,
103 rust_trait: &RustTrait,
104 ) -> CodegenResult<()> {
105 let traits_dir = base_output_dir.as_ref().join("src").join("traits");
106
107 std::fs::create_dir_all(&traits_dir)?;
109
110 let struct_name = crate::naming::Naming::struct_name(structure_def);
112 let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
113 let filename = format!("{snake_case_name}.rs");
114 let output_path = traits_dir.join(filename);
115
116 self.generate_trait_to_file(structure_def, output_path, rust_trait)
117 }
118
119 pub fn generate_to_file<P: AsRef<Path>>(
121 &self,
122 structure_def: &StructureDefinition,
123 output_path: P,
124 rust_struct: &RustStruct,
125 nested_structs: &[RustStruct],
126 ) -> CodegenResult<()> {
127 let file_generator = FileGenerator::new(self.config, self.token_generator);
128 file_generator.generate_to_file(structure_def, output_path, rust_struct, nested_structs)
129 }
130
131 pub fn generate_traits_to_file<P: AsRef<Path>>(
133 &self,
134 structure_def: &StructureDefinition,
135 output_path: P,
136 rust_traits: &[RustTrait],
137 ) -> CodegenResult<()> {
138 let file_generator = FileGenerator::new(self.config, self.token_generator);
139 let rust_trait_refs: Vec<&RustTrait> = rust_traits.iter().collect();
141 file_generator.generate_traits_to_file(structure_def, output_path, &rust_trait_refs)
142 }
143
144 pub fn generate_trait_to_file<P: AsRef<Path>>(
146 &self,
147 structure_def: &StructureDefinition,
148 output_path: P,
149 rust_trait: &RustTrait,
150 ) -> CodegenResult<()> {
151 let file_generator = FileGenerator::new(self.config, self.token_generator);
152 file_generator.generate_trait_to_file(structure_def, output_path, rust_trait)
153 }
154
155 pub fn generate_trait_file_from_trait<P: AsRef<Path>>(
157 &self,
158 rust_trait: &RustTrait,
159 output_path: P,
160 ) -> CodegenResult<()> {
161 let file_generator = FileGenerator::new(self.config, self.token_generator);
162 file_generator.generate_trait_file_from_trait(rust_trait, output_path)
163 }
164
165 fn is_direct_nested_struct(parent_name: &str, cached_name: &str) -> bool {
168 if let Some(classification) =
171 crate::generators::type_registry::TypeRegistry::get_classification(cached_name)
172 {
173 match classification {
174 crate::generators::type_registry::TypeClassification::NestedStructure {
175 parent_resource,
176 } => {
177 return parent_resource == parent_name;
179 }
180 _ => {
181 return false;
184 }
185 }
186 }
187
188 if !cached_name.starts_with(parent_name) {
193 return false;
194 }
195
196 if cached_name.len() <= parent_name.len() {
198 return false;
199 }
200
201 let remainder = &cached_name[parent_name.len()..];
205
206 if let Some(first_char) = remainder.chars().next() {
214 if first_char.is_lowercase() {
215 return false;
216 }
217 }
218
219 let separate_resource_suffixes = [
221 "Definition",
222 "Specification",
223 "Polymer",
224 "Protein",
225 "ReferenceInformation",
226 "SourceMaterial",
227 "NucleicAcid",
228 "Authorization",
229 "Contraindication",
230 "Indication",
231 "Ingredient",
232 "Interaction",
233 "Manufactured",
234 "Packaged",
235 "Pharmaceutical",
236 "UndesirableEffect",
237 "Knowledge",
238 "Administration",
239 "Dispense",
240 "Request",
241 "Statement",
242 ];
243
244 if separate_resource_suffixes.contains(&remainder) {
245 return false;
246 }
247
248 if separate_resource_suffixes
250 .iter()
251 .any(|&suffix| remainder.starts_with(suffix))
252 {
253 return false;
254 }
255
256 true
257 }
258
259 pub fn collect_nested_structs(
261 struct_name: &str,
262 type_cache: &HashMap<String, RustStruct>,
263 ) -> Vec<RustStruct> {
264 let mut nested_structs = vec![];
265
266 for (cached_name, cached_struct) in type_cache {
269 if cached_name != struct_name && Self::is_direct_nested_struct(struct_name, cached_name)
270 {
271 nested_structs.push(cached_struct.clone());
272 }
273 }
274
275 nested_structs
276 }
277
278 pub fn classify_fhir_structure_def(
280 &self,
281 structure_def: &StructureDefinition,
282 ) -> FhirTypeCategory {
283 let file_generator = FileGenerator::new(self.config, self.token_generator);
284 file_generator.classify_fhir_structure_def(structure_def)
285 }
286
287 pub fn ensure_directory(dir_path: &Path) -> CodegenResult<()> {
289 std::fs::create_dir_all(dir_path)?;
290 Ok(())
291 }
292
293 pub fn get_output_path_for_structure<P: AsRef<Path>>(
295 base_dir: P,
296 structure_def: &StructureDefinition,
297 file_generator: &FileGenerator,
298 ) -> std::path::PathBuf {
299 let base_dir = base_dir.as_ref();
300
301 let target_dir = match file_generator.classify_fhir_structure_def(structure_def) {
303 FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
304 FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
305 FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
306 FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
307 FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
308 };
309
310 let filename = crate::naming::Naming::filename(structure_def);
311 target_dir.join(filename)
312 }
313
314 pub fn get_traits_directory_path<P: AsRef<Path>>(base_dir: P) -> std::path::PathBuf {
316 base_dir.as_ref().join("src").join("traits")
317 }
318
319 pub fn get_trait_file_path<P: AsRef<Path>>(
321 base_dir: P,
322 structure_def: &StructureDefinition,
323 ) -> std::path::PathBuf {
324 let traits_dir = Self::get_traits_directory_path(base_dir);
325 let struct_name = crate::naming::Naming::struct_name(structure_def);
326 let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
327 let filename = format!("{snake_case_name}.rs");
328 traits_dir.join(filename)
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::config::CodegenConfig;
336 use crate::generators::TokenGenerator;
337 use tempfile::TempDir;
338
339 #[test]
340 fn test_load_structure_definition() {
341 use std::fs;
342
343 let temp_dir = TempDir::new().unwrap();
344 let file_path = temp_dir.path().join("test_structure.json");
345
346 let json_content = r#"{
348 "resourceType": "StructureDefinition",
349 "id": "Patient",
350 "url": "http://hl7.org/fhir/StructureDefinition/Patient",
351 "name": "Patient",
352 "title": "Patient",
353 "status": "active",
354 "kind": "resource",
355 "abstract": false,
356 "type": "Patient",
357 "description": "A patient resource",
358 "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
359 }"#;
360
361 fs::write(&file_path, json_content).unwrap();
362
363 let result = FileIoManager::load_structure_definition(&file_path);
365 assert!(
366 result.is_ok(),
367 "Should load StructureDefinition successfully"
368 );
369
370 let loaded_structure = result.unwrap();
371 assert_eq!(loaded_structure.name, "Patient");
372 assert_eq!(loaded_structure.kind, "resource");
373 }
374
375 #[test]
376 fn test_load_structure_definition_invalid_json() {
377 let temp_dir = TempDir::new().unwrap();
378 let file_path = temp_dir.path().join("invalid.json");
379
380 fs::write(&file_path, "{ invalid json }").unwrap();
382
383 let result = FileIoManager::load_structure_definition(&file_path);
384 assert!(result.is_err(), "Should fail to load invalid JSON");
385 }
386
387 #[test]
388 fn test_load_structure_definition_missing_file() {
389 let temp_dir = TempDir::new().unwrap();
390 let file_path = temp_dir.path().join("missing.json");
391
392 let result = FileIoManager::load_structure_definition(&file_path);
393 assert!(result.is_err(), "Should fail to load missing file");
394 }
395
396 #[test]
397 fn test_collect_nested_structs() {
398 let mut type_cache = HashMap::new();
399
400 let patient_struct = RustStruct::new("Patient".to_string());
402 type_cache.insert("Patient".to_string(), patient_struct);
403
404 let patient_contact_struct = RustStruct::new("PatientContact".to_string());
406 type_cache.insert("PatientContact".to_string(), patient_contact_struct);
407
408 let patient_link_struct = RustStruct::new("PatientLink".to_string());
409 type_cache.insert("PatientLink".to_string(), patient_link_struct);
410
411 let observation_struct = RustStruct::new("Observation".to_string());
413 type_cache.insert("Observation".to_string(), observation_struct);
414
415 let nested_structs = FileIoManager::collect_nested_structs("Patient", &type_cache);
416
417 assert_eq!(
418 nested_structs.len(),
419 2,
420 "Should collect exactly 2 nested structs"
421 );
422
423 let nested_names: Vec<String> = nested_structs.iter().map(|s| s.name.clone()).collect();
424 assert!(nested_names.contains(&"PatientContact".to_string()));
425 assert!(nested_names.contains(&"PatientLink".to_string()));
426 assert!(!nested_names.contains(&"Patient".to_string())); assert!(!nested_names.contains(&"Observation".to_string())); }
429
430 #[test]
431 fn test_ensure_directory_exists() {
432 let temp_dir = TempDir::new().unwrap();
433 let new_dir = temp_dir.path().join("nested").join("directory");
434
435 assert!(!new_dir.exists());
437
438 let result = FileIoManager::ensure_directory(&new_dir);
439 assert!(result.is_ok(), "Should create directory successfully");
440
441 assert!(new_dir.exists());
443 assert!(new_dir.is_dir());
444 }
445
446 #[test]
447 fn test_get_output_path_for_structure() {
448 let config = CodegenConfig::default();
449 let token_generator = TokenGenerator::new();
450 let file_generator = FileGenerator::new(&config, &token_generator);
451
452 let temp_dir = TempDir::new().unwrap();
453
454 let json_content = r#"{
456 "resourceType": "StructureDefinition",
457 "id": "Patient",
458 "url": "http://hl7.org/fhir/StructureDefinition/Patient",
459 "name": "Patient",
460 "title": "Patient",
461 "status": "active",
462 "kind": "resource",
463 "abstract": false,
464 "type": "Patient",
465 "description": "A patient resource",
466 "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
467 }"#;
468
469 let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
471
472 let output_path = FileIoManager::get_output_path_for_structure(
473 temp_dir.path(),
474 &patient_structure,
475 &file_generator,
476 );
477
478 let expected_path = temp_dir
479 .path()
480 .join("src")
481 .join("resources")
482 .join("patient.rs");
483 assert_eq!(output_path, expected_path);
484 }
485
486 #[test]
487 fn test_get_trait_file_path() {
488 let temp_dir = TempDir::new().unwrap();
489
490 let json_content = r#"{
492 "resourceType": "StructureDefinition",
493 "id": "Patient",
494 "url": "http://hl7.org/fhir/StructureDefinition/Patient",
495 "name": "Patient",
496 "title": "Patient",
497 "status": "active",
498 "kind": "resource",
499 "abstract": false,
500 "type": "Patient",
501 "description": "A patient resource",
502 "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
503 }"#;
504
505 let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
507
508 let trait_path = FileIoManager::get_trait_file_path(temp_dir.path(), &patient_structure);
509 let expected_path = temp_dir
510 .path()
511 .join("src")
512 .join("traits")
513 .join("patient.rs");
514 assert_eq!(trait_path, expected_path);
515 }
516
517 #[test]
518 fn test_file_io_manager_creation() {
519 let config = CodegenConfig::default();
520 let token_generator = TokenGenerator::new();
521 let file_io_manager = FileIoManager::new(&config, &token_generator);
522
523 assert_eq!(
527 std::mem::size_of_val(&file_io_manager),
528 std::mem::size_of::<FileIoManager>()
529 );
530 }
531
532 #[test]
533 fn test_nested_struct_collection_fix() {
534 use crate::rust_types::RustStruct;
535 use std::collections::HashMap;
536
537 let mut type_cache = HashMap::new();
538
539 let element_struct = RustStruct::new("Element".to_string());
541 let element_definition_struct = RustStruct::new("ElementDefinition".to_string());
542
543 let element_extension_struct = RustStruct::new("ElementExtension".to_string());
545 let element_binding_struct = RustStruct::new("ElementBinding".to_string());
546
547 let element_definition_binding_struct =
549 RustStruct::new("ElementDefinitionBinding".to_string());
550 let element_definition_constraint_struct =
551 RustStruct::new("ElementDefinitionConstraint".to_string());
552 let element_definition_type_struct = RustStruct::new("ElementDefinitionType".to_string());
553
554 type_cache.insert("Element".to_string(), element_struct);
556 type_cache.insert("ElementDefinition".to_string(), element_definition_struct);
557 type_cache.insert("ElementExtension".to_string(), element_extension_struct);
558 type_cache.insert("ElementBinding".to_string(), element_binding_struct);
559 type_cache.insert(
560 "ElementDefinitionBinding".to_string(),
561 element_definition_binding_struct,
562 );
563 type_cache.insert(
564 "ElementDefinitionConstraint".to_string(),
565 element_definition_constraint_struct,
566 );
567 type_cache.insert(
568 "ElementDefinitionType".to_string(),
569 element_definition_type_struct,
570 );
571
572 let elementdefinition_binding_struct =
574 RustStruct::new("ElementdefinitionBinding".to_string());
575 type_cache.insert(
576 "ElementdefinitionBinding".to_string(),
577 elementdefinition_binding_struct,
578 );
579 let elementdefinition_constraint_struct =
580 RustStruct::new("ElementdefinitionConstraint".to_string());
581 type_cache.insert(
582 "ElementdefinitionConstraint".to_string(),
583 elementdefinition_constraint_struct,
584 );
585 let elementdefinition_type_struct = RustStruct::new("ElementdefinitionType".to_string());
586 type_cache.insert(
587 "ElementdefinitionType".to_string(),
588 elementdefinition_type_struct,
589 );
590
591 let element_nested = FileIoManager::collect_nested_structs("Element", &type_cache);
593
594 assert_eq!(
597 element_nested.len(),
598 2,
599 "Element should have 2 nested structs"
600 );
601
602 let element_nested_names: Vec<String> =
603 element_nested.iter().map(|s| s.name.clone()).collect();
604 assert!(element_nested_names.contains(&"ElementExtension".to_string()));
605 assert!(element_nested_names.contains(&"ElementBinding".to_string()));
606 assert!(!element_nested_names.contains(&"ElementDefinitionBinding".to_string()));
607 assert!(!element_nested_names.contains(&"ElementDefinitionConstraint".to_string()));
608 assert!(!element_nested_names.contains(&"ElementDefinitionType".to_string()));
609 assert!(!element_nested_names.contains(&"ElementDefinition".to_string()));
610 assert!(!element_nested_names.contains(&"ElementdefinitionBinding".to_string()));
612 assert!(!element_nested_names.contains(&"ElementdefinitionConstraint".to_string()));
613 assert!(!element_nested_names.contains(&"ElementdefinitionType".to_string()));
614
615 let element_definition_nested =
617 FileIoManager::collect_nested_structs("ElementDefinition", &type_cache);
618
619 assert_eq!(
622 element_definition_nested.len(),
623 3,
624 "ElementDefinition should have 3 nested structs (only uppercase)"
625 );
626
627 let element_definition_nested_names: Vec<String> = element_definition_nested
628 .iter()
629 .map(|s| s.name.clone())
630 .collect();
631 assert!(element_definition_nested_names.contains(&"ElementDefinitionBinding".to_string()));
632 assert!(
633 element_definition_nested_names.contains(&"ElementDefinitionConstraint".to_string())
634 );
635 assert!(element_definition_nested_names.contains(&"ElementDefinitionType".to_string()));
636 assert!(!element_definition_nested_names.contains(&"ElementdefinitionBinding".to_string()));
638 assert!(
639 !element_definition_nested_names.contains(&"ElementdefinitionConstraint".to_string())
640 );
641 assert!(!element_definition_nested_names.contains(&"ElementdefinitionType".to_string()));
642 assert!(!element_definition_nested_names.contains(&"ElementExtension".to_string()));
643 assert!(!element_definition_nested_names.contains(&"ElementBinding".to_string()));
644
645 type_cache.insert("Bundle".to_string(), RustStruct::new("Bundle".to_string()));
649 type_cache.insert(
650 "BundleEntry".to_string(),
651 RustStruct::new("BundleEntry".to_string()),
652 );
653 type_cache.insert(
654 "BundleLink".to_string(),
655 RustStruct::new("BundleLink".to_string()),
656 );
657
658 let bundle_nested = FileIoManager::collect_nested_structs("Bundle", &type_cache);
659 assert_eq!(
660 bundle_nested.len(),
661 2,
662 "Bundle should have 2 nested structs"
663 );
664
665 let bundle_nested_names: Vec<String> =
666 bundle_nested.iter().map(|s| s.name.clone()).collect();
667 assert!(bundle_nested_names.contains(&"BundleEntry".to_string()));
668 assert!(bundle_nested_names.contains(&"BundleLink".to_string()));
669 }
670}