rh-foundation 0.2.0-beta.2

Foundation crate providing common utilities, error handling, and shared functionality
Documentation
use std::path::{Path, PathBuf};

use tracing::{debug, info, warn};

use crate::snapshot::error::{SnapshotError, SnapshotResult};
use crate::snapshot::sd_load_support::{
    ensure_directory, json_files_in_directory, package_directory, read_file,
    try_parse_structure_definition, validate_structure_definition,
};
use crate::snapshot::types::StructureDefinition;

pub struct StructureDefinitionLoader;

impl StructureDefinitionLoader {
    pub fn load_from_file(path: &Path) -> SnapshotResult<StructureDefinition> {
        debug!("Loading StructureDefinition from file: {}", path.display());

        let content = read_file(path)?;
        let structure_definition: StructureDefinition =
            serde_json::from_str(&content).map_err(SnapshotError::SerializationError)?;

        Self::validate_structure_definition(&structure_definition)?;

        info!(
            "Loaded StructureDefinition: {} ({})",
            structure_definition.name, structure_definition.url
        );

        Ok(structure_definition)
    }

    pub fn load_from_directory(dir: &Path) -> SnapshotResult<Vec<StructureDefinition>> {
        info!(
            "Loading StructureDefinitions from directory: {}",
            dir.display()
        );

        ensure_directory(dir)?;

        let mut structure_definitions = Vec::new();
        for path in json_files_in_directory(dir)? {
            match Self::try_load_structure_definition(&path) {
                Ok(Some(structure_definition)) => structure_definitions.push(structure_definition),
                Ok(None) => {
                    debug!("Skipping non-StructureDefinition file: {}", path.display());
                }
                Err(error) => {
                    warn!(
                        "Failed to load StructureDefinition from {}: {}",
                        path.display(),
                        error
                    );
                }
            }
        }

        info!(
            "Loaded {} StructureDefinitions from {}",
            structure_definitions.len(),
            dir.display()
        );

        Ok(structure_definitions)
    }

    pub fn load_from_package(
        package_name: &str,
        version: &str,
        packages_dir: &Path,
    ) -> SnapshotResult<Vec<StructureDefinition>> {
        info!(
            "Loading StructureDefinitions from package {}@{}",
            package_name, version
        );

        let package_dir = Self::get_package_directory(packages_dir, package_name, version);

        if !package_dir.exists() {
            return Err(SnapshotError::Other(format!(
                "Package not found: {}@{} at {}",
                package_name,
                version,
                package_dir.display()
            )));
        }

        let package_subdir = package_dir.join("package");
        let dir_to_scan = if package_subdir.exists() && package_subdir.is_dir() {
            package_subdir
        } else {
            package_dir
        };

        Self::load_from_directory(&dir_to_scan)
    }

    fn try_load_structure_definition(path: &Path) -> SnapshotResult<Option<StructureDefinition>> {
        let content = read_file(path)?;
        try_parse_structure_definition(&content)
    }

    fn validate_structure_definition(sd: &StructureDefinition) -> SnapshotResult<()> {
        validate_structure_definition(sd)
    }

    fn get_package_directory(packages_dir: &Path, package_name: &str, version: &str) -> PathBuf {
        package_directory(packages_dir, package_name, version)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::io::Write;
    use tempfile::TempDir;

    fn create_test_structure_definition() -> String {
        r#"{
            "resourceType": "StructureDefinition",
            "url": "http://example.org/StructureDefinition/test-patient",
            "name": "TestPatient",
            "type": "Patient",
            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
            "differential": {
                "element": [
                    {
                        "path": "Patient",
                        "min": 0,
                        "max": "*"
                    }
                ]
            }
        }"#
        .to_string()
    }

    fn create_invalid_structure_definition() -> String {
        r#"{
            "resourceType": "StructureDefinition",
            "url": "",
            "name": "TestPatient",
            "type": "Patient"
        }"#
        .to_string()
    }

    fn create_non_structure_definition() -> String {
        r#"{
            "resourceType": "Patient",
            "id": "example"
        }"#
        .to_string()
    }

    #[test]
    fn test_load_from_file_success() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.json");

        let mut file = fs::File::create(&file_path).unwrap();
        file.write_all(create_test_structure_definition().as_bytes())
            .unwrap();

        let result = StructureDefinitionLoader::load_from_file(&file_path);
        assert!(result.is_ok());

        let sd = result.unwrap();
        assert_eq!(sd.name, "TestPatient");
        assert_eq!(
            sd.url,
            "http://example.org/StructureDefinition/test-patient"
        );
        assert_eq!(sd.type_, "Patient");
    }

    #[test]
    fn test_load_from_file_missing_url() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("invalid.json");

        let mut file = fs::File::create(&file_path).unwrap();
        file.write_all(create_invalid_structure_definition().as_bytes())
            .unwrap();

        let result = StructureDefinitionLoader::load_from_file(&file_path);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            SnapshotError::InvalidStructureDefinition(_)
        ));
    }

    #[test]
    fn test_load_from_directory() {
        let temp_dir = TempDir::new().unwrap();

        let file1 = temp_dir.path().join("sd1.json");
        fs::write(&file1, create_test_structure_definition()).unwrap();

        let file2 = temp_dir.path().join("patient.json");
        fs::write(&file2, create_non_structure_definition()).unwrap();

        let file3 = temp_dir.path().join("readme.txt");
        fs::write(&file3, "not json").unwrap();

        let result = StructureDefinitionLoader::load_from_directory(temp_dir.path());
        assert!(result.is_ok());

        let sds = result.unwrap();
        assert_eq!(sds.len(), 1);
        assert_eq!(sds[0].name, "TestPatient");
    }

    #[test]
    fn test_validate_structure_definition() {
        let valid_sd = StructureDefinition {
            url: "http://example.org/test".to_string(),
            name: "Test".to_string(),
            type_: "Patient".to_string(),
            base_definition: None,
            differential: None,
            snapshot: None,
        };

        assert!(StructureDefinitionLoader::validate_structure_definition(&valid_sd).is_ok());

        let invalid_sd = StructureDefinition {
            url: "".to_string(),
            name: "Test".to_string(),
            type_: "Patient".to_string(),
            base_definition: None,
            differential: None,
            snapshot: None,
        };

        assert!(StructureDefinitionLoader::validate_structure_definition(&invalid_sd).is_err());
    }
}