use std::collections::BTreeSet;
use fhir_types::codec::Value;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Severity;
use crate::release::Release;
use crate::validate::reference::address;
use crate::validate::reference::literal;
use crate::validate::resolver::Resolution;
use crate::validate::resolver::Via;
use crate::validate::walk::Walk;
type Required<'i> = (String, String, &'i Value);
impl<'i, R: Release> Walk<'_, '_, 'i, R> {
pub(crate) fn document(&mut self, bundle: &'i Value, location: &str) {
if bundle.get("type").and_then(Value::as_str) != Some("document") {
return;
}
let entries = bundle
.get("entry")
.and_then(Value::as_array)
.unwrap_or_default();
let Some(composition) = entries
.first()
.and_then(|entry| entry.get("resource"))
.filter(|resource| resource_type(resource) == Some("Composition"))
else {
return;
};
let mut required = Vec::new();
composition_references(composition, &mut required);
for (path, element, reference) in required {
let Some(reference) = literal(reference) else {
continue;
};
match self.resolve_from(bundle, composition, reference) {
Resolution::Found { .. } | Resolution::Ambiguous => {}
Resolution::Missing | Resolution::OutOfScope => self.out().issue(
Severity::Error,
IssueType::NotFound,
MessageId::DocumentReferenceMissing,
&format!("{location}.entry[0].resource.{path}"),
format!(
"the resource {element} references is not in the document Bundle, which SHALL include it"
),
),
}
}
let included = self.included(composition);
for (index, entry) in entries.iter().enumerate().skip(1) {
let Some(resource) = entry.get("resource") else {
continue;
};
if included.contains(&address(resource))
|| stylesheet(resource)
|| self.provenance_of(bundle, resource, &included)
{
continue;
}
self.out().issue(
Severity::Error,
IssueType::Invalid,
MessageId::DocumentEntryUnreferenced,
&format!("{location}.entry[{index}]"),
format!(
"the document Bundle holds a {} the Composition does not reference, directly or indirectly, and that is no stylesheet Binary or Provenance of the document",
resource_type(resource).unwrap_or("resource")
),
);
}
}
fn resolve_from(
&self,
bundle: &'i Value,
resource: &'i Value,
reference: &str,
) -> Resolution<'i> {
let mut scope = self.scope().to_vec();
scope.push(bundle);
scope.push(resource);
self.resolve_reference_in(reference, &scope)
}
fn included(&self, composition: &'i Value) -> BTreeSet<usize> {
let mut included = BTreeSet::from([address(composition)]);
let mut pending = vec![address(composition)];
while let Some(from) = pending.pop() {
for to in self.links(from) {
if included.insert(to) {
pending.push(to);
}
}
}
included
}
fn provenance_of(
&self,
bundle: &'i Value,
resource: &'i Value,
included: &BTreeSet<usize>,
) -> bool {
resource_type(resource) == Some("Provenance")
&& members(resource, "target").into_iter().any(|(_, target)| {
literal(target).is_some_and(|reference| {
matches!(
self.resolve_from(bundle, resource, reference),
Resolution::Found { target, via: Via::Entry { .. } }
if included.contains(&address(target))
)
})
})
}
}
fn resource_type(resource: &Value) -> Option<&str> {
resource.get("resourceType").and_then(Value::as_str)
}
fn stylesheet(resource: &Value) -> bool {
resource_type(resource) == Some("Binary")
&& resource
.get("contentType")
.and_then(Value::as_str)
.is_some_and(|kind| {
kind.split(';')
.next()
.is_some_and(|media| media.trim().eq_ignore_ascii_case("text/css"))
})
}
fn members<'i>(value: &'i Value, key: &str) -> Vec<(String, &'i Value)> {
match value.get(key) {
Some(Value::Array(items)) => items
.iter()
.enumerate()
.map(|(index, item)| (format!("{key}[{index}]"), item))
.collect(),
Some(item) => vec![(key.to_owned(), item)],
None => Vec::new(),
}
}
fn composition_references<'i>(composition: &'i Value, out: &mut Vec<Required<'i>>) {
for key in ["subject", "encounter", "author", "custodian"] {
for (path, reference) in members(composition, key) {
out.push((path, format!("Composition.{key}"), reference));
}
}
for (outer, inner) in [("attester", "party"), ("event", "detail")] {
for (step, parent) in members(composition, outer) {
for (path, reference) in members(parent, inner) {
out.push((
format!("{step}.{path}"),
format!("Composition.{outer}.{inner}"),
reference,
));
}
}
}
sections(composition, "", out);
}
fn sections<'i>(parent: &'i Value, prefix: &str, out: &mut Vec<Required<'i>>) {
for (step, section) in members(parent, "section") {
let at = format!("{prefix}{step}");
for key in ["author", "focus", "entry"] {
for (path, reference) in members(section, key) {
out.push((
format!("{at}.{path}"),
format!("Composition.section.{key}"),
reference,
));
}
}
sections(section, &format!("{at}."), out);
}
}