Skip to main content

rh_foundation/snapshot/
sd_loader.rs

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