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;
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";
pub(super) fn not_carried(
document: &Value,
accounted: &[&str],
reads: &[Location],
) -> Vec<OperationOutcomeIssue> {
let mut issues = Vec::new();
let Some(Value::Array(entries)) = document.get("entry") else {
return issues;
};
for resource in entries.iter().filter_map(|entry| 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();
for path in accounted {
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);
for (at, value) in found {
report(&at, value, &read, &mut issues);
}
}
}
issues
}
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"));
}
}