use std::collections::BTreeMap;
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 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::new();
for (dep_name, dep_version) in &package.dependencies {
if is_core(dep_name) {
continue;
}
match installed_folder(&manager, dep_name, dep_version).await {
Ok(folder) => dependencies.push(read_folder(&folder)?),
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;
};
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)
}