#[cfg(feature = "discharge")]
pub mod discharge;
#[cfg(feature = "dispensation")]
pub mod dispensation;
#[cfg(feature = "imaging")]
pub mod imaging;
#[cfg(feature = "laboratory")]
pub mod laboratory;
#[cfg(feature = "patient-summary")]
pub mod patient_summary;
#[cfg(feature = "prescription")]
pub mod prescription;
use std::collections::BTreeSet;
use crate::category::Root;
use crate::dataset::ABLE_TO_POPULATE;
use crate::dataset::Dataset;
use crate::dataset::ObligationProfile;
use crate::dataset::PRODUCER;
use crate::dataset::ResourceProfile;
pub const SHOULD_POPULATE: &str = "SHOULD:able-to-populate";
pub const ANY_RESOURCE: &str = "http://hl7.org/fhir/StructureDefinition/Resource";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Populate {
Shall,
Should,
}
impl Populate {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::Shall => ABLE_TO_POPULATE,
Self::Should => SHOULD_POPULATE,
}
}
#[must_use]
pub fn of(profile: &ObligationProfile, path: &str) -> Option<Self> {
let list = profile
.obligations()
.iter()
.find(|(held, _)| held.as_str() == path)
.map(|(_, list)| list)?;
[Self::Shall, Self::Should].into_iter().find(|populate| {
list.iter()
.any(|obligation| obligation.binds(populate.code(), PRODUCER))
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EhnElement {
pub id: &'static str,
pub name: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Row {
pub path: &'static str,
pub ehn: &'static [&'static str],
pub producer: Option<Populate>,
pub eps: &'static str,
pub contexts: &'static [&'static str],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Gap {
pub path: &'static str,
pub ehn: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct Crosswalk<'a> {
pub root: Root,
pub profile: &'static str,
pub companions: &'a [&'static str],
pub sliced: &'static str,
pub ehn: &'a [EhnElement],
pub rows: &'a [Row],
pub gaps: &'a [Gap],
}
impl Crosswalk<'_> {
#[must_use]
pub fn row(&self, path: &str) -> Option<&Row> {
self.rows.iter().find(|row| row.path == path)
}
#[must_use]
pub fn check(&self, dataset: Dataset<'_>, profile: &ResourceProfile) -> Vec<Defect> {
self.check_with(dataset, profile, &[])
}
#[must_use]
pub fn check_with(
&self,
dataset: Dataset<'_>,
profile: &ResourceProfile,
companions: &[ResourceProfile],
) -> Vec<Defect> {
let mut defects = Vec::new();
if dataset.model().url() != self.root.model
|| dataset.obligations().url() != self.root.obligations
{
defects.push(Defect::Dataset {
model: dataset.model().url().to_owned(),
});
}
if profile.url() != self.profile {
defects.push(Defect::Profile {
url: profile.url().to_owned(),
});
}
let urls: Vec<&str> = companions.iter().map(ResourceProfile::url).collect();
if urls != self.companions {
defects.push(Defect::Companions {
urls: urls.into_iter().map(str::to_owned).collect(),
});
}
let mut seen = BTreeSet::new();
for row in self.rows {
if !seen.insert(row.path) {
defects.push(Defect::Duplicate { path: row.path });
}
self.check_row(row, dataset, profile, companions, &mut defects);
}
for element in dataset.producer_elements() {
let path = element.path().as_str();
if !seen.contains(path) {
defects.push(Defect::Uncovered {
path: path.to_owned(),
});
}
}
for element in profile.elements() {
let id = element.path().as_str();
let slice = id
.strip_prefix(self.sliced)
.and_then(|rest| rest.strip_prefix(':'))
.is_some_and(|name| !name.contains('.'));
if slice
&& element.cardinality().is_required()
&& !self.rows.iter().any(|row| row.eps == id)
{
defects.push(Defect::RequiredSlice { id: id.to_owned() });
}
}
for gap in self.gaps {
if !self
.row(gap.path)
.is_some_and(|row| row.ehn.contains(&gap.ehn))
{
defects.push(Defect::Gap {
path: gap.path,
ehn: gap.ehn,
});
}
}
defects
}
fn check_row(
&self,
row: &Row,
dataset: Dataset<'_>,
profile: &ResourceProfile,
companions: &[ResourceProfile],
defects: &mut Vec<Defect>,
) {
if dataset.model().element(row.path).is_none() {
defects.push(Defect::Path { path: row.path });
}
let package = Populate::of(dataset.obligations(), row.path);
if package != row.producer {
defects.push(Defect::Obligation {
path: row.path,
row: row.producer,
package,
});
}
for id in row.ehn {
if !self.ehn.iter().any(|element| element.id == *id) {
defects.push(Defect::Ehn { path: row.path, id });
}
}
let Some(element) = std::iter::once(profile)
.chain(companions)
.find_map(|profile| profile.element(row.eps))
else {
defects.push(Defect::Slice {
path: row.path,
eps: row.eps,
});
return;
};
for context in row.contexts {
let admitted = element.target_profiles().iter().any(|target| {
let target = target
.split_once('|')
.map_or(target.as_str(), |(url, _)| url);
target == *context || target == ANY_RESOURCE
});
if !admitted {
defects.push(Defect::Context {
path: row.path,
eps: row.eps,
profile: context,
});
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum Defect {
#[error("the crosswalk is checked against the dataset {model}, which it does not name")]
Dataset {
model: String,
},
#[error("the crosswalk is checked against the profile {url}, which it does not name")]
Profile {
url: String,
},
#[error("two rows carry {path}")]
Duplicate {
path: &'static str,
},
#[error("the row {path} names no element of the logical model")]
Path {
path: &'static str,
},
#[error("the row {path} states the producer obligation {row:?}, the package {package:?}")]
Obligation {
path: &'static str,
row: Option<Populate>,
package: Option<Populate>,
},
#[error("the row {path} names the eHealth Network element {id}, which the catalogue lacks")]
Ehn {
path: &'static str,
id: &'static str,
},
#[error(
"the crosswalk is checked against the companion profiles {urls:?}, which it does not name"
)]
Companions {
urls: Vec<String>,
},
#[error("the row {path} names {eps}, which the profile and its companions lack")]
Slice {
path: &'static str,
eps: &'static str,
},
#[error("the row {path} admits contexts of {profile}, which {eps} does not target")]
Context {
path: &'static str,
eps: &'static str,
profile: &'static str,
},
#[error("{path} is SHALL:able-to-populate for a producer and has no row")]
Uncovered {
path: String,
},
#[error("the required slice {id} is named by no row")]
RequiredSlice {
id: String,
},
#[error("the gap {ehn} at {path} names no row carrying that id")]
Gap {
path: &'static str,
ehn: &'static str,
},
}