pub mod constraint;
mod read;
use std::collections::BTreeMap;
use std::fmt;
use std::io;
use std::io::Read;
use crate::category::Root;
use crate::dataset::constraint::Binding;
use crate::dataset::constraint::Invariant;
use crate::dataset::constraint::Pattern;
use crate::dataset::constraint::Slicing;
pub const OBLIGATION_EXTENSION: &str = "http://hl7.org/fhir/StructureDefinition/obligation";
pub const PRODUCER: &str = "https://www.xt-ehr.eu/specifications/fhir/actor-producer";
pub const CONSUMER: &str = "https://www.xt-ehr.eu/specifications/fhir/actor-consumer";
pub const ABLE_TO_POPULATE: &str = "SHALL:able-to-populate";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PackageId {
name: String,
version: String,
}
impl PackageId {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
}
impl fmt::Display for PackageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}#{}", self.name, self.version)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ElementPath(String);
impl ElementPath {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn model(&self) -> &str {
self.0
.split_once('.')
.map_or(self.0.as_str(), |(model, _)| model)
}
#[must_use]
pub fn parent(&self) -> Option<Self> {
self.0
.rsplit_once('.')
.map(|(parent, _)| Self(parent.to_owned()))
}
}
impl fmt::Display for ElementPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Max {
Bounded(u32),
Unbounded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Cardinality {
min: u32,
max: Max,
}
impl Cardinality {
#[must_use]
pub const fn lower(&self) -> u32 {
self.min
}
#[must_use]
pub const fn upper(&self) -> Max {
self.max
}
#[must_use]
pub const fn is_required(&self) -> bool {
self.min > 0
}
}
impl fmt::Display for Cardinality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.max {
Max::Bounded(max) => write!(f, "{}..{max}", self.min),
Max::Unbounded => write!(f, "{}..*", self.min),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Element {
path: ElementPath,
cardinality: Cardinality,
types: Vec<String>,
profiles: Vec<String>,
short: Option<String>,
pattern: Option<Pattern>,
slicing: Option<Slicing>,
invariants: Vec<Invariant>,
binding: Option<Binding>,
}
impl Element {
#[must_use]
pub const fn path(&self) -> &ElementPath {
&self.path
}
#[must_use]
pub const fn cardinality(&self) -> Cardinality {
self.cardinality
}
#[must_use]
pub fn types(&self) -> &[String] {
&self.types
}
#[must_use]
pub fn target_profiles(&self) -> &[String] {
&self.profiles
}
#[must_use]
pub fn short(&self) -> Option<&str> {
self.short.as_deref()
}
#[must_use]
pub const fn pattern(&self) -> Option<&Pattern> {
self.pattern.as_ref()
}
#[must_use]
pub const fn slicing(&self) -> Option<&Slicing> {
self.slicing.as_ref()
}
#[must_use]
pub fn invariants(&self) -> &[Invariant] {
&self.invariants
}
#[must_use]
pub const fn binding(&self) -> Option<&Binding> {
self.binding.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Elements {
list: Vec<Element>,
index: BTreeMap<ElementPath, usize>,
}
impl Elements {
fn get(&self, path: &str) -> Option<&Element> {
self.index
.get(&ElementPath(path.to_owned()))
.and_then(|position| self.list.get(*position))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogicalModel {
url: String,
name: String,
base: Option<String>,
elements: Elements,
}
impl LogicalModel {
#[must_use]
pub fn url(&self) -> &str {
&self.url
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn base(&self) -> Option<&str> {
self.base.as_deref()
}
#[must_use]
pub fn elements(&self) -> &[Element] {
&self.elements.list
}
#[must_use]
pub fn element(&self, path: &str) -> Option<&Element> {
self.elements.get(path)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Obligation {
code: String,
actors: Vec<String>,
}
impl Obligation {
#[must_use]
pub fn code(&self) -> &str {
&self.code
}
#[must_use]
pub fn actors(&self) -> &[String] {
&self.actors
}
#[must_use]
pub fn binds(&self, code: &str, actor: &str) -> bool {
self.code == code
&& (self.actors.is_empty() || self.actors.iter().any(|named| named == actor))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObligationProfile {
url: String,
name: String,
constrains: String,
elements: Elements,
obligations: BTreeMap<ElementPath, Vec<Obligation>>,
}
impl ObligationProfile {
#[must_use]
pub fn url(&self) -> &str {
&self.url
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn constrains(&self) -> &str {
&self.constrains
}
#[must_use]
pub fn elements(&self) -> &[Element] {
&self.elements.list
}
#[must_use]
pub fn element(&self, path: &str) -> Option<&Element> {
self.elements.get(path)
}
#[must_use]
pub const fn obligations(&self) -> &BTreeMap<ElementPath, Vec<Obligation>> {
&self.obligations
}
pub fn paths_with<'a>(
&'a self,
code: &'a str,
actor: &'a str,
) -> impl Iterator<Item = &'a ElementPath> + 'a {
self.obligations.iter().filter_map(move |(path, list)| {
list.iter()
.any(|obligation| obligation.binds(code, actor))
.then_some(path)
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct Dataset<'a> {
model: &'a LogicalModel,
obligations: &'a ObligationProfile,
}
impl<'a> Dataset<'a> {
#[must_use]
pub const fn model(&self) -> &'a LogicalModel {
self.model
}
#[must_use]
pub const fn obligations(&self) -> &'a ObligationProfile {
self.obligations
}
pub fn producer_elements(&self) -> impl Iterator<Item = &'a Element> + 'a {
let profile = self.obligations;
profile
.paths_with(ABLE_TO_POPULATE, PRODUCER)
.filter_map(move |path| profile.element(path.as_str()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatasetModel {
package: PackageId,
models: BTreeMap<String, LogicalModel>,
profiles: BTreeMap<String, ObligationProfile>,
}
impl DatasetModel {
pub fn read(archive: impl Read) -> Result<Self, DatasetError> {
read::package(archive)
}
#[must_use]
pub const fn package(&self) -> &PackageId {
&self.package
}
pub fn models(&self) -> impl Iterator<Item = &LogicalModel> {
self.models.values()
}
#[must_use]
pub fn model(&self, url: &str) -> Option<&LogicalModel> {
self.models.get(url)
}
pub fn profiles(&self) -> impl Iterator<Item = &ObligationProfile> {
self.profiles.values()
}
#[must_use]
pub fn profile(&self, url: &str) -> Option<&ObligationProfile> {
self.profiles.get(url)
}
pub fn dataset(&self, root: Root) -> Result<Dataset<'_>, DatasetError> {
let missing = || DatasetError::MissingRoot {
model: root.model.to_owned(),
obligations: root.obligations.to_owned(),
};
let model = self.models.get(root.model).ok_or_else(missing)?;
let obligations = self
.profiles
.get(root.obligations)
.filter(|profile| profile.constrains == root.model)
.ok_or_else(missing)?;
Ok(Dataset { model, obligations })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceProfile {
package: PackageId,
url: String,
name: String,
version: Option<String>,
elements: Elements,
}
impl ResourceProfile {
pub fn read(archive: impl Read, url: &str) -> Result<Self, DatasetError> {
read::resource_profile(archive, url)
}
#[must_use]
pub const fn package(&self) -> &PackageId {
&self.package
}
#[must_use]
pub fn url(&self) -> &str {
&self.url
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn version(&self) -> Option<&str> {
self.version.as_deref()
}
#[must_use]
pub fn elements(&self) -> &[Element] {
&self.elements.list
}
#[must_use]
pub fn element(&self, id: &str) -> Option<&Element> {
self.elements.get(id)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DatasetError {
#[error("the package archive cannot be read")]
Archive {
#[source]
source: io::Error,
},
#[error("the package archive holds no package/package.json")]
MissingManifest,
#[error("{file} does not parse")]
Json {
file: String,
#[source]
source: serde_json::Error,
},
#[error("two definitions carry the canonical URL {url}")]
DuplicateUrl {
url: String,
},
#[error("{url} has the derivation {derivation:?}, neither specialization nor constraint")]
Derivation {
url: String,
derivation: Option<String>,
},
#[error("{url} carries no snapshot")]
MissingSnapshot {
url: String,
},
#[error("{url} has an element at {path} with no id or no readable cardinality")]
Element {
url: String,
path: String,
},
#[error("{url} has an element that does not parse")]
ElementJson {
url: String,
#[source]
source: serde_json::Error,
},
#[error("{url} slices {path} with a discriminator or rules outside the R4 value sets")]
Slicing {
url: String,
path: String,
},
#[error("{url} binds {path} with a strength outside the R4 value set")]
Binding {
url: String,
path: String,
},
#[error("{url} carries the element {path} twice")]
DuplicateElement {
url: String,
path: String,
},
#[error("{url} is a constraint with no baseDefinition")]
MissingBase {
url: String,
},
#[error("{url} constrains {base}, which the package holds no model for")]
UnknownBase {
url: String,
base: String,
},
#[error("{url} constrains {base} but its snapshot lacks {path}")]
Uncovered {
url: String,
base: String,
path: String,
},
#[error("{url} has an obligation on {path} with no single code")]
Obligation {
url: String,
path: String,
},
#[error("the package holds no StructureDefinition {url}")]
MissingProfile {
url: String,
},
#[error("the package holds no model {model} with the profile {obligations} over it")]
MissingRoot {
model: String,
obligations: String,
},
}