use std::collections::BTreeSet;
use fhir_types::codec::Object;
use fhir_types::codec::Value;
use fhir_types::r4::schema::SCHEMAS;
use crate::receive::ReceiveError;
use crate::receive::reference::ReferenceError;
use crate::receive::reference::Target;
use crate::receive::reference::Url;
use crate::receive::reference::entries;
use crate::receive::reference::resolve_in;
use crate::receive::references;
pub(super) fn known_resources(node: &Object, location: &str) -> Result<(), ReceiveError> {
if let Some(kind) = node.get("resourceType").and_then(Value::as_str)
&& !SCHEMAS.is_resource(kind)
{
return Err(ReceiveError::UnknownResource {
location: location.to_owned(),
});
}
for (key, value) in node {
match value {
Value::Object(object) => known_resources(object, &format!("{location}.{key}"))?,
Value::Array(items) => {
for (index, item) in items.iter().enumerate() {
if let Some(object) = item.as_object() {
known_resources(object, &format!("{location}.{key}[{index}]"))?;
}
}
}
_ => {}
}
}
Ok(())
}
pub(super) fn full_urls(tree: &Object) -> Result<(), ReceiveError> {
let mut seen = BTreeSet::new();
for (index, entry) in entries(tree).iter().enumerate() {
let Some(url) = entry.full_url else {
continue;
};
let parsed = Url::parse(url).ok_or(ReceiveError::NonCanonicalFullUrl { entry: index })?;
if url.contains("/_history/") {
return Err(ReceiveError::VersionedFullUrl { entry: index });
}
let version = entry
.resource
.and_then(|resource| resource.get("meta"))
.and_then(|meta| meta.get("versionId"))
.and_then(Value::as_str);
if !seen.insert((url, version)) {
return Err(ReceiveError::DuplicateFullUrl { entry: index });
}
if let Url::Rest { kind, id, .. } = parsed {
let resource_id = entry
.resource
.and_then(|resource| resource.get("id"))
.and_then(Value::as_str);
if entry.resource_type() != Some(kind) || resource_id != Some(id) {
return Err(ReceiveError::FullUrlMismatch { entry: index });
}
}
}
Ok(())
}
pub(super) fn patient(tree: &Object) -> Result<usize, ReceiveError> {
let entries = entries(tree);
let subject = entries
.first()
.and_then(|entry| entry.resource)
.and_then(|composition| composition.get("subject"))
.and_then(Value::as_object)
.ok_or(ReceiveError::NoSubject)?;
let patient = match resolve_in(&entries, 0, subject) {
Ok(Target::Entry(index))
if entries
.get(index)
.is_some_and(|entry| entry.resource_type() == Some("Patient")) =>
{
index
}
Err(ReferenceError::Unreferenced { .. }) => return Err(ReceiveError::NoSubject),
_ => return Err(ReceiveError::SubjectUnresolved),
};
if let Some((other, _)) = entries
.iter()
.enumerate()
.find(|(index, entry)| *index != patient && entry.resource_type() == Some("Patient"))
{
return Err(ReceiveError::SeveralPatients { entry: other });
}
for (index, entry) in entries.iter().enumerate() {
if let Some(resource) = entry.resource {
let location = format!("Bundle.entry[{index}].resource");
references::hold(&entries, index, patient, resource, &location)?;
}
}
Ok(patient)
}