use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::config::CodegenConfig;
use crate::fhir_types::StructureDefinition;
use crate::generators::{FileGenerator, TokenGenerator};
use crate::rust_types::{RustStruct, RustTrait};
use crate::CodegenResult;
pub use crate::generators::file_generator::FhirTypeCategory;
pub struct FileIoManager<'a> {
config: &'a CodegenConfig,
token_generator: &'a TokenGenerator,
}
impl<'a> FileIoManager<'a> {
pub fn new(config: &'a CodegenConfig, token_generator: &'a TokenGenerator) -> Self {
Self {
config,
token_generator,
}
}
pub fn load_structure_definition<P: AsRef<Path>>(
path: P,
) -> CodegenResult<StructureDefinition> {
let content = fs::read_to_string(&path)?;
let structure_def: StructureDefinition = serde_json::from_str(&content)?;
Ok(structure_def)
}
pub fn generate_to_organized_directories<P: AsRef<Path>>(
&self,
structure_def: &StructureDefinition,
base_output_dir: P,
rust_struct: &RustStruct,
nested_structs: &[RustStruct],
) -> CodegenResult<()> {
let base_dir = base_output_dir.as_ref();
let target_dir = match self.classify_fhir_structure_def(structure_def) {
FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
};
std::fs::create_dir_all(&target_dir)?;
let filename = crate::naming::Naming::filename(structure_def);
let output_path = target_dir.join(filename);
self.generate_to_file(structure_def, output_path, rust_struct, nested_structs)
}
pub fn generate_traits_to_organized_directory<P: AsRef<Path>>(
&self,
structure_def: &StructureDefinition,
base_output_dir: P,
rust_traits: &[RustTrait],
) -> CodegenResult<()> {
let traits_dir = base_output_dir.as_ref().join("src").join("traits");
std::fs::create_dir_all(&traits_dir)?;
let struct_name = crate::naming::Naming::struct_name(structure_def);
let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
let filename = format!("{snake_case_name}.rs");
let output_path = traits_dir.join(filename);
let rust_trait_refs: Vec<&RustTrait> = rust_traits.iter().collect();
let file_generator = FileGenerator::new(self.config, self.token_generator);
file_generator.generate_traits_to_file(structure_def, output_path, &rust_trait_refs)
}
pub fn generate_trait_to_organized_directory<P: AsRef<Path>>(
&self,
structure_def: &StructureDefinition,
base_output_dir: P,
rust_trait: &RustTrait,
) -> CodegenResult<()> {
let traits_dir = base_output_dir.as_ref().join("src").join("traits");
std::fs::create_dir_all(&traits_dir)?;
let struct_name = crate::naming::Naming::struct_name(structure_def);
let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
let filename = format!("{snake_case_name}.rs");
let output_path = traits_dir.join(filename);
self.generate_trait_to_file(structure_def, output_path, rust_trait)
}
pub fn generate_to_file<P: AsRef<Path>>(
&self,
structure_def: &StructureDefinition,
output_path: P,
rust_struct: &RustStruct,
nested_structs: &[RustStruct],
) -> CodegenResult<()> {
let file_generator = FileGenerator::new(self.config, self.token_generator);
file_generator.generate_to_file(structure_def, output_path, rust_struct, nested_structs)
}
pub fn generate_traits_to_file<P: AsRef<Path>>(
&self,
structure_def: &StructureDefinition,
output_path: P,
rust_traits: &[RustTrait],
) -> CodegenResult<()> {
let file_generator = FileGenerator::new(self.config, self.token_generator);
let rust_trait_refs: Vec<&RustTrait> = rust_traits.iter().collect();
file_generator.generate_traits_to_file(structure_def, output_path, &rust_trait_refs)
}
pub fn generate_trait_to_file<P: AsRef<Path>>(
&self,
structure_def: &StructureDefinition,
output_path: P,
rust_trait: &RustTrait,
) -> CodegenResult<()> {
let file_generator = FileGenerator::new(self.config, self.token_generator);
file_generator.generate_trait_to_file(structure_def, output_path, rust_trait)
}
pub fn generate_trait_file_from_trait<P: AsRef<Path>>(
&self,
rust_trait: &RustTrait,
output_path: P,
) -> CodegenResult<()> {
let file_generator = FileGenerator::new(self.config, self.token_generator);
file_generator.generate_trait_file_from_trait(rust_trait, output_path)
}
fn is_direct_nested_struct(parent_name: &str, cached_name: &str) -> bool {
if let Some(classification) =
crate::generators::type_registry::TypeRegistry::get_classification(cached_name)
{
match classification {
crate::generators::type_registry::TypeClassification::NestedStructure {
parent_resource,
} => {
return parent_resource == parent_name;
}
_ => {
return false;
}
}
}
if !cached_name.starts_with(parent_name) {
return false;
}
if cached_name.len() <= parent_name.len() {
return false;
}
let remainder = &cached_name[parent_name.len()..];
if let Some(first_char) = remainder.chars().next() {
if first_char.is_lowercase() {
return false;
}
}
let separate_resource_suffixes = [
"Definition",
"Specification",
"Polymer",
"Protein",
"ReferenceInformation",
"SourceMaterial",
"NucleicAcid",
"Authorization",
"Contraindication",
"Indication",
"Ingredient",
"Interaction",
"Manufactured",
"Packaged",
"Pharmaceutical",
"UndesirableEffect",
"Knowledge",
"Administration",
"Dispense",
"Request",
"Statement",
];
if separate_resource_suffixes.contains(&remainder) {
return false;
}
if separate_resource_suffixes
.iter()
.any(|&suffix| remainder.starts_with(suffix))
{
return false;
}
true
}
pub fn collect_nested_structs(
struct_name: &str,
type_cache: &HashMap<String, RustStruct>,
) -> Vec<RustStruct> {
let mut nested_structs = vec![];
for (cached_name, cached_struct) in type_cache {
if cached_name != struct_name && Self::is_direct_nested_struct(struct_name, cached_name)
{
nested_structs.push(cached_struct.clone());
}
}
nested_structs
}
pub fn classify_fhir_structure_def(
&self,
structure_def: &StructureDefinition,
) -> FhirTypeCategory {
let file_generator = FileGenerator::new(self.config, self.token_generator);
file_generator.classify_fhir_structure_def(structure_def)
}
pub fn ensure_directory(dir_path: &Path) -> CodegenResult<()> {
std::fs::create_dir_all(dir_path)?;
Ok(())
}
pub fn get_output_path_for_structure<P: AsRef<Path>>(
base_dir: P,
structure_def: &StructureDefinition,
file_generator: &FileGenerator,
) -> std::path::PathBuf {
let base_dir = base_dir.as_ref();
let target_dir = match file_generator.classify_fhir_structure_def(structure_def) {
FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
};
let filename = crate::naming::Naming::filename(structure_def);
target_dir.join(filename)
}
pub fn get_traits_directory_path<P: AsRef<Path>>(base_dir: P) -> std::path::PathBuf {
base_dir.as_ref().join("src").join("traits")
}
pub fn get_trait_file_path<P: AsRef<Path>>(
base_dir: P,
structure_def: &StructureDefinition,
) -> std::path::PathBuf {
let traits_dir = Self::get_traits_directory_path(base_dir);
let struct_name = crate::naming::Naming::struct_name(structure_def);
let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
let filename = format!("{snake_case_name}.rs");
traits_dir.join(filename)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::CodegenConfig;
use crate::generators::TokenGenerator;
use tempfile::TempDir;
#[test]
fn test_load_structure_definition() {
use std::fs;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test_structure.json");
let json_content = r#"{
"resourceType": "StructureDefinition",
"id": "Patient",
"url": "http://hl7.org/fhir/StructureDefinition/Patient",
"name": "Patient",
"title": "Patient",
"status": "active",
"kind": "resource",
"abstract": false,
"type": "Patient",
"description": "A patient resource",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
}"#;
fs::write(&file_path, json_content).unwrap();
let result = FileIoManager::load_structure_definition(&file_path);
assert!(
result.is_ok(),
"Should load StructureDefinition successfully"
);
let loaded_structure = result.unwrap();
assert_eq!(loaded_structure.name, "Patient");
assert_eq!(loaded_structure.kind, "resource");
}
#[test]
fn test_load_structure_definition_invalid_json() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("invalid.json");
fs::write(&file_path, "{ invalid json }").unwrap();
let result = FileIoManager::load_structure_definition(&file_path);
assert!(result.is_err(), "Should fail to load invalid JSON");
}
#[test]
fn test_load_structure_definition_missing_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("missing.json");
let result = FileIoManager::load_structure_definition(&file_path);
assert!(result.is_err(), "Should fail to load missing file");
}
#[test]
fn test_collect_nested_structs() {
let mut type_cache = HashMap::new();
let patient_struct = RustStruct::new("Patient".to_string());
type_cache.insert("Patient".to_string(), patient_struct);
let patient_contact_struct = RustStruct::new("PatientContact".to_string());
type_cache.insert("PatientContact".to_string(), patient_contact_struct);
let patient_link_struct = RustStruct::new("PatientLink".to_string());
type_cache.insert("PatientLink".to_string(), patient_link_struct);
let observation_struct = RustStruct::new("Observation".to_string());
type_cache.insert("Observation".to_string(), observation_struct);
let nested_structs = FileIoManager::collect_nested_structs("Patient", &type_cache);
assert_eq!(
nested_structs.len(),
2,
"Should collect exactly 2 nested structs"
);
let nested_names: Vec<String> = nested_structs.iter().map(|s| s.name.clone()).collect();
assert!(nested_names.contains(&"PatientContact".to_string()));
assert!(nested_names.contains(&"PatientLink".to_string()));
assert!(!nested_names.contains(&"Patient".to_string())); assert!(!nested_names.contains(&"Observation".to_string())); }
#[test]
fn test_ensure_directory_exists() {
let temp_dir = TempDir::new().unwrap();
let new_dir = temp_dir.path().join("nested").join("directory");
assert!(!new_dir.exists());
let result = FileIoManager::ensure_directory(&new_dir);
assert!(result.is_ok(), "Should create directory successfully");
assert!(new_dir.exists());
assert!(new_dir.is_dir());
}
#[test]
fn test_get_output_path_for_structure() {
let config = CodegenConfig::default();
let token_generator = TokenGenerator::new();
let file_generator = FileGenerator::new(&config, &token_generator);
let temp_dir = TempDir::new().unwrap();
let json_content = r#"{
"resourceType": "StructureDefinition",
"id": "Patient",
"url": "http://hl7.org/fhir/StructureDefinition/Patient",
"name": "Patient",
"title": "Patient",
"status": "active",
"kind": "resource",
"abstract": false,
"type": "Patient",
"description": "A patient resource",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
}"#;
let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
let output_path = FileIoManager::get_output_path_for_structure(
temp_dir.path(),
&patient_structure,
&file_generator,
);
let expected_path = temp_dir
.path()
.join("src")
.join("resources")
.join("patient.rs");
assert_eq!(output_path, expected_path);
}
#[test]
fn test_get_trait_file_path() {
let temp_dir = TempDir::new().unwrap();
let json_content = r#"{
"resourceType": "StructureDefinition",
"id": "Patient",
"url": "http://hl7.org/fhir/StructureDefinition/Patient",
"name": "Patient",
"title": "Patient",
"status": "active",
"kind": "resource",
"abstract": false,
"type": "Patient",
"description": "A patient resource",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
}"#;
let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
let trait_path = FileIoManager::get_trait_file_path(temp_dir.path(), &patient_structure);
let expected_path = temp_dir
.path()
.join("src")
.join("traits")
.join("patient.rs");
assert_eq!(trait_path, expected_path);
}
#[test]
fn test_file_io_manager_creation() {
let config = CodegenConfig::default();
let token_generator = TokenGenerator::new();
let file_io_manager = FileIoManager::new(&config, &token_generator);
assert_eq!(
std::mem::size_of_val(&file_io_manager),
std::mem::size_of::<FileIoManager>()
);
}
#[test]
fn test_nested_struct_collection_fix() {
use crate::rust_types::RustStruct;
use std::collections::HashMap;
let mut type_cache = HashMap::new();
let element_struct = RustStruct::new("Element".to_string());
let element_definition_struct = RustStruct::new("ElementDefinition".to_string());
let element_extension_struct = RustStruct::new("ElementExtension".to_string());
let element_binding_struct = RustStruct::new("ElementBinding".to_string());
let element_definition_binding_struct =
RustStruct::new("ElementDefinitionBinding".to_string());
let element_definition_constraint_struct =
RustStruct::new("ElementDefinitionConstraint".to_string());
let element_definition_type_struct = RustStruct::new("ElementDefinitionType".to_string());
type_cache.insert("Element".to_string(), element_struct);
type_cache.insert("ElementDefinition".to_string(), element_definition_struct);
type_cache.insert("ElementExtension".to_string(), element_extension_struct);
type_cache.insert("ElementBinding".to_string(), element_binding_struct);
type_cache.insert(
"ElementDefinitionBinding".to_string(),
element_definition_binding_struct,
);
type_cache.insert(
"ElementDefinitionConstraint".to_string(),
element_definition_constraint_struct,
);
type_cache.insert(
"ElementDefinitionType".to_string(),
element_definition_type_struct,
);
let elementdefinition_binding_struct =
RustStruct::new("ElementdefinitionBinding".to_string());
type_cache.insert(
"ElementdefinitionBinding".to_string(),
elementdefinition_binding_struct,
);
let elementdefinition_constraint_struct =
RustStruct::new("ElementdefinitionConstraint".to_string());
type_cache.insert(
"ElementdefinitionConstraint".to_string(),
elementdefinition_constraint_struct,
);
let elementdefinition_type_struct = RustStruct::new("ElementdefinitionType".to_string());
type_cache.insert(
"ElementdefinitionType".to_string(),
elementdefinition_type_struct,
);
let element_nested = FileIoManager::collect_nested_structs("Element", &type_cache);
assert_eq!(
element_nested.len(),
2,
"Element should have 2 nested structs"
);
let element_nested_names: Vec<String> =
element_nested.iter().map(|s| s.name.clone()).collect();
assert!(element_nested_names.contains(&"ElementExtension".to_string()));
assert!(element_nested_names.contains(&"ElementBinding".to_string()));
assert!(!element_nested_names.contains(&"ElementDefinitionBinding".to_string()));
assert!(!element_nested_names.contains(&"ElementDefinitionConstraint".to_string()));
assert!(!element_nested_names.contains(&"ElementDefinitionType".to_string()));
assert!(!element_nested_names.contains(&"ElementDefinition".to_string()));
assert!(!element_nested_names.contains(&"ElementdefinitionBinding".to_string()));
assert!(!element_nested_names.contains(&"ElementdefinitionConstraint".to_string()));
assert!(!element_nested_names.contains(&"ElementdefinitionType".to_string()));
let element_definition_nested =
FileIoManager::collect_nested_structs("ElementDefinition", &type_cache);
assert_eq!(
element_definition_nested.len(),
3,
"ElementDefinition should have 3 nested structs (only uppercase)"
);
let element_definition_nested_names: Vec<String> = element_definition_nested
.iter()
.map(|s| s.name.clone())
.collect();
assert!(element_definition_nested_names.contains(&"ElementDefinitionBinding".to_string()));
assert!(
element_definition_nested_names.contains(&"ElementDefinitionConstraint".to_string())
);
assert!(element_definition_nested_names.contains(&"ElementDefinitionType".to_string()));
assert!(!element_definition_nested_names.contains(&"ElementdefinitionBinding".to_string()));
assert!(
!element_definition_nested_names.contains(&"ElementdefinitionConstraint".to_string())
);
assert!(!element_definition_nested_names.contains(&"ElementdefinitionType".to_string()));
assert!(!element_definition_nested_names.contains(&"ElementExtension".to_string()));
assert!(!element_definition_nested_names.contains(&"ElementBinding".to_string()));
type_cache.insert("Bundle".to_string(), RustStruct::new("Bundle".to_string()));
type_cache.insert(
"BundleEntry".to_string(),
RustStruct::new("BundleEntry".to_string()),
);
type_cache.insert(
"BundleLink".to_string(),
RustStruct::new("BundleLink".to_string()),
);
let bundle_nested = FileIoManager::collect_nested_structs("Bundle", &type_cache);
assert_eq!(
bundle_nested.len(),
2,
"Bundle should have 2 nested structs"
);
let bundle_nested_names: Vec<String> =
bundle_nested.iter().map(|s| s.name.clone()).collect();
assert!(bundle_nested_names.contains(&"BundleEntry".to_string()));
assert!(bundle_nested_names.contains(&"BundleLink".to_string()));
}
}