rh-foundation 0.1.0-beta.1

Foundation crate providing common utilities, error handling, and shared functionality
Documentation
use crate::snapshot::error::{SnapshotError, SnapshotResult};
use crate::snapshot::types::StructureDefinition;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};

pub struct StructureDefinitionLoader;

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

        let content = fs::read_to_string(path).map_err(|e| {
            SnapshotError::Other(format!("Failed to read file {}: {}", path.display(), e))
        })?;

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

        Self::validate_structure_definition(&sd)?;

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

        Ok(sd)
    }

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

        if !dir.exists() {
            return Err(SnapshotError::Other(format!(
                "Directory does not exist: {}",
                dir.display()
            )));
        }

        if !dir.is_dir() {
            return Err(SnapshotError::Other(format!(
                "Path is not a directory: {}",
                dir.display()
            )));
        }

        let mut structure_definitions = Vec::new();
        let entries = fs::read_dir(dir).map_err(|e| {
            SnapshotError::Other(format!("Failed to read directory {}: {}", dir.display(), e))
        })?;

        for entry in entries {
            let entry = entry.map_err(|e| {
                SnapshotError::Other(format!("Failed to read directory entry: {e}"))
            })?;

            let path = entry.path();

            if path.is_file() {
                if let Some(ext) = path.extension() {
                    if ext == "json" {
                        match Self::try_load_structure_definition(&path) {
                            Ok(Some(sd)) => {
                                structure_definitions.push(sd);
                            }
                            Ok(None) => {
                                debug!("Skipping non-StructureDefinition file: {}", path.display());
                            }
                            Err(e) => {
                                warn!(
                                    "Failed to load StructureDefinition from {}: {}",
                                    path.display(),
                                    e
                                );
                            }
                        }
                    }
                }
            }
        }

        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 = fs::read_to_string(path).map_err(|e| {
            SnapshotError::Other(format!("Failed to read file {}: {}", path.display(), e))
        })?;

        let json: serde_json::Value =
            serde_json::from_str(&content).map_err(SnapshotError::SerializationError)?;

        if json.get("resourceType")
            == Some(&serde_json::Value::String(
                "StructureDefinition".to_string(),
            ))
        {
            let sd: StructureDefinition =
                serde_json::from_value(json).map_err(SnapshotError::SerializationError)?;

            Self::validate_structure_definition(&sd)?;
            Ok(Some(sd))
        } else {
            Ok(None)
        }
    }

    fn validate_structure_definition(sd: &StructureDefinition) -> SnapshotResult<()> {
        if sd.url.is_empty() {
            return Err(SnapshotError::InvalidStructureDefinition(
                "StructureDefinition.url is required".to_string(),
            ));
        }

        if sd.name.is_empty() {
            return Err(SnapshotError::InvalidStructureDefinition(
                "StructureDefinition.name is required".to_string(),
            ));
        }

        if sd.type_.is_empty() {
            return Err(SnapshotError::InvalidStructureDefinition(
                "StructureDefinition.type is required".to_string(),
            ));
        }

        Ok(())
    }

    fn get_package_directory(packages_dir: &Path, package_name: &str, version: &str) -> PathBuf {
        packages_dir.join(format!("{package_name}#{version}"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    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());
    }
}