#[cfg(feature = "registry")]
use std::collections::VecDeque;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
#[cfg(feature = "registry")]
use octofhir_canonical_manager::{CanonicalManager, FcmConfig};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct Definition {
pub file: String,
pub json: Value,
}
#[derive(Debug, Clone, Default)]
pub struct Package {
pub name: String,
pub version: String,
pub fhir_versions: Vec<String>,
pub dependencies: BTreeMap<String, String>,
pub definitions: Vec<Definition>,
}
impl Package {
pub fn id(&self) -> String {
format!("{}@{}", self.name, self.version)
}
}
#[derive(Debug, Clone)]
pub struct Loaded {
pub package: Package,
pub dependencies: Vec<Package>,
}
pub fn is_core(name: &str) -> bool {
name.starts_with("hl7.fhir.r") && name.ends_with(".core")
}
pub fn holds_no_profiles(name: &str) -> bool {
name.starts_with("hl7.terminology")
|| matches!(
name,
"us.nlm.vsac" | "us.cdc.phinvads" | "ihe.formatcode.fhir"
)
}
pub fn base_dependencies(package: &Package) -> impl Iterator<Item = (&str, &str)> {
package
.dependencies
.iter()
.filter(|(name, _)| !is_core(name) && !holds_no_profiles(name))
.map(|(name, version)| (name.as_str(), version.as_str()))
}
pub fn missing_bases<'a>(
packages: impl IntoIterator<Item = &'a Package> + Clone,
) -> BTreeSet<String> {
let defined: BTreeSet<&str> = packages
.clone()
.into_iter()
.flat_map(|p| &p.definitions)
.filter_map(|d| d.json["url"].as_str())
.collect();
packages
.into_iter()
.flat_map(|p| &p.definitions)
.filter_map(|d| d.json["baseDefinition"].as_str())
.map(|url| url.split('|').next().unwrap_or(url))
.filter(|url| !url.starts_with(CORE_PREFIX) && !defined.contains(url))
.map(str::to_owned)
.collect()
}
const CORE_PREFIX: &str = "http://hl7.org/fhir/StructureDefinition/";
pub fn load(spec: &str) -> Result<Loaded, String> {
let path = Path::new(spec);
if path.is_dir() {
let package = read_folder(&package_folder(path))?;
return Ok(Loaded {
package,
dependencies: Vec::new(),
});
}
let Some((name, version)) = spec.split_once('@') else {
return Err(format!("`{spec}` is neither a folder nor name@version"));
};
from_registry(name, version)
}
#[cfg(not(feature = "registry"))]
fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
Err(format!(
"{name}@{version}: loading from the package registry needs the `registry` feature"
))
}
#[cfg(feature = "registry")]
fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("cannot start the runtime: {e}"))?;
runtime.block_on(async {
let config = FcmConfig::load().await.map_err(|e| e.to_string())?;
let manager = CanonicalManager::new(config)
.await
.map_err(|e| e.to_string())?;
let folder = installed_folder(&manager, name, version).await?;
let package = read_folder(&folder)?;
let mut dependencies: Vec<Package> = Vec::new();
let mut seen = BTreeSet::from([package.id()]);
let mut queue: VecDeque<(String, String)> = base_dependencies(&package)
.map(|(n, v)| (n.to_owned(), v.to_owned()))
.collect();
while !missing_bases(std::iter::once(&package).chain(&dependencies)).is_empty() {
let Some((dep_name, dep_version)) = queue.pop_front() else {
break;
};
if !seen.insert(format!("{dep_name}@{dep_version}")) {
continue;
}
match installed_folder(&manager, &dep_name, &dep_version).await {
Ok(folder) => {
let dependency = read_folder(&folder)?;
queue.extend(
base_dependencies(&dependency).map(|(n, v)| (n.to_owned(), v.to_owned())),
);
dependencies.push(dependency);
}
Err(error) => eprintln!("warning: {dep_name}@{dep_version}: {error}"),
}
}
Ok(Loaded {
package,
dependencies,
})
})
}
#[cfg(feature = "registry")]
async fn installed_folder(
manager: &CanonicalManager,
name: &str,
version: &str,
) -> Result<PathBuf, String> {
let spec = format!("{name}@{version}");
let installed = manager.list_packages().await.map_err(|e| e.to_string())?;
if !installed.iter().any(|p| p == &spec) {
manager
.install_package(name, version)
.await
.map_err(|e| format!("cannot install {spec}: {e}"))?;
}
let dir = manager
.package_dir(name, version)
.await
.map_err(|e| format!("{spec}: {e}"))?;
Ok(package_folder(&dir))
}
fn package_folder(path: &Path) -> PathBuf {
let inner = path.join("package");
if inner.is_dir() {
inner
} else {
path.to_path_buf()
}
}
pub fn read_folder(folder: &Path) -> Result<Package, String> {
let manifest: Value = std::fs::read_to_string(folder.join("package.json"))
.ok()
.and_then(|text| serde_json::from_str(&text).ok())
.unwrap_or(Value::Null);
let text = |value: &Value| value.as_str().unwrap_or_default().to_owned();
let mut package = Package {
name: text(&manifest["name"]),
version: text(&manifest["version"]),
fhir_versions: manifest["fhirVersions"]
.as_array()
.into_iter()
.flatten()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect(),
dependencies: manifest["dependencies"]
.as_object()
.into_iter()
.flatten()
.filter_map(|(name, version)| Some((name.clone(), version.as_str()?.to_owned())))
.collect(),
definitions: Vec::new(),
};
if package.name.is_empty() {
package.name = folder
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
}
let mut entries: Vec<PathBuf> = std::fs::read_dir(folder)
.map_err(|e| format!("{}: {e}", folder.display()))?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| path.extension().is_some_and(|e| e == "json"))
.collect();
entries.sort();
for path in entries {
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
if !text.contains("\"StructureDefinition\"") {
continue;
}
let Ok(json) = serde_json::from_str::<Value>(&text) else {
continue;
};
if json["resourceType"] == "StructureDefinition" {
package.definitions.push(Definition {
file: path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default(),
json,
});
}
}
Ok(package)
}