use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::Path;
use std::sync::Arc;
use std::sync::OnceLock;
use fhir_types::codec::DecodeError;
use fhir_types::codec::Json;
use fhir_types::codec::Value;
use crate::canonical::Canonical;
use crate::canonical::compare_versions;
use crate::package::Content;
use crate::package::Package;
use crate::package::PackageError;
use crate::release::R4;
use crate::release::Release;
#[derive(Debug)]
pub struct PackageSet<R: Release = R4> {
packages: Vec<Package>,
structures: BTreeMap<String, Vec<Entry<R::StructureDefinition>>>,
value_sets: BTreeMap<String, Vec<Entry<R::ValueSet>>>,
code_systems: BTreeMap<String, Vec<Entry<R::CodeSystem>>>,
closures: Vec<BTreeSet<PackageId>>,
}
#[derive(Debug)]
struct Entry<T> {
version: Option<String>,
package: String,
content: Content,
origin: PackageId,
decoded: OnceLock<Arc<T>>,
}
impl<T> Entry<T> {
fn new(version: Option<String>, package: String, content: Content, origin: PackageId) -> Self {
Self {
version,
package,
content,
origin,
decoded: OnceLock::new(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error(transparent)]
Package(#[from] PackageError),
#[error("package {name}#{version} is given twice")]
DuplicatePackage {
name: String,
version: String,
},
#[error(
"{package}#{version} depends on {dependency}#{dependency_version}, which the set does not hold"
)]
MissingDependency {
package: String,
version: String,
dependency: String,
dependency_version: String,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ResolveError {
#[error("no resource {canonical} of the type asked for in the package set")]
NotFound {
canonical: String,
},
#[error("{canonical} names a contained resource, which the package set does not resolve")]
Fragment {
canonical: String,
},
#[error("{canonical} resolves to different definitions in {first} and {second}")]
Ambiguous {
canonical: String,
first: String,
second: String,
},
#[error("{canonical} in {package} cannot be read")]
Io {
canonical: String,
package: String,
#[source]
source: std::io::Error,
},
#[error("{canonical} in {package} is no JSON document")]
Json {
canonical: String,
package: String,
#[source]
source: serde_json::Error,
},
#[error("{canonical} in {package} does not decode into the release model of its type")]
Decode {
canonical: String,
package: String,
#[source]
source: DecodeError,
},
}
impl PackageSet<R4> {
pub fn load<P: AsRef<Path>>(paths: impl IntoIterator<Item = P>) -> Result<Self, LoadError> {
Self::load_release(paths)
}
pub fn new(packages: Vec<Package>) -> Result<Self, LoadError> {
Self::new_release(packages)
}
}
impl<R: Release> PackageSet<R> {
pub fn load_release<P: AsRef<Path>>(
paths: impl IntoIterator<Item = P>,
) -> Result<Self, LoadError> {
let packages = paths
.into_iter()
.map(|path| Package::read(path.as_ref()))
.collect::<Result<Vec<_>, _>>()?;
Self::new_release(packages)
}
pub fn new_release(mut packages: Vec<Package>) -> Result<Self, LoadError> {
packages.sort_by(|a, b| {
a.name()
.cmp(b.name())
.then_with(|| compare_versions(a.version(), b.version()))
});
for pair in packages.windows(2) {
if let [first, second] = pair
&& first.name() == second.name()
&& first.version() == second.version()
{
return Err(LoadError::DuplicatePackage {
name: first.name().to_owned(),
version: first.version().to_owned(),
});
}
}
for package in &packages {
for (dependency, wanted) in package.dependencies() {
let held = packages.iter().any(|candidate| {
candidate.name() == dependency && version_satisfies(candidate.version(), wanted)
});
if !held {
return Err(LoadError::MissingDependency {
package: package.name().to_owned(),
version: package.version().to_owned(),
dependency: dependency.clone(),
dependency_version: wanted.clone(),
});
}
}
}
let mut structures: BTreeMap<String, Vec<Entry<R::StructureDefinition>>> = BTreeMap::new();
let mut value_sets: BTreeMap<String, Vec<Entry<R::ValueSet>>> = BTreeMap::new();
let mut code_systems: BTreeMap<String, Vec<Entry<R::CodeSystem>>> = BTreeMap::new();
for (index, package) in packages.iter().enumerate() {
let label = format!("{}#{}", package.name(), package.version());
let origin = PackageId(index);
for resource in package.resources() {
let Some(url) = &resource.url else { continue };
let version = resource.version.clone();
let source = format!("{label}/{}", resource.file);
let content = resource.content.clone();
match resource.type_name.as_str() {
"StructureDefinition" => structures
.entry(url.clone())
.or_default()
.push(Entry::new(version, source, content, origin)),
"ValueSet" => value_sets
.entry(url.clone())
.or_default()
.push(Entry::new(version, source, content, origin)),
"CodeSystem" => code_systems
.entry(url.clone())
.or_default()
.push(Entry::new(version, source, content, origin)),
_ => {}
}
}
}
let closures = (0..packages.len())
.map(|index| dependency_closure(&packages, index))
.collect();
Ok(Self {
packages,
structures,
value_sets,
code_systems,
closures,
})
}
#[must_use]
pub fn packages(&self) -> &[Package] {
&self.packages
}
pub fn structure_definition(
&self,
canonical: &str,
) -> Result<Arc<R::StructureDefinition>, ResolveError> {
self.structure_definition_in(canonical, None)
.map(|(definition, _)| definition)
}
pub fn value_set(&self, canonical: &str) -> Result<Arc<R::ValueSet>, ResolveError> {
self.value_set_in(canonical, None)
.map(|(value_set, _)| value_set)
}
pub fn code_system(&self, canonical: &str) -> Result<Arc<R::CodeSystem>, ResolveError> {
self.code_system_in(canonical, None)
.map(|(code_system, _)| code_system)
}
pub(crate) fn structure_definition_in(
&self,
canonical: &str,
scope: Option<PackageId>,
) -> Result<(Arc<R::StructureDefinition>, PackageId), ResolveError> {
resolve(
&self.packages,
&self.structures,
canonical,
"StructureDefinition",
self.closure(scope),
)
}
pub(crate) fn value_set_in(
&self,
canonical: &str,
scope: Option<PackageId>,
) -> Result<(Arc<R::ValueSet>, PackageId), ResolveError> {
resolve(
&self.packages,
&self.value_sets,
canonical,
"ValueSet",
self.closure(scope),
)
}
pub(crate) fn code_system_in(
&self,
canonical: &str,
scope: Option<PackageId>,
) -> Result<(Arc<R::CodeSystem>, PackageId), ResolveError> {
resolve(
&self.packages,
&self.code_systems,
canonical,
"CodeSystem",
self.closure(scope),
)
}
fn closure(&self, scope: Option<PackageId>) -> Option<&BTreeSet<PackageId>> {
scope.and_then(|PackageId(index)| self.closures.get(index))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct PackageId(usize);
fn dependency_closure(packages: &[Package], start: usize) -> BTreeSet<PackageId> {
let mut seen = BTreeSet::new();
let mut pending = vec![start];
while let Some(index) = pending.pop() {
if !seen.insert(PackageId(index)) {
continue;
}
let Some(package) = packages.get(index) else {
continue;
};
for (dependency, wanted) in package.dependencies() {
let best = packages
.iter()
.enumerate()
.filter(|(_, candidate)| {
candidate.name() == dependency && version_satisfies(candidate.version(), wanted)
})
.max_by(|(_, a), (_, b)| compare_versions(a.version(), b.version()));
if let Some((found, _)) = best {
pending.push(found);
}
}
}
seen
}
fn resolve<T: Json>(
packages: &[Package],
index: &BTreeMap<String, Vec<Entry<T>>>,
canonical: &str,
kind: &str,
closure: Option<&BTreeSet<PackageId>>,
) -> Result<(Arc<T>, PackageId), ResolveError> {
let parsed = Canonical::parse(canonical);
if parsed.fragment().is_some() {
return Err(ResolveError::Fragment {
canonical: canonical.to_owned(),
});
}
let not_found = || ResolveError::NotFound {
canonical: canonical.to_owned(),
};
let all = index.get(parsed.url()).ok_or_else(not_found)?;
let scoped: Vec<&Entry<T>> = all
.iter()
.filter(|entry| closure.is_none_or(|closure| closure.contains(&entry.origin)))
.collect();
let entries: Vec<&Entry<T>> = if scoped.is_empty() || parsed.version().is_some() {
all.iter().collect()
} else {
scoped
};
let wanted = if let Some(version) = parsed.version() {
Some(version)
} else {
entries
.iter()
.max_by(|a, b| order_versions(a.version.as_deref(), b.version.as_deref()))
.ok_or_else(not_found)?
.version
.as_deref()
};
let matching: Vec<&Entry<T>> = entries
.iter()
.copied()
.filter(|entry| entry.version.as_deref() == wanted)
.collect();
let package = |entry: &Entry<T>| packages.get(entry.origin.0);
let matching: Vec<&Entry<T>> = matching
.iter()
.copied()
.filter(|entry| {
!matching
.iter()
.any(|other| match (package(entry), package(other)) {
(Some(held), Some(rival)) => {
held.name() == rival.name()
&& compare_versions(rival.version(), held.version())
== Ordering::Greater
}
_ => false,
})
})
.collect();
let (entry, others) = matching.split_first().ok_or_else(not_found)?;
let origin = entry.origin;
if let Some(decoded) = entry.decoded.get() {
return Ok((Arc::clone(decoded), origin));
}
let bytes = read(canonical, entry)?;
for other in others {
if read(canonical, other)? != bytes {
return Err(ResolveError::Ambiguous {
canonical: canonical.to_owned(),
first: entry.package.clone(),
second: other.package.clone(),
});
}
}
let decoded = Arc::new(decode(canonical, entry, &bytes, kind)?);
Ok((Arc::clone(entry.decoded.get_or_init(|| decoded)), origin))
}
fn read<T>(canonical: &str, entry: &Entry<T>) -> Result<Arc<[u8]>, ResolveError> {
entry.content.read().map_err(|source| ResolveError::Io {
canonical: canonical.to_owned(),
package: entry.package.clone(),
source,
})
}
fn decode<T: Json>(
canonical: &str,
entry: &Entry<T>,
bytes: &[u8],
kind: &str,
) -> Result<T, ResolveError> {
let document: Value = serde_json::from_slice(bytes).map_err(|source| ResolveError::Json {
canonical: canonical.to_owned(),
package: entry.package.clone(),
source,
})?;
let mut path = fhir_types::codec::Path::root(kind);
let decode = |source| ResolveError::Decode {
canonical: canonical.to_owned(),
package: entry.package.clone(),
source,
};
let object = fhir_types::codec::expect_object(&document, &path).map_err(decode)?;
T::from_json(object, &mut path).map_err(decode)
}
fn order_versions(left: Option<&str>, right: Option<&str>) -> Ordering {
match (left, right) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(left), Some(right)) => compare_versions(left, right),
}
}
fn version_satisfies(version: &str, wanted: &str) -> bool {
match wanted.strip_suffix(".x") {
Some(prefix) => version
.strip_prefix(prefix)
.and_then(|rest| rest.strip_prefix('.'))
.is_some_and(|patch| !patch.is_empty() && !patch.contains('.')),
None => version == wanted,
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::PackageId;
use super::PackageSet;
use super::version_satisfies;
#[test]
fn a_patch_wildcard_accepts_any_patch_of_its_minor() {
assert!(version_satisfies("4.0.1", "4.0.1"));
assert!(version_satisfies("4.0.1", "4.0.x"));
assert!(!version_satisfies("4.1.0", "4.0.x"));
assert!(!version_satisfies("4.0.1", "4.0.2"));
}
#[test]
fn a_canonical_eps_names_resolves_to_the_eu_base_version_eps_depends_on() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/specs/fhir-packages");
if !root
.join("hl7.fhir.eu.eps#1.0.0-ballot/package/package.json")
.is_file()
{
assert_ne!(
std::env::var("CI").as_deref(),
Ok("true"),
"the profile packages are absent under CI; run scripts/vendor/profile-packages.sh"
);
return;
}
let mut paths = vec![
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tools/fhir-codegen/vendor/hl7.fhir.r4.core"),
];
for entry in std::fs::read_dir(&root).expect("the fetched packages") {
let path = entry.expect("a fetched package").path();
if path.join("package/package.json").is_file() {
paths.push(path);
}
}
let set = PackageSet::load(paths).expect("the EU closure");
let eps = set
.packages()
.iter()
.position(|package| package.name() == "hl7.fhir.eu.eps")
.expect("the EPS package");
let canonical = "http://hl7.eu/fhir/base/StructureDefinition/patient-eu-core";
let (scoped, _) = set
.structure_definition_in(canonical, Some(PackageId(eps)))
.expect("patient-eu-core in the EPS closure");
assert_eq!(
scoped
.version
.as_ref()
.and_then(|version| version.value.as_deref()),
Some("2.0.0")
);
let latest = set
.structure_definition(canonical)
.expect("patient-eu-core");
assert_eq!(
latest
.version
.as_ref()
.and_then(|version| version.value.as_deref()),
Some("2.0.1")
);
}
}