1#[cfg(feature = "registry")]
6use std::collections::VecDeque;
7use std::collections::{BTreeMap, BTreeSet};
8use std::path::{Path, PathBuf};
9
10#[cfg(feature = "registry")]
11use octofhir_canonical_manager::{CanonicalManager, FcmConfig};
12use serde_json::Value;
13
14#[derive(Debug, Clone)]
16pub struct Definition {
17 pub file: String,
19 pub json: Value,
20}
21
22#[derive(Debug, Clone, Default)]
24pub struct Package {
25 pub name: String,
26 pub version: String,
27 pub fhir_versions: Vec<String>,
29 pub dependencies: BTreeMap<String, String>,
31 pub definitions: Vec<Definition>,
32}
33
34impl Package {
35 pub fn id(&self) -> String {
37 format!("{}@{}", self.name, self.version)
38 }
39}
40
41#[derive(Debug, Clone)]
44pub struct Loaded {
45 pub package: Package,
46 pub dependencies: Vec<Package>,
47}
48
49pub fn is_core(name: &str) -> bool {
51 name.starts_with("hl7.fhir.r") && name.ends_with(".core")
52}
53
54pub fn holds_no_profiles(name: &str) -> bool {
57 name.starts_with("hl7.terminology")
58 || matches!(
59 name,
60 "us.nlm.vsac" | "us.cdc.phinvads" | "ihe.formatcode.fhir"
61 )
62}
63
64pub fn base_dependencies(package: &Package) -> impl Iterator<Item = (&str, &str)> {
66 package
67 .dependencies
68 .iter()
69 .filter(|(name, _)| !is_core(name) && !holds_no_profiles(name))
70 .map(|(name, version)| (name.as_str(), version.as_str()))
71}
72
73pub fn missing_bases<'a>(
75 packages: impl IntoIterator<Item = &'a Package> + Clone,
76) -> BTreeSet<String> {
77 let defined: BTreeSet<&str> = packages
78 .clone()
79 .into_iter()
80 .flat_map(|p| &p.definitions)
81 .filter_map(|d| d.json["url"].as_str())
82 .collect();
83 packages
84 .into_iter()
85 .flat_map(|p| &p.definitions)
86 .filter_map(|d| d.json["baseDefinition"].as_str())
87 .map(|url| url.split('|').next().unwrap_or(url))
88 .filter(|url| !url.starts_with(CORE_PREFIX) && !defined.contains(url))
89 .map(str::to_owned)
90 .collect()
91}
92
93const CORE_PREFIX: &str = "http://hl7.org/fhir/StructureDefinition/";
94
95pub fn load(spec: &str) -> Result<Loaded, String> {
98 let path = Path::new(spec);
99 if path.is_dir() {
100 let package = read_folder(&package_folder(path))?;
101 return Ok(Loaded {
102 package,
103 dependencies: Vec::new(),
104 });
105 }
106 let Some((name, version)) = spec.split_once('@') else {
107 return Err(format!("`{spec}` is neither a folder nor name@version"));
108 };
109 from_registry(name, version)
110}
111
112#[cfg(not(feature = "registry"))]
113fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
114 Err(format!(
115 "{name}@{version}: loading from the package registry needs the `registry` feature"
116 ))
117}
118
119#[cfg(feature = "registry")]
122fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
123 let runtime = tokio::runtime::Builder::new_current_thread()
124 .enable_all()
125 .build()
126 .map_err(|e| format!("cannot start the runtime: {e}"))?;
127 runtime.block_on(async {
128 let config = FcmConfig::load().await.map_err(|e| e.to_string())?;
129 let manager = CanonicalManager::new(config)
130 .await
131 .map_err(|e| e.to_string())?;
132 let folder = installed_folder(&manager, name, version).await?;
133 let package = read_folder(&folder)?;
134 let mut dependencies: Vec<Package> = Vec::new();
136 let mut seen = BTreeSet::from([package.id()]);
137 let mut queue: VecDeque<(String, String)> = base_dependencies(&package)
138 .map(|(n, v)| (n.to_owned(), v.to_owned()))
139 .collect();
140 while !missing_bases(std::iter::once(&package).chain(&dependencies)).is_empty() {
141 let Some((dep_name, dep_version)) = queue.pop_front() else {
142 break;
143 };
144 if !seen.insert(format!("{dep_name}@{dep_version}")) {
145 continue;
146 }
147 match installed_folder(&manager, &dep_name, &dep_version).await {
149 Ok(folder) => {
150 let dependency = read_folder(&folder)?;
151 queue.extend(
152 base_dependencies(&dependency).map(|(n, v)| (n.to_owned(), v.to_owned())),
153 );
154 dependencies.push(dependency);
155 }
156 Err(error) => eprintln!("warning: {dep_name}@{dep_version}: {error}"),
157 }
158 }
159 Ok(Loaded {
160 package,
161 dependencies,
162 })
163 })
164}
165
166#[cfg(feature = "registry")]
167async fn installed_folder(
168 manager: &CanonicalManager,
169 name: &str,
170 version: &str,
171) -> Result<PathBuf, String> {
172 let spec = format!("{name}@{version}");
173 let installed = manager.list_packages().await.map_err(|e| e.to_string())?;
174 if !installed.iter().any(|p| p == &spec) {
175 manager
176 .install_package(name, version)
177 .await
178 .map_err(|e| format!("cannot install {spec}: {e}"))?;
179 }
180 let dir = manager
181 .package_dir(name, version)
182 .await
183 .map_err(|e| format!("{spec}: {e}"))?;
184 Ok(package_folder(&dir))
185}
186
187fn package_folder(path: &Path) -> PathBuf {
189 let inner = path.join("package");
190 if inner.is_dir() {
191 inner
192 } else {
193 path.to_path_buf()
194 }
195}
196
197pub fn read_folder(folder: &Path) -> Result<Package, String> {
199 let manifest: Value = std::fs::read_to_string(folder.join("package.json"))
200 .ok()
201 .and_then(|text| serde_json::from_str(&text).ok())
202 .unwrap_or(Value::Null);
203 let text = |value: &Value| value.as_str().unwrap_or_default().to_owned();
204 let mut package = Package {
205 name: text(&manifest["name"]),
206 version: text(&manifest["version"]),
207 fhir_versions: manifest["fhirVersions"]
208 .as_array()
209 .into_iter()
210 .flatten()
211 .filter_map(|v| v.as_str().map(str::to_owned))
212 .collect(),
213 dependencies: manifest["dependencies"]
214 .as_object()
215 .into_iter()
216 .flatten()
217 .filter_map(|(name, version)| Some((name.clone(), version.as_str()?.to_owned())))
218 .collect(),
219 definitions: Vec::new(),
220 };
221 if package.name.is_empty() {
222 package.name = folder
223 .file_name()
224 .map(|name| name.to_string_lossy().into_owned())
225 .unwrap_or_default();
226 }
227 let mut entries: Vec<PathBuf> = std::fs::read_dir(folder)
228 .map_err(|e| format!("{}: {e}", folder.display()))?
229 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
230 .filter(|path| path.extension().is_some_and(|e| e == "json"))
231 .collect();
232 entries.sort();
233 for path in entries {
234 let Ok(text) = std::fs::read_to_string(&path) else {
235 continue;
236 };
237 if !text.contains("\"StructureDefinition\"") {
240 continue;
241 }
242 let Ok(json) = serde_json::from_str::<Value>(&text) else {
243 continue;
244 };
245 if json["resourceType"] == "StructureDefinition" {
246 package.definitions.push(Definition {
247 file: path
248 .file_name()
249 .map(|name| name.to_string_lossy().into_owned())
250 .unwrap_or_default(),
251 json,
252 });
253 }
254 }
255 Ok(package)
256}