use std::collections::BTreeSet;
use fhir_types::codec::Value;
use fhir_types::r4::operation_outcome::OperationOutcomeIssue;
use fhir_types::r4::primitives;
use fhirconnect::engine::outcome::Location;
use fhirconnect::operations::issues::WARNING;
use fhirconnect::operations::issues::issue;
use crate::receive::reference::{self, Target};
const INCOMPLETE: &str = "incomplete";
const NOT_CARRIED: &str = "the element is carried into no structured element of the \
composition; the composition's original content keeps it";
const SECTION: &str = "Composition.section";
pub(super) fn not_carried(
document: &Value,
accounted: &[&str],
followed: &[&str],
reads: &[Location],
) -> Vec<OperationOutcomeIssue> {
let mut issues = Vec::new();
let Some(Value::Array(entries)) = document.get("entry") else {
return issues;
};
let resolved = Resolved::new(document, reads);
for (holder, resource) in entries
.iter()
.enumerate()
.filter_map(|(holder, entry)| Some((holder, entry.get("resource")?)))
{
let Some(kind) = resource.get("resourceType").and_then(Value::as_str) else {
continue;
};
let reference = resource
.get("id")
.and_then(Value::as_str)
.map(|id| format!("{kind}/{id}"));
let read: Vec<&str> = reads
.iter()
.filter(|location| location.resource() == reference.as_deref())
.filter_map(Location::fhir)
.collect();
if kind == "Composition" && accounted.contains(&SECTION) {
sections(&resolved, holder, &mut issues);
}
for path in accounted.iter().filter(|path| **path != SECTION) {
let Some((root, steps)) = path.split_once('.') else {
continue;
};
if root != kind {
continue;
}
let steps: Vec<&str> = steps.split('.').collect();
let mut found = Vec::new();
instances(resource, String::from(kind), &steps, &mut found);
let follows = followed.contains(path);
for (at, value) in found {
if !(follows && resolved.reaches_read(holder, value)) {
report(&at, value, &read, &mut issues);
}
}
}
}
issues
}
struct Resolved<'d> {
entries: Vec<reference::Entry<'d>>,
read: BTreeSet<&'d str>,
}
impl<'d> Resolved<'d> {
fn new(document: &'d Value, reads: &'d [Location]) -> Self {
Self {
entries: document
.as_object()
.map(reference::entries)
.unwrap_or_default(),
read: reads.iter().filter_map(Location::resource).collect(),
}
}
fn reaches_read(&self, holder: usize, value: &Value) -> bool {
let Some(reference) = value.as_object() else {
return false;
};
let Ok(Target::Entry(index)) = reference::resolve_in(&self.entries, holder, reference)
else {
return false;
};
self.entries
.get(index)
.and_then(|entry| entry.resource)
.and_then(|resource| {
let kind = resource.get("resourceType").and_then(Value::as_str)?;
let id = resource.get("id").and_then(Value::as_str)?;
Some(format!("{kind}/{id}"))
})
.is_some_and(|named| self.read.contains(named.as_str()))
}
}
fn sections(resolved: &Resolved<'_>, holder: usize, issues: &mut Vec<OperationOutcomeIssue>) {
let Some(Value::Array(held)) = resolved
.entries
.get(holder)
.and_then(|entry| entry.resource)
.and_then(|composition| composition.get("section"))
else {
return;
};
for (index, section) in held.iter().enumerate() {
let carried = section
.get("entry")
.and_then(Value::as_array)
.unwrap_or_default()
.iter()
.any(|entry| resolved.reaches_read(holder, entry));
if !carried {
let mut declared = issue(WARNING, INCOMPLETE, NOT_CARRIED);
declared.expression = vec![primitives::String::from(format!("{SECTION}[{index}]"))];
issues.push(declared);
}
}
}
fn instances<'value>(
value: &'value Value,
path: String,
steps: &[&str],
found: &mut Vec<(String, &'value Value)>,
) {
let Some((step, rest)) = steps.split_first() else {
found.push((path, value));
return;
};
match value.get(step) {
Some(Value::Array(items)) => {
for (index, item) in items.iter().enumerate() {
instances(item, format!("{path}.{step}[{index}]"), rest, found);
}
}
Some(member) => instances(member, format!("{path}.{step}"), rest, found),
None => {}
}
}
fn report(path: &str, value: &Value, read: &[&str], issues: &mut Vec<OperationOutcomeIssue>) {
if read.iter().any(|reached| within(path, reached)) {
return;
}
if !read.iter().any(|reached| within(reached, path)) {
let mut declared = issue(WARNING, INCOMPLETE, NOT_CARRIED);
declared.expression = vec![primitives::String::from(path)];
issues.push(declared);
return;
}
match value {
Value::Object(members) => {
for (key, member) in members {
let name = key.strip_prefix('_').unwrap_or(key);
report(&format!("{path}.{name}"), member, read, issues);
}
}
Value::Array(items) => {
for (index, item) in items.iter().enumerate() {
report(&format!("{path}[{index}]"), item, read, issues);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
fn within(path: &str, ancestor: &str) -> bool {
path.strip_prefix(ancestor)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('.') || rest.starts_with('['))
}
#[cfg(test)]
mod tests {
use super::within;
#[test]
fn an_element_lies_within_itself_and_its_ancestors_alone() {
assert!(within("Observation.basedOn[0]", "Observation.basedOn[0]"));
assert!(within(
"Observation.basedOn[0].identifier",
"Observation.basedOn[0]"
));
assert!(within("Observation.basedOn[0]", "Observation.basedOn"));
assert!(!within("Observation.basedOn[1]", "Observation.basedOn[0]"));
assert!(!within("Observation.basedOnX", "Observation.basedOn"));
}
}