1use std::cmp::Ordering;
9use std::collections::BTreeMap;
10use std::collections::BTreeSet;
11use std::path::Path;
12use std::sync::Arc;
13use std::sync::OnceLock;
14
15use fhir_types::codec::DecodeError;
16use fhir_types::codec::Json;
17use fhir_types::codec::Value;
18use fhir_types::r4::code_system::CodeSystem;
19use fhir_types::r4::structure_definition::StructureDefinition;
20use fhir_types::r4::value_set::ValueSet;
21
22use crate::canonical::Canonical;
23use crate::canonical::compare_versions;
24use crate::package::Content;
25use crate::package::Package;
26use crate::package::PackageError;
27
28#[derive(Debug)]
30pub struct PackageSet {
31 packages: Vec<Package>,
32 structures: BTreeMap<String, Vec<Entry<StructureDefinition>>>,
33 value_sets: BTreeMap<String, Vec<Entry<ValueSet>>>,
34 code_systems: BTreeMap<String, Vec<Entry<CodeSystem>>>,
35 closures: Vec<BTreeSet<PackageId>>,
37}
38
39#[derive(Debug)]
41struct Entry<T> {
42 version: Option<String>,
43 package: String,
45 content: Content,
46 origin: PackageId,
48 decoded: OnceLock<Arc<T>>,
49}
50
51impl<T> Entry<T> {
52 fn new(version: Option<String>, package: String, content: Content, origin: PackageId) -> Self {
54 Self {
55 version,
56 package,
57 content,
58 origin,
59 decoded: OnceLock::new(),
60 }
61 }
62}
63
64#[derive(Debug, thiserror::Error)]
66pub enum LoadError {
67 #[error(transparent)]
69 Package(#[from] PackageError),
70 #[error("package {name}#{version} is given twice")]
72 DuplicatePackage {
73 name: String,
75 version: String,
77 },
78 #[error(
80 "{package}#{version} depends on {dependency}#{dependency_version}, which the set does not hold"
81 )]
82 MissingDependency {
83 package: String,
85 version: String,
87 dependency: String,
89 dependency_version: String,
91 },
92}
93
94#[derive(Debug, thiserror::Error)]
96pub enum ResolveError {
97 #[error("no resource {canonical} of the type asked for in the package set")]
99 NotFound {
100 canonical: String,
102 },
103 #[error("{canonical} names a contained resource, which the package set does not resolve")]
106 Fragment {
107 canonical: String,
109 },
110 #[error("{canonical} resolves to different definitions in {first} and {second}")]
112 Ambiguous {
113 canonical: String,
115 first: String,
117 second: String,
119 },
120 #[error("{canonical} in {package} cannot be read")]
122 Io {
123 canonical: String,
125 package: String,
127 #[source]
129 source: std::io::Error,
130 },
131 #[error("{canonical} in {package} is no JSON document")]
133 Json {
134 canonical: String,
136 package: String,
138 #[source]
140 source: serde_json::Error,
141 },
142 #[error("{canonical} in {package} does not decode into the R4 model of its type")]
144 Decode {
145 canonical: String,
147 package: String,
149 #[source]
151 source: DecodeError,
152 },
153}
154
155impl PackageSet {
156 pub fn load<P: AsRef<Path>>(paths: impl IntoIterator<Item = P>) -> Result<Self, LoadError> {
164 let packages = paths
165 .into_iter()
166 .map(|path| Package::read(path.as_ref()))
167 .collect::<Result<Vec<_>, _>>()?;
168 Self::new(packages)
169 }
170
171 pub fn new(mut packages: Vec<Package>) -> Result<Self, LoadError> {
183 packages.sort_by(|a, b| {
184 a.name()
185 .cmp(b.name())
186 .then_with(|| compare_versions(a.version(), b.version()))
187 });
188 for pair in packages.windows(2) {
189 if let [first, second] = pair
190 && first.name() == second.name()
191 && first.version() == second.version()
192 {
193 return Err(LoadError::DuplicatePackage {
194 name: first.name().to_owned(),
195 version: first.version().to_owned(),
196 });
197 }
198 }
199 for package in &packages {
200 for (dependency, wanted) in package.dependencies() {
201 let held = packages.iter().any(|candidate| {
202 candidate.name() == dependency && version_satisfies(candidate.version(), wanted)
203 });
204 if !held {
205 return Err(LoadError::MissingDependency {
206 package: package.name().to_owned(),
207 version: package.version().to_owned(),
208 dependency: dependency.clone(),
209 dependency_version: wanted.clone(),
210 });
211 }
212 }
213 }
214 let mut structures: BTreeMap<String, Vec<Entry<StructureDefinition>>> = BTreeMap::new();
215 let mut value_sets: BTreeMap<String, Vec<Entry<ValueSet>>> = BTreeMap::new();
216 let mut code_systems: BTreeMap<String, Vec<Entry<CodeSystem>>> = BTreeMap::new();
217 for (index, package) in packages.iter().enumerate() {
218 let label = format!("{}#{}", package.name(), package.version());
219 let origin = PackageId(index);
220 for resource in package.resources() {
221 let Some(url) = &resource.url else { continue };
222 let version = resource.version.clone();
223 let source = format!("{label}/{}", resource.file);
224 let content = resource.content.clone();
225 match resource.type_name.as_str() {
226 "StructureDefinition" => structures
227 .entry(url.clone())
228 .or_default()
229 .push(Entry::new(version, source, content, origin)),
230 "ValueSet" => value_sets
231 .entry(url.clone())
232 .or_default()
233 .push(Entry::new(version, source, content, origin)),
234 "CodeSystem" => code_systems
235 .entry(url.clone())
236 .or_default()
237 .push(Entry::new(version, source, content, origin)),
238 _ => {}
239 }
240 }
241 }
242 let closures = (0..packages.len())
243 .map(|index| dependency_closure(&packages, index))
244 .collect();
245 Ok(Self {
246 packages,
247 structures,
248 value_sets,
249 code_systems,
250 closures,
251 })
252 }
253
254 #[must_use]
256 pub fn packages(&self) -> &[Package] {
257 &self.packages
258 }
259
260 pub fn structure_definition(
274 &self,
275 canonical: &str,
276 ) -> Result<Arc<StructureDefinition>, ResolveError> {
277 self.structure_definition_in(canonical, None)
278 .map(|(definition, _)| definition)
279 }
280
281 pub fn value_set(&self, canonical: &str) -> Result<Arc<ValueSet>, ResolveError> {
288 self.value_set_in(canonical, None)
289 .map(|(value_set, _)| value_set)
290 }
291
292 pub fn code_system(&self, canonical: &str) -> Result<Arc<CodeSystem>, ResolveError> {
299 self.code_system_in(canonical, None)
300 .map(|(code_system, _)| code_system)
301 }
302
303 pub(crate) fn structure_definition_in(
306 &self,
307 canonical: &str,
308 scope: Option<PackageId>,
309 ) -> Result<(Arc<StructureDefinition>, PackageId), ResolveError> {
310 resolve(
311 &self.structures,
312 canonical,
313 "StructureDefinition",
314 self.closure(scope),
315 )
316 }
317
318 pub(crate) fn value_set_in(
320 &self,
321 canonical: &str,
322 scope: Option<PackageId>,
323 ) -> Result<(Arc<ValueSet>, PackageId), ResolveError> {
324 resolve(&self.value_sets, canonical, "ValueSet", self.closure(scope))
325 }
326
327 pub(crate) fn code_system_in(
329 &self,
330 canonical: &str,
331 scope: Option<PackageId>,
332 ) -> Result<(Arc<CodeSystem>, PackageId), ResolveError> {
333 resolve(
334 &self.code_systems,
335 canonical,
336 "CodeSystem",
337 self.closure(scope),
338 )
339 }
340
341 fn closure(&self, scope: Option<PackageId>) -> Option<&BTreeSet<PackageId>> {
343 scope.and_then(|PackageId(index)| self.closures.get(index))
344 }
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
349pub(crate) struct PackageId(usize);
350
351fn dependency_closure(packages: &[Package], start: usize) -> BTreeSet<PackageId> {
355 let mut seen = BTreeSet::new();
356 let mut pending = vec![start];
357 while let Some(index) = pending.pop() {
358 if !seen.insert(PackageId(index)) {
359 continue;
360 }
361 let Some(package) = packages.get(index) else {
362 continue;
363 };
364 for (dependency, wanted) in package.dependencies() {
365 let best = packages
366 .iter()
367 .enumerate()
368 .filter(|(_, candidate)| {
369 candidate.name() == dependency && version_satisfies(candidate.version(), wanted)
370 })
371 .max_by(|(_, a), (_, b)| compare_versions(a.version(), b.version()));
372 if let Some((found, _)) = best {
373 pending.push(found);
374 }
375 }
376 }
377 seen
378}
379
380fn resolve<T: Json>(
383 index: &BTreeMap<String, Vec<Entry<T>>>,
384 canonical: &str,
385 kind: &str,
386 closure: Option<&BTreeSet<PackageId>>,
387) -> Result<(Arc<T>, PackageId), ResolveError> {
388 let parsed = Canonical::parse(canonical);
389 if parsed.fragment().is_some() {
390 return Err(ResolveError::Fragment {
391 canonical: canonical.to_owned(),
392 });
393 }
394 let not_found = || ResolveError::NotFound {
395 canonical: canonical.to_owned(),
396 };
397 let all = index.get(parsed.url()).ok_or_else(not_found)?;
398 let scoped: Vec<&Entry<T>> = all
399 .iter()
400 .filter(|entry| closure.is_none_or(|closure| closure.contains(&entry.origin)))
401 .collect();
402 let entries: Vec<&Entry<T>> = if scoped.is_empty() || parsed.version().is_some() {
405 all.iter().collect()
406 } else {
407 scoped
408 };
409 let wanted = if let Some(version) = parsed.version() {
410 Some(version)
411 } else {
412 entries
413 .iter()
414 .max_by(|a, b| order_versions(a.version.as_deref(), b.version.as_deref()))
415 .ok_or_else(not_found)?
416 .version
417 .as_deref()
418 };
419 let matching: Vec<&Entry<T>> = entries
420 .iter()
421 .copied()
422 .filter(|entry| entry.version.as_deref() == wanted)
423 .collect();
424 let (entry, others) = matching.split_first().ok_or_else(not_found)?;
425 let origin = entry.origin;
426 if let Some(decoded) = entry.decoded.get() {
427 return Ok((Arc::clone(decoded), origin));
428 }
429 let bytes = read(canonical, entry)?;
430 for other in others {
431 if read(canonical, other)? != bytes {
434 return Err(ResolveError::Ambiguous {
435 canonical: canonical.to_owned(),
436 first: entry.package.clone(),
437 second: other.package.clone(),
438 });
439 }
440 }
441 let decoded = Arc::new(decode(canonical, entry, &bytes, kind)?);
442 Ok((Arc::clone(entry.decoded.get_or_init(|| decoded)), origin))
443}
444
445fn read<T>(canonical: &str, entry: &Entry<T>) -> Result<Arc<[u8]>, ResolveError> {
447 entry.content.read().map_err(|source| ResolveError::Io {
448 canonical: canonical.to_owned(),
449 package: entry.package.clone(),
450 source,
451 })
452}
453
454fn decode<T: Json>(
456 canonical: &str,
457 entry: &Entry<T>,
458 bytes: &[u8],
459 kind: &str,
460) -> Result<T, ResolveError> {
461 let document: Value = serde_json::from_slice(bytes).map_err(|source| ResolveError::Json {
462 canonical: canonical.to_owned(),
463 package: entry.package.clone(),
464 source,
465 })?;
466 let mut path = fhir_types::codec::Path::root(kind);
467 let decode = |source| ResolveError::Decode {
468 canonical: canonical.to_owned(),
469 package: entry.package.clone(),
470 source,
471 };
472 let object = fhir_types::codec::expect_object(&document, &path).map_err(decode)?;
473 T::from_json(object, &mut path).map_err(decode)
474}
475
476fn order_versions(left: Option<&str>, right: Option<&str>) -> Ordering {
478 match (left, right) {
479 (None, None) => Ordering::Equal,
480 (None, Some(_)) => Ordering::Less,
481 (Some(_), None) => Ordering::Greater,
482 (Some(left), Some(right)) => compare_versions(left, right),
483 }
484}
485
486fn version_satisfies(version: &str, wanted: &str) -> bool {
490 match wanted.strip_suffix(".x") {
491 Some(prefix) => version
492 .strip_prefix(prefix)
493 .and_then(|rest| rest.strip_prefix('.'))
494 .is_some_and(|patch| !patch.is_empty() && !patch.contains('.')),
495 None => version == wanted,
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use std::path::Path;
502
503 use super::PackageId;
504 use super::PackageSet;
505 use super::version_satisfies;
506
507 #[test]
508 fn a_patch_wildcard_accepts_any_patch_of_its_minor() {
509 assert!(version_satisfies("4.0.1", "4.0.1"));
510 assert!(version_satisfies("4.0.1", "4.0.x"));
511 assert!(!version_satisfies("4.1.0", "4.0.x"));
512 assert!(!version_satisfies("4.0.1", "4.0.2"));
513 }
514
515 #[test]
518 fn a_canonical_eps_names_resolves_to_the_eu_base_version_eps_depends_on() {
519 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/specs/fhir-packages");
520 if !root
521 .join("hl7.fhir.eu.eps#1.0.0-ballot/package/package.json")
522 .is_file()
523 {
524 return;
525 }
526 let mut paths = vec![
527 Path::new(env!("CARGO_MANIFEST_DIR"))
528 .join("../../tools/fhir-codegen/vendor/hl7.fhir.r4.core"),
529 ];
530 for entry in std::fs::read_dir(&root).expect("the fetched packages") {
531 let path = entry.expect("a fetched package").path();
532 if path.join("package/package.json").is_file() {
533 paths.push(path);
534 }
535 }
536 let set = PackageSet::load(paths).expect("the EU closure");
537 let eps = set
538 .packages()
539 .iter()
540 .position(|package| package.name() == "hl7.fhir.eu.eps")
541 .expect("the EPS package");
542 let canonical = "http://hl7.eu/fhir/base/StructureDefinition/patient-eu-core";
543 let (scoped, _) = set
544 .structure_definition_in(canonical, Some(PackageId(eps)))
545 .expect("patient-eu-core in the EPS closure");
546 assert_eq!(
547 scoped
548 .version
549 .as_ref()
550 .and_then(|version| version.value.as_deref()),
551 Some("2.0.0")
552 );
553 let latest = set
554 .structure_definition(canonical)
555 .expect("patient-eu-core");
556 assert_eq!(
557 latest
558 .version
559 .as_ref()
560 .and_then(|version| version.value.as_deref()),
561 Some("2.0.1")
562 );
563 }
564}