Skip to main content

rh_foundation/snapshot/
sd_loader.rs

1use std::path::{Path, PathBuf};
2
3use tracing::{debug, info, warn};
4
5use crate::snapshot::error::{SnapshotError, SnapshotResult};
6use crate::snapshot::sd_load_support::{
7    ensure_directory, json_files_in_directory, package_directory, read_file,
8    try_parse_structure_definition, validate_structure_definition,
9};
10use crate::snapshot::types::StructureDefinition;
11
12pub struct StructureDefinitionLoader;
13
14impl StructureDefinitionLoader {
15    pub fn load_from_file(path: &Path) -> SnapshotResult<StructureDefinition> {
16        debug!("Loading StructureDefinition from file: {}", path.display());
17
18        let content = read_file(path)?;
19        let structure_definition: StructureDefinition =
20            serde_json::from_str(&content).map_err(SnapshotError::SerializationError)?;
21
22        Self::validate_structure_definition(&structure_definition)?;
23
24        info!(
25            "Loaded StructureDefinition: {} ({})",
26            structure_definition.name, structure_definition.url
27        );
28
29        Ok(structure_definition)
30    }
31
32    pub fn load_from_directory(dir: &Path) -> SnapshotResult<Vec<StructureDefinition>> {
33        info!(
34            "Loading StructureDefinitions from directory: {}",
35            dir.display()
36        );
37
38        ensure_directory(dir)?;
39
40        let mut structure_definitions = Vec::new();
41        for path in json_files_in_directory(dir)? {
42            match Self::try_load_structure_definition(&path) {
43                Ok(Some(structure_definition)) => structure_definitions.push(structure_definition),
44                Ok(None) => {
45                    debug!("Skipping non-StructureDefinition file: {}", path.display());
46                }
47                Err(error) => {
48                    warn!(
49                        "Failed to load StructureDefinition from {}: {}",
50                        path.display(),
51                        error
52                    );
53                }
54            }
55        }
56
57        info!(
58            "Loaded {} StructureDefinitions from {}",
59            structure_definitions.len(),
60            dir.display()
61        );
62
63        Ok(structure_definitions)
64    }
65
66    pub fn load_from_package(
67        package_name: &str,
68        version: &str,
69        packages_dir: &Path,
70    ) -> SnapshotResult<Vec<StructureDefinition>> {
71        info!(
72            "Loading StructureDefinitions from package {}@{}",
73            package_name, version
74        );
75
76        let package_dir = Self::get_package_directory(packages_dir, package_name, version);
77
78        if !package_dir.exists() {
79            return Err(SnapshotError::Other(format!(
80                "Package not found: {}@{} at {}",
81                package_name,
82                version,
83                package_dir.display()
84            )));
85        }
86
87        let package_subdir = package_dir.join("package");
88        let dir_to_scan = if package_subdir.exists() && package_subdir.is_dir() {
89            package_subdir
90        } else {
91            package_dir
92        };
93
94        Self::load_from_directory(&dir_to_scan)
95    }
96
97    fn try_load_structure_definition(path: &Path) -> SnapshotResult<Option<StructureDefinition>> {
98        let content = read_file(path)?;
99        try_parse_structure_definition(&content)
100    }
101
102    fn validate_structure_definition(sd: &StructureDefinition) -> SnapshotResult<()> {
103        validate_structure_definition(sd)
104    }
105
106    fn get_package_directory(packages_dir: &Path, package_name: &str, version: &str) -> PathBuf {
107        package_directory(packages_dir, package_name, version)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use std::fs;
115    use std::io::Write;
116    use tempfile::TempDir;
117
118    fn create_test_structure_definition() -> String {
119        r#"{
120            "resourceType": "StructureDefinition",
121            "url": "http://example.org/StructureDefinition/test-patient",
122            "name": "TestPatient",
123            "type": "Patient",
124            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
125            "differential": {
126                "element": [
127                    {
128                        "path": "Patient",
129                        "min": 0,
130                        "max": "*"
131                    }
132                ]
133            }
134        }"#
135        .to_string()
136    }
137
138    fn create_invalid_structure_definition() -> String {
139        r#"{
140            "resourceType": "StructureDefinition",
141            "url": "",
142            "name": "TestPatient",
143            "type": "Patient"
144        }"#
145        .to_string()
146    }
147
148    fn create_non_structure_definition() -> String {
149        r#"{
150            "resourceType": "Patient",
151            "id": "example"
152        }"#
153        .to_string()
154    }
155
156    #[test]
157    fn test_load_from_file_success() {
158        let temp_dir = TempDir::new().unwrap();
159        let file_path = temp_dir.path().join("test.json");
160
161        let mut file = fs::File::create(&file_path).unwrap();
162        file.write_all(create_test_structure_definition().as_bytes())
163            .unwrap();
164
165        let result = StructureDefinitionLoader::load_from_file(&file_path);
166        assert!(result.is_ok());
167
168        let sd = result.unwrap();
169        assert_eq!(sd.name, "TestPatient");
170        assert_eq!(
171            sd.url,
172            "http://example.org/StructureDefinition/test-patient"
173        );
174        assert_eq!(sd.type_, "Patient");
175    }
176
177    #[test]
178    fn test_load_from_file_missing_url() {
179        let temp_dir = TempDir::new().unwrap();
180        let file_path = temp_dir.path().join("invalid.json");
181
182        let mut file = fs::File::create(&file_path).unwrap();
183        file.write_all(create_invalid_structure_definition().as_bytes())
184            .unwrap();
185
186        let result = StructureDefinitionLoader::load_from_file(&file_path);
187        assert!(result.is_err());
188        assert!(matches!(
189            result.unwrap_err(),
190            SnapshotError::InvalidStructureDefinition(_)
191        ));
192    }
193
194    #[test]
195    fn test_load_from_directory() {
196        let temp_dir = TempDir::new().unwrap();
197
198        let file1 = temp_dir.path().join("sd1.json");
199        fs::write(&file1, create_test_structure_definition()).unwrap();
200
201        let file2 = temp_dir.path().join("patient.json");
202        fs::write(&file2, create_non_structure_definition()).unwrap();
203
204        let file3 = temp_dir.path().join("readme.txt");
205        fs::write(&file3, "not json").unwrap();
206
207        let result = StructureDefinitionLoader::load_from_directory(temp_dir.path());
208        assert!(result.is_ok());
209
210        let sds = result.unwrap();
211        assert_eq!(sds.len(), 1);
212        assert_eq!(sds[0].name, "TestPatient");
213    }
214
215    #[test]
216    fn test_validate_structure_definition() {
217        let valid_sd = StructureDefinition {
218            url: "http://example.org/test".to_string(),
219            name: "Test".to_string(),
220            type_: "Patient".to_string(),
221            base_definition: None,
222            differential: None,
223            snapshot: None,
224        };
225
226        assert!(StructureDefinitionLoader::validate_structure_definition(&valid_sd).is_ok());
227
228        let invalid_sd = StructureDefinition {
229            url: "".to_string(),
230            name: "Test".to_string(),
231            type_: "Patient".to_string(),
232            base_definition: None,
233            differential: None,
234            snapshot: None,
235        };
236
237        assert!(StructureDefinitionLoader::validate_structure_definition(&invalid_sd).is_err());
238    }
239}