mod values;
mod walk;
use std::fmt;
use fhir_types::codec::{Object, Value};
use crate::dataset::ResourceProfile;
use crate::dataset::constraint::DiscriminatorKind;
use crate::receive::ReceivedDocument;
use crate::receive::conform::walk::Walk;
use crate::terminology::bindings::BindingFinding;
use crate::terminology::bindings::BindingGap;
use crate::terminology::bindings::UnevaluatedBinding;
impl ReceivedDocument {
#[expect(
clippy::expect_used,
reason = "ReceivedDocument::read and the rebuild after displays are its only constructors, \
and both refuse a document whose first entry is no Composition"
)]
pub fn check(
&self,
bundle: &ResourceProfile,
composition: &ResourceProfile,
) -> Result<Conformance, CheckError> {
let root = self.tree();
let first = root
.get("entry")
.and_then(Value::as_array)
.and_then(<[Value]>::first)
.and_then(|entry| entry.get("resource"))
.and_then(Value::as_object)
.expect("a received document should carry its Composition as its first entry");
let mut walk = Walk::default();
walk.profile(bundle, "Bundle", root)?;
walk.profile(composition, "Composition", first)?;
walk.finish()
}
}
pub fn check_resource(
profile: &ResourceProfile,
expected: &'static str,
resource: &Object,
) -> Result<Conformance, CheckError> {
let mut walk = Walk::default();
walk.profile(profile, expected, resource)?;
walk.finish()
}
impl Walk {
fn finish(self) -> Result<Conformance, CheckError> {
if self.findings.is_empty() {
Ok(Conformance {
unevaluated: self.unevaluated.into_iter().collect(),
})
} else {
Err(CheckError::NonConformant {
findings: self.findings,
})
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Conformance {
unevaluated: Vec<Unevaluated>,
}
impl Conformance {
#[must_use]
pub fn unevaluated(&self) -> &[Unevaluated] {
&self.unevaluated
}
#[cfg(any(feature = "prescription", feature = "dispensation"))]
pub(crate) fn listing(unevaluated: impl IntoIterator<Item = Unevaluated>) -> Self {
Self::default().join(Self {
unevaluated: unevaluated.into_iter().collect(),
})
}
#[cfg(any(feature = "prescription", feature = "dispensation"))]
pub(crate) fn join(mut self, other: Self) -> Self {
self.unevaluated.extend(other.unevaluated);
self.unevaluated.sort();
self.unevaluated.dedup();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Unevaluated {
pub profile: String,
pub element: String,
pub reason: Unread,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Unread {
Pattern(String),
Discriminator {
kind: DiscriminatorKind,
path: String,
},
Order,
Invariant(String),
Constraint {
constraint: String,
reason: String,
},
Binding {
value_set: Option<String>,
gap: BindingGap,
},
}
impl fmt::Display for Unread {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Pattern(key) => write!(f, "the {key} value"),
Self::Discriminator { kind, path } => write!(f, "the {kind} discriminator at {path}"),
Self::Order => f.write_str("the slice order"),
Self::Invariant(key) => write!(f, "the invariant {key}"),
Self::Constraint { constraint, reason } => write!(f, "{constraint} ({reason})"),
Self::Binding { value_set, gap } => {
let value_set = value_set.as_deref().unwrap_or("no value set");
match gap {
BindingGap::NoExpansion => write!(
f,
"the required binding to {value_set}, of which no expansion is read"
),
BindingGap::NoValueSet => {
f.write_str("the required binding, which names no value set")
}
BindingGap::Sliced => {
write!(f, "the required binding to {value_set} inside a slice")
}
BindingGap::Type(code) => write!(
f,
"the required binding to {value_set} on an element of type {code}"
),
}
}
}
}
}
impl From<UnevaluatedBinding> for Unevaluated {
fn from(binding: UnevaluatedBinding) -> Self {
Self {
profile: binding.profile,
element: binding.element,
reason: Unread::Binding {
value_set: binding.value_set,
gap: binding.gap,
},
}
}
}
impl From<BindingFinding> for Finding {
fn from(finding: BindingFinding) -> Self {
Self {
profile: finding.profile,
element: finding.element,
location: finding.location,
kind: FindingKind::NotInValueSet {
value_set: finding.value_set,
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
pub profile: String,
pub element: String,
pub location: String,
pub kind: FindingKind,
}
impl fmt::Display for Finding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} at {}: {}", self.element, self.location, self.kind)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FindingKind {
TooFew {
min: u32,
found: usize,
},
TooMany {
max: u32,
found: usize,
},
Pattern,
NoSlice,
TypeNotAllowed,
Invariant {
key: String,
},
NotInValueSet {
value_set: String,
},
Issue {
severity: &'static str,
code: &'static str,
message: String,
},
}
impl fmt::Display for FindingKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooFew { min, found } => {
write!(f, "{found} occurrences, at least {min} required")
}
Self::TooMany { max, found } => write!(f, "{found} occurrences, at most {max} allowed"),
Self::Pattern => f.write_str("the value differs from the profile's required value"),
Self::NoSlice => f.write_str("the occurrence matches no slice of a closed slicing"),
Self::TypeNotAllowed => f.write_str("the resource type is one no slice names"),
Self::Invariant { key } => write!(f, "the invariant {key} does not hold"),
Self::NotInValueSet { value_set } => {
write!(
f,
"the concept is not in the required value set {value_set}"
)
}
Self::Issue { message, .. } => f.write_str(message),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum CheckError {
#[error("the profile {url} does not constrain {expected}")]
ProfileType {
url: String,
expected: &'static str,
},
#[error("{location} is a coded element whose bindings cannot be read")]
Unreadable {
location: String,
},
#[error("the document breaks its profiles in {} places: {}", findings.len(), Listed(findings))]
NonConformant {
findings: Vec<Finding>,
},
}
struct Listed<'a>(&'a [Finding]);
impl fmt::Display for Listed<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (position, finding) in self.0.iter().enumerate() {
if position > 0 {
f.write_str("; ")?;
}
write!(f, "{finding}")?;
}
Ok(())
}
}