rh_foundation/snapshot/
sd_loader.rs1use 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}