1use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8#[cfg(feature = "registry")]
9use octofhir_canonical_manager::{CanonicalManager, FcmConfig};
10use serde_json::Value;
11
12#[derive(Debug, Clone)]
14pub struct Definition {
15 pub file: String,
17 pub json: Value,
18}
19
20#[derive(Debug, Clone, Default)]
22pub struct Package {
23 pub name: String,
24 pub version: String,
25 pub fhir_versions: Vec<String>,
27 pub dependencies: BTreeMap<String, String>,
29 pub definitions: Vec<Definition>,
30}
31
32impl Package {
33 pub fn id(&self) -> String {
35 format!("{}@{}", self.name, self.version)
36 }
37}
38
39#[derive(Debug, Clone)]
42pub struct Loaded {
43 pub package: Package,
44 pub dependencies: Vec<Package>,
45}
46
47pub fn is_core(name: &str) -> bool {
49 name.starts_with("hl7.fhir.r") && name.ends_with(".core")
50}
51
52pub fn load(spec: &str) -> Result<Loaded, String> {
55 let path = Path::new(spec);
56 if path.is_dir() {
57 let package = read_folder(&package_folder(path))?;
58 return Ok(Loaded {
59 package,
60 dependencies: Vec::new(),
61 });
62 }
63 let Some((name, version)) = spec.split_once('@') else {
64 return Err(format!("`{spec}` is neither a folder nor name@version"));
65 };
66 from_registry(name, version)
67}
68
69#[cfg(not(feature = "registry"))]
70fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
71 Err(format!(
72 "{name}@{version}: loading from the package registry needs the `registry` feature"
73 ))
74}
75
76#[cfg(feature = "registry")]
79fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
80 let runtime = tokio::runtime::Builder::new_current_thread()
81 .enable_all()
82 .build()
83 .map_err(|e| format!("cannot start the runtime: {e}"))?;
84 runtime.block_on(async {
85 let config = FcmConfig::load().await.map_err(|e| e.to_string())?;
86 let manager = CanonicalManager::new(config)
87 .await
88 .map_err(|e| e.to_string())?;
89 let folder = installed_folder(&manager, name, version).await?;
90 let package = read_folder(&folder)?;
91 let mut dependencies = Vec::new();
92 for (dep_name, dep_version) in &package.dependencies {
93 if is_core(dep_name) {
94 continue;
95 }
96 match installed_folder(&manager, dep_name, dep_version).await {
98 Ok(folder) => dependencies.push(read_folder(&folder)?),
99 Err(error) => eprintln!("warning: {dep_name}@{dep_version}: {error}"),
100 }
101 }
102 Ok(Loaded {
103 package,
104 dependencies,
105 })
106 })
107}
108
109#[cfg(feature = "registry")]
110async fn installed_folder(
111 manager: &CanonicalManager,
112 name: &str,
113 version: &str,
114) -> Result<PathBuf, String> {
115 let spec = format!("{name}@{version}");
116 let installed = manager.list_packages().await.map_err(|e| e.to_string())?;
117 if !installed.iter().any(|p| p == &spec) {
118 manager
119 .install_package(name, version)
120 .await
121 .map_err(|e| format!("cannot install {spec}: {e}"))?;
122 }
123 let dir = manager
124 .package_dir(name, version)
125 .await
126 .map_err(|e| format!("{spec}: {e}"))?;
127 Ok(package_folder(&dir))
128}
129
130fn package_folder(path: &Path) -> PathBuf {
132 let inner = path.join("package");
133 if inner.is_dir() {
134 inner
135 } else {
136 path.to_path_buf()
137 }
138}
139
140pub fn read_folder(folder: &Path) -> Result<Package, String> {
142 let manifest: Value = std::fs::read_to_string(folder.join("package.json"))
143 .ok()
144 .and_then(|text| serde_json::from_str(&text).ok())
145 .unwrap_or(Value::Null);
146 let text = |value: &Value| value.as_str().unwrap_or_default().to_owned();
147 let mut package = Package {
148 name: text(&manifest["name"]),
149 version: text(&manifest["version"]),
150 fhir_versions: manifest["fhirVersions"]
151 .as_array()
152 .into_iter()
153 .flatten()
154 .filter_map(|v| v.as_str().map(str::to_owned))
155 .collect(),
156 dependencies: manifest["dependencies"]
157 .as_object()
158 .into_iter()
159 .flatten()
160 .filter_map(|(name, version)| Some((name.clone(), version.as_str()?.to_owned())))
161 .collect(),
162 definitions: Vec::new(),
163 };
164 if package.name.is_empty() {
165 package.name = folder
166 .file_name()
167 .map(|name| name.to_string_lossy().into_owned())
168 .unwrap_or_default();
169 }
170 let mut entries: Vec<PathBuf> = std::fs::read_dir(folder)
171 .map_err(|e| format!("{}: {e}", folder.display()))?
172 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
173 .filter(|path| path.extension().is_some_and(|e| e == "json"))
174 .collect();
175 entries.sort();
176 for path in entries {
177 let Ok(text) = std::fs::read_to_string(&path) else {
178 continue;
179 };
180 let Ok(json) = serde_json::from_str::<Value>(&text) else {
181 continue;
182 };
183 if json["resourceType"] == "StructureDefinition" {
184 package.definitions.push(Definition {
185 file: path
186 .file_name()
187 .map(|name| name.to_string_lossy().into_owned())
188 .unwrap_or_default(),
189 json,
190 });
191 }
192 }
193 Ok(package)
194}