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.sort_by(|left, right| left.name.cmp(&right.name));
276 nested_structs
277 }
278
279 pub fn classify_fhir_structure_def(
281 &self,
282 structure_def: &StructureDefinition,
283 ) -> FhirTypeCategory {
284 let file_generator = FileGenerator::new(self.config, self.token_generator);
285 file_generator.classify_fhir_structure_def(structure_def)
286 }
287
288 pub fn ensure_directory(dir_path: &Path) -> CodegenResult<()> {
290 std::fs::create_dir_all(dir_path)?;
291 Ok(())
292 }
293
294 pub fn get_output_path_for_structure<P: AsRef<Path>>(
296 base_dir: P,
297 structure_def: &StructureDefinition,
298 file_generator: &FileGenerator,
299 ) -> std::path::PathBuf {
300 let base_dir = base_dir.as_ref();
301
302 let target_dir = match file_generator.classify_fhir_structure_def(structure_def) {
304 FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
305 FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
306 FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
307 FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
308 FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
309 };
310
311 let filename = crate::naming::Naming::filename(structure_def);
312 target_dir.join(filename)
313 }
314
315 pub fn get_traits_directory_path<P: AsRef<Path>>(base_dir: P) -> std::path::PathBuf {
317 base_dir.as_ref().join("src").join("traits")
318 }
319
320 pub fn get_trait_file_path<P: AsRef<Path>>(
322 base_dir: P,
323 structure_def: &StructureDefinition,
324 ) -> std::path::PathBuf {
325 let traits_dir = Self::get_traits_directory_path(base_dir);
326 let struct_name = crate::naming::Naming::struct_name(structure_def);
327 let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
328 let filename = format!("{snake_case_name}.rs");
329 traits_dir.join(filename)
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::config::CodegenConfig;
337 use crate::generators::TokenGenerator;
338 use tempfile::TempDir;
339
340 #[test]
341 fn test_load_structure_definition() {
342 use std::fs;
343
344 let temp_dir = TempDir::new().unwrap();
345 let file_path = temp_dir.path().join("test_structure.json");
346
347 let json_content = r#"{
349 "resourceType": "StructureDefinition",
350 "id": "Patient",
351 "url": "http://hl7.org/fhir/StructureDefinition/Patient",
352 "name": "Patient",
353 "title": "Patient",
354 "status": "active",
355 "kind": "resource",
356 "abstract": false,
357 "type": "Patient",
358 "description": "A patient resource",
359 "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
360 }"#;
361
362 fs::write(&file_path, json_content).unwrap();
363
364 let result = FileIoManager::load_structure_definition(&file_path);
366 assert!(
367 result.is_ok(),
368 "Should load StructureDefinition successfully"
369 );
370
371 let loaded_structure = result.unwrap();
372 assert_eq!(loaded_structure.name, "Patient");
373 assert_eq!(loaded_structure.kind, "resource");
374 }
375
376 #[test]
377 fn test_load_structure_definition_invalid_json() {
378 let temp_dir = TempDir::new().unwrap();
379 let file_path = temp_dir.path().join("invalid.json");
380
381 fs::write(&file_path, "{ invalid json }").unwrap();
383
384 let result = FileIoManager::load_structure_definition(&file_path);
385 assert!(result.is_err(), "Should fail to load invalid JSON");
386 }
387
388 #[test]
389 fn test_load_structure_definition_missing_file() {
390 let temp_dir = TempDir::new().unwrap();
391 let file_path = temp_dir.path().join("missing.json");
392
393 let result = FileIoManager::load_structure_definition(&file_path);
394 assert!(result.is_err(), "Should fail to load missing file");
395 }
396
397 #[test]
398 fn test_collect_nested_structs() {
399 let mut type_cache = HashMap::new();
400
401 let patient_struct = RustStruct::new("Patient".to_string());
403 type_cache.insert("Patient".to_string(), patient_struct);
404
405 let patient_contact_struct = RustStruct::new("PatientContact".to_string());
407 type_cache.insert("PatientContact".to_string(), patient_contact_struct);
408
409 let patient_link_struct = RustStruct::new("PatientLink".to_string());
410 type_cache.insert("PatientLink".to_string(), patient_link_struct);
411
412 let observation_struct = RustStruct::new("Observation".to_string());
414 type_cache.insert("Observation".to_string(), observation_struct);
415
416 let nested_structs = FileIoManager::collect_nested_structs("Patient", &type_cache);
417
418 assert_eq!(
419 nested_structs.len(),
420 2,
421 "Should collect exactly 2 nested structs"
422 );
423
424 let nested_names: Vec<String> = nested_structs.iter().map(|s| s.name.clone()).collect();
425 assert!(nested_names.contains(&"PatientContact".to_string()));
426 assert!(nested_names.contains(&"PatientLink".to_string()));
427 assert!(!nested_names.contains(&"Patient".to_string())); assert!(!nested_names.contains(&"Observation".to_string())); }
430
431 #[test]
432 fn test_ensure_directory_exists() {
433 let temp_dir = TempDir::new().unwrap();
434 let new_dir = temp_dir.path().join("nested").join("directory");
435
436 assert!(!new_dir.exists());
438
439 let result = FileIoManager::ensure_directory(&new_dir);
440 assert!(result.is_ok(), "Should create directory successfully");
441
442 assert!(new_dir.exists());
444 assert!(new_dir.is_dir());
445 }
446
447 #[test]
448 fn test_get_output_path_for_structure() {
449 let config = CodegenConfig::default();
450 let token_generator = TokenGenerator::new();
451 let file_generator = FileGenerator::new(&config, &token_generator);
452
453 let temp_dir = TempDir::new().unwrap();
454
455 let json_content = r#"{
457 "resourceType": "StructureDefinition",
458 "id": "Patient",
459 "url": "http://hl7.org/fhir/StructureDefinition/Patient",
460 "name": "Patient",
461 "title": "Patient",
462 "status": "active",
463 "kind": "resource",
464 "abstract": false,
465 "type": "Patient",
466 "description": "A patient resource",
467 "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
468 }"#;
469
470 let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
472
473 let output_path = FileIoManager::get_output_path_for_structure(
474 temp_dir.path(),
475 &patient_structure,
476 &file_generator,
477 );
478
479 let expected_path = temp_dir
480 .path()
481 .join("src")
482 .join("resources")
483 .join("patient.rs");
484 assert_eq!(output_path, expected_path);
485 }
486
487 #[test]
488 fn test_get_trait_file_path() {
489 let temp_dir = TempDir::new().unwrap();
490
491 let json_content = r#"{
493 "resourceType": "StructureDefinition",
494 "id": "Patient",
495 "url": "http://hl7.org/fhir/StructureDefinition/Patient",
496 "name": "Patient",
497 "title": "Patient",
498 "status": "active",
499 "kind": "resource",
500 "abstract": false,
501 "type": "Patient",
502 "description": "A patient resource",
503 "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
504 }"#;
505
506 let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
508
509 let trait_path = FileIoManager::get_trait_file_path(temp_dir.path(), &patient_structure);
510 let expected_path = temp_dir
511 .path()
512 .join("src")
513 .join("traits")
514 .join("patient.rs");
515 assert_eq!(trait_path, expected_path);
516 }
517
518 #[test]
519 fn test_file_io_manager_creation() {
520 let config = CodegenConfig::default();
521 let token_generator = TokenGenerator::new();
522 let file_io_manager = FileIoManager::new(&config, &token_generator);
523
524 assert_eq!(
528 std::mem::size_of_val(&file_io_manager),
529 std::mem::size_of::<FileIoManager>()
530 );
531 }
532
533 #[test]
534 fn test_nested_struct_collection_fix() {
535 use crate::rust_types::RustStruct;
536 use std::collections::HashMap;
537
538 let mut type_cache = HashMap::new();
539
540 let element_struct = RustStruct::new("Element".to_string());
542 let element_definition_struct = RustStruct::new("ElementDefinition".to_string());
543
544 let element_extension_struct = RustStruct::new("ElementExtension".to_string());
546 let element_binding_struct = RustStruct::new("ElementBinding".to_string());
547
548 let element_definition_binding_struct =
550 RustStruct::new("ElementDefinitionBinding".to_string());
551 let element_definition_constraint_struct =
552 RustStruct::new("ElementDefinitionConstraint".to_string());
553 let element_definition_type_struct = RustStruct::new("ElementDefinitionType".to_string());
554
555 type_cache.insert("Element".to_string(), element_struct);
557 type_cache.insert("ElementDefinition".to_string(), element_definition_struct);
558 type_cache.insert("ElementExtension".to_string(), element_extension_struct);
559 type_cache.insert("ElementBinding".to_string(), element_binding_struct);
560 type_cache.insert(
561 "ElementDefinitionBinding".to_string(),
562 element_definition_binding_struct,
563 );
564 type_cache.insert(
565 "ElementDefinitionConstraint".to_string(),
566 element_definition_constraint_struct,
567 );
568 type_cache.insert(
569 "ElementDefinitionType".to_string(),
570 element_definition_type_struct,
571 );
572
573 let elementdefinition_binding_struct =
575 RustStruct::new("ElementdefinitionBinding".to_string());
576 type_cache.insert(
577 "ElementdefinitionBinding".to_string(),
578 elementdefinition_binding_struct,
579 );
580 let elementdefinition_constraint_struct =
581 RustStruct::new("ElementdefinitionConstraint".to_string());
582 type_cache.insert(
583 "ElementdefinitionConstraint".to_string(),
584 elementdefinition_constraint_struct,
585 );
586 let elementdefinition_type_struct = RustStruct::new("ElementdefinitionType".to_string());
587 type_cache.insert(
588 "ElementdefinitionType".to_string(),
589 elementdefinition_type_struct,
590 );
591
592 let element_nested = FileIoManager::collect_nested_structs("Element", &type_cache);
594
595 assert_eq!(
598 element_nested.len(),
599 2,
600 "Element should have 2 nested structs"
601 );
602
603 let element_nested_names: Vec<String> =
604 element_nested.iter().map(|s| s.name.clone()).collect();
605 assert!(element_nested_names.contains(&"ElementExtension".to_string()));
606 assert!(element_nested_names.contains(&"ElementBinding".to_string()));
607 assert!(!element_nested_names.contains(&"ElementDefinitionBinding".to_string()));
608 assert!(!element_nested_names.contains(&"ElementDefinitionConstraint".to_string()));
609 assert!(!element_nested_names.contains(&"ElementDefinitionType".to_string()));
610 assert!(!element_nested_names.contains(&"ElementDefinition".to_string()));
611 assert!(!element_nested_names.contains(&"ElementdefinitionBinding".to_string()));
613 assert!(!element_nested_names.contains(&"ElementdefinitionConstraint".to_string()));
614 assert!(!element_nested_names.contains(&"ElementdefinitionType".to_string()));
615
616 let element_definition_nested =
618 FileIoManager::collect_nested_structs("ElementDefinition", &type_cache);
619
620 assert_eq!(
623 element_definition_nested.len(),
624 3,
625 "ElementDefinition should have 3 nested structs (only uppercase)"
626 );
627
628 let element_definition_nested_names: Vec<String> = element_definition_nested
629 .iter()
630 .map(|s| s.name.clone())
631 .collect();
632 assert!(element_definition_nested_names.contains(&"ElementDefinitionBinding".to_string()));
633 assert!(
634 element_definition_nested_names.contains(&"ElementDefinitionConstraint".to_string())
635 );
636 assert!(element_definition_nested_names.contains(&"ElementDefinitionType".to_string()));
637 assert!(!element_definition_nested_names.contains(&"ElementdefinitionBinding".to_string()));
639 assert!(
640 !element_definition_nested_names.contains(&"ElementdefinitionConstraint".to_string())
641 );
642 assert!(!element_definition_nested_names.contains(&"ElementdefinitionType".to_string()));
643 assert!(!element_definition_nested_names.contains(&"ElementExtension".to_string()));
644 assert!(!element_definition_nested_names.contains(&"ElementBinding".to_string()));
645
646 type_cache.insert("Bundle".to_string(), RustStruct::new("Bundle".to_string()));
650 type_cache.insert(
651 "BundleEntry".to_string(),
652 RustStruct::new("BundleEntry".to_string()),
653 );
654 type_cache.insert(
655 "BundleLink".to_string(),
656 RustStruct::new("BundleLink".to_string()),
657 );
658
659 let bundle_nested = FileIoManager::collect_nested_structs("Bundle", &type_cache);
660 assert_eq!(
661 bundle_nested.len(),
662 2,
663 "Bundle should have 2 nested structs"
664 );
665
666 let bundle_nested_names: Vec<String> =
667 bundle_nested.iter().map(|s| s.name.clone()).collect();
668 assert!(bundle_nested_names.contains(&"BundleEntry".to_string()));
669 assert!(bundle_nested_names.contains(&"BundleLink".to_string()));
670 }
671}