use fhir_types::codec::Json;
use fhir_types::codec::Object;
use fhir_types::codec::Path;
use fhir_types::codec::Value;
use fhir_types::r4::codeable_concept::CodeableConcept;
use fhir_types::r4::coding::Coding;
use crate::dataset::ResourceProfile;
use crate::dataset::constraint::BindingStrength;
use crate::receive::ReceivedDocument;
use crate::terminology::Candidate;
use crate::terminology::Terminology;
use crate::terminology::TerminologyError;
use crate::terminology::expansion::Expansions;
use crate::terminology::walk;
use crate::terminology::walk::Coded;
use crate::terminology::walk::Occurrence;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BindingCheck {
findings: Vec<BindingFinding>,
unevaluated: Vec<UnevaluatedBinding>,
pending: Vec<Pending>,
}
impl BindingCheck {
#[must_use]
pub fn findings(&self) -> &[BindingFinding] {
&self.findings
}
#[must_use]
pub fn unevaluated(&self) -> &[UnevaluatedBinding] {
&self.unevaluated
}
pub async fn complete(mut self, terminology: &Terminology) -> Result<Self, TerminologyError> {
if !terminology.is_configured() || self.pending.is_empty() {
return Ok(self);
}
for pending in std::mem::take(&mut self.pending) {
if !terminology
.validate(&pending.finding.value_set, &pending.concept)
.await?
{
self.findings.push(pending.finding);
}
}
self.unevaluated
.retain(|binding| binding.gap != BindingGap::NoExpansion);
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingFinding {
pub profile: String,
pub element: String,
pub location: String,
pub value_set: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct UnevaluatedBinding {
pub profile: String,
pub element: String,
pub value_set: Option<String>,
pub gap: BindingGap,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum BindingGap {
NoExpansion,
NoValueSet,
Sliced,
Type(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Pending {
finding: BindingFinding,
concept: Candidate,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum BindingError {
#[error("{location} is an element the R4 element table does not describe")]
Unreadable {
location: String,
},
#[error("{location} does not decode as its R4 type")]
Decode {
location: String,
#[source]
source: fhir_types::codec::DecodeError,
},
}
pub fn check_required(
profile: &ResourceProfile,
resource: &Object,
location: &str,
expansions: &Expansions,
) -> Result<BindingCheck, BindingError> {
let found =
walk::coded(resource, location, false).map_err(|refused| BindingError::Unreadable {
location: refused.location,
})?;
let mut check = BindingCheck::default();
for element in profile.elements() {
let Some(binding) = element
.binding()
.filter(|binding| binding.strength() == BindingStrength::Required)
else {
continue;
};
let id = element.path().as_str();
let value_set = binding.value_set().map(str::to_owned);
let unevaluated = |gap| UnevaluatedBinding {
profile: profile.url().to_owned(),
element: id.to_owned(),
value_set: value_set.clone(),
gap,
};
let types = element.types();
if let Some(other) = types
.iter()
.find(|code| !matches!(code.as_str(), "code" | "Coding" | "CodeableConcept"))
{
check
.unevaluated
.push(unevaluated(BindingGap::Type(other.clone())));
continue;
}
if id.contains(':') {
check.unevaluated.push(unevaluated(BindingGap::Sliced));
continue;
}
let Some(url) = &value_set else {
check.unevaluated.push(unevaluated(BindingGap::NoValueSet));
continue;
};
let occurrences: Vec<&Occurrence<'_>> =
found.iter().filter(|found| found.element == id).collect();
if occurrences.is_empty() {
continue;
}
let expanded = expansions.has(url);
if !expanded {
check.unevaluated.push(unevaluated(BindingGap::NoExpansion));
}
for occurrence in occurrences {
let finding = BindingFinding {
profile: profile.url().to_owned(),
element: id.to_owned(),
location: occurrence.location.clone(),
value_set: url.clone(),
};
if expanded {
if !held(expansions, url, &occurrence.value) {
check.findings.push(finding);
}
} else {
let concept = concept(profile.url(), id, occurrence)?;
check.pending.push(Pending { finding, concept });
}
}
}
check.unevaluated.sort();
check.unevaluated.dedup();
Ok(check)
}
impl ReceivedDocument {
pub fn check_bindings(
&self,
bundle: &ResourceProfile,
composition: &ResourceProfile,
expansions: &Expansions,
) -> Result<BindingCheck, BindingError> {
let root = self.tree();
let location = "Bundle.entry[0].resource";
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)
.ok_or_else(|| BindingError::Unreadable {
location: location.to_owned(),
})?;
let mut check = check_required(bundle, root, "Bundle", expansions)?;
let entry = check_required(composition, first, location, expansions)?;
check.findings.extend(entry.findings);
check.unevaluated.extend(entry.unevaluated);
check.pending.extend(entry.pending);
check.unevaluated.sort();
check.unevaluated.dedup();
Ok(check)
}
}
fn held(expansions: &Expansions, url: &str, value: &Coded<'_>) -> bool {
let coding_held = |coding: &Object| {
let system = coding.get("system").and_then(|system| system.as_str());
coding
.get("code")
.and_then(|code| code.as_str())
.is_some_and(|code| {
system.is_some() && expansions.contains(url, system, code) == Some(true)
})
};
match value {
Coded::Code(code) => expansions.contains(url, None, code) == Some(true),
Coded::Coding(coding) => coding_held(coding),
Coded::Concept(concept) => concept
.get("coding")
.and_then(|codings| codings.as_array())
.unwrap_or_default()
.iter()
.filter_map(|coding| coding.as_object())
.any(coding_held),
}
}
fn concept(
profile: &str,
element: &str,
occurrence: &Occurrence<'_>,
) -> Result<Candidate, BindingError> {
let decode_error = |source| BindingError::Decode {
location: occurrence.location.clone(),
source,
};
Ok(match occurrence.value {
Coded::Code(code) => Candidate::Code {
code: code.to_owned(),
context: format!("{profile}#{element}"),
},
Coded::Coding(coding) => Candidate::Coding(Box::new(
Coding::from_json(coding, &mut Path::root("Coding")).map_err(decode_error)?,
)),
Coded::Concept(concept) => Candidate::Concept(Box::new(
CodeableConcept::from_json(concept, &mut Path::root("CodeableConcept"))
.map_err(decode_error)?,
)),
})
}