use fhir_types::codec::EncodeError;
use fhir_types::codec::Json;
use fhir_types::codec::Object;
use fhir_types::codec::Value;
use fhir_types::r4::bundle::Bundle;
use fhir_types::r4::reference::Reference;
use fhir_types::r4::schema::SCHEMAS;
const ID_LENGTH: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Entry(usize),
Contained {
entry: usize,
id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ReferenceError {
#[error("the reference names no target by reference")]
Unreferenced {
identifier: bool,
},
#[error("the reference is not spelled in its canonical form")]
NonCanonical,
#[error("the reference names no contained resource")]
LocalMissing,
#[error("a relative reference is held by an entry with no REST-style fullUrl")]
RelativeWithoutBase,
#[error("the reference names no entry of the document")]
NotInBundle {
kind: Option<String>,
},
#[error("the reference names more than one entry")]
Ambiguous,
#[error("the reference names a version its entry does not carry")]
VersionMismatch,
#[error("the reference's type is not its target's type")]
TypeDisagrees,
#[error("the reference's identifier is not one of its target's")]
IdentifierDisagrees,
#[error("the document cannot be encoded as JSON")]
Unencodable {
#[source]
source: EncodeError,
},
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Entry<'d> {
pub(super) full_url: Option<&'d str>,
pub(super) resource: Option<&'d Object>,
}
impl Entry<'_> {
pub(super) fn resource_type(&self) -> Option<&str> {
self.resource?.get("resourceType")?.as_str()
}
}
pub(super) fn entries(tree: &Object) -> Vec<Entry<'_>> {
tree.get("entry")
.and_then(Value::as_array)
.unwrap_or_default()
.iter()
.map(|entry| Entry {
full_url: entry.get("fullUrl").and_then(Value::as_str),
resource: entry.get("resource").and_then(Value::as_object),
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Url<'u> {
Urn,
Rest {
base: &'u str,
kind: &'u str,
id: &'u str,
version: Option<&'u str>,
},
Other,
}
impl<'u> Url<'u> {
pub(super) fn parse(text: &'u str) -> Option<Self> {
if let Some(uuid) = text.strip_prefix("urn:uuid:") {
return canonical_uuid(uuid).then_some(Self::Urn);
}
if let Some(oid) = text.strip_prefix("urn:oid:") {
return canonical_oid(oid).then_some(Self::Urn);
}
let rest = text
.strip_prefix("https://")
.or_else(|| text.strip_prefix("http://"))?;
let (host, path) = rest.split_once('/').unwrap_or((rest, ""));
let canonical_host = !host.is_empty()
&& host.chars().all(|character| {
character.is_ascii_lowercase()
|| character.is_ascii_digit()
|| matches!(character, '-' | '.' | ':')
});
let segments: Vec<&str> = path.split('/').collect();
let canonical_path = path.is_empty()
|| segments.iter().all(|segment| {
!segment.is_empty()
&& *segment != "."
&& *segment != ".."
&& segment.chars().all(|character| {
character.is_ascii_alphanumeric()
|| matches!(character, '-' | '.' | '_' | '~')
})
});
if !canonical_host || !canonical_path {
return None;
}
let (unversioned, version) = match text.rsplit_once("/_history/") {
Some((unversioned, version)) => (unversioned, Some(version)),
None => (text, None),
};
if version.is_some_and(|version| !canonical_id(version)) {
return None;
}
let rest_style = unversioned.rsplit_once('/').and_then(|(head, id)| {
let (base, kind) = head.rsplit_once('/')?;
(SCHEMAS.is_resource(kind) && canonical_id(id) && base.contains("://")).then_some(
Self::Rest {
base,
kind,
id,
version,
},
)
});
match (rest_style, version) {
(Some(url), _) => Some(url),
(None, Some(_)) => None,
(None, None) => Some(Self::Other),
}
}
}
fn canonical_id(text: &str) -> bool {
(1..=ID_LENGTH).contains(&text.len())
&& text
.chars()
.all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '.'))
}
fn canonical_uuid(text: &str) -> bool {
let groups: Vec<&str> = text.split('-').collect();
groups.iter().map(|group| group.len()).eq([8, 4, 4, 4, 12])
&& groups.iter().all(|group| {
group
.chars()
.all(|character| character.is_ascii_digit() || ('a'..='f').contains(&character))
})
}
fn canonical_oid(text: &str) -> bool {
let mut arcs = text.split('.');
let first = arcs.next();
let rest: Vec<&str> = arcs.collect();
matches!(first, Some("0" | "1" | "2"))
&& !rest.is_empty()
&& rest.iter().all(|arc| {
!arc.is_empty()
&& arc.chars().all(|character| character.is_ascii_digit())
&& (*arc == "0" || !arc.starts_with('0'))
})
}
pub fn resolve(
bundle: &Bundle,
holder: usize,
reference: &Reference,
) -> Result<Target, ReferenceError> {
let tree = Json::to_json(bundle).map_err(|source| ReferenceError::Unencodable { source })?;
let reference =
Json::to_json(reference).map_err(|source| ReferenceError::Unencodable { source })?;
resolve_in(&entries(&tree), holder, &reference)
}
pub(super) fn resolve_in(
entries: &[Entry<'_>],
holder: usize,
reference: &Object,
) -> Result<Target, ReferenceError> {
let declared = reference.get("type").and_then(Value::as_str);
let identifier = reference.get("identifier").and_then(Value::as_object);
let Some(text) = reference.get("reference").and_then(Value::as_str) else {
return Err(ReferenceError::Unreferenced {
identifier: identifier.is_some(),
});
};
if declared.is_some_and(|kind| !SCHEMAS.is_resource(kind)) {
return Err(ReferenceError::TypeDisagrees);
}
let (target, resource) = if let Some(id) = text.strip_prefix('#') {
local(entries, holder, id)?
} else {
let index = entry(entries, holder, text)?;
let resource = entries
.get(index)
.and_then(|entry| entry.resource)
.ok_or(ReferenceError::NotInBundle { kind: None })?;
(Target::Entry(index), resource)
};
let kind = resource.get("resourceType").and_then(Value::as_str);
if declared.is_some_and(|declared| Some(declared) != kind) {
return Err(ReferenceError::TypeDisagrees);
}
if let Some(identifier) = identifier
&& !carries(resource, identifier)
{
return Err(ReferenceError::IdentifierDisagrees);
}
Ok(target)
}
fn local<'d>(
entries: &[Entry<'d>],
holder: usize,
id: &str,
) -> Result<(Target, &'d Object), ReferenceError> {
let resource = entries
.get(holder)
.and_then(|entry| entry.resource)
.ok_or(ReferenceError::LocalMissing)?;
if id.is_empty() {
return Ok((Target::Entry(holder), resource));
}
if !canonical_id(id) {
return Err(ReferenceError::NonCanonical);
}
let mut named = resource
.get("contained")
.and_then(Value::as_array)
.unwrap_or_default()
.iter()
.filter_map(Value::as_object)
.filter(|contained| contained.get("id").and_then(Value::as_str) == Some(id));
match (named.next(), named.next()) {
(Some(contained), None) => Ok((
Target::Contained {
entry: holder,
id: id.to_owned(),
},
contained,
)),
(Some(_), Some(_)) => Err(ReferenceError::Ambiguous),
(None, _) => Err(ReferenceError::LocalMissing),
}
}
fn entry(entries: &[Entry<'_>], holder: usize, text: &str) -> Result<usize, ReferenceError> {
let absolute = if text.contains(':') {
Url::parse(text).ok_or(ReferenceError::NonCanonical)?;
text.to_owned()
} else {
let segments: Vec<&str> = text.split('/').collect();
let relative = match segments.as_slice() {
[kind, id] => SCHEMAS.is_resource(kind) && canonical_id(id),
[kind, id, "_history", version] => {
SCHEMAS.is_resource(kind) && canonical_id(id) && canonical_id(version)
}
_ => false,
};
if !relative {
return Err(ReferenceError::NonCanonical);
}
let from = entries
.get(holder)
.and_then(|entry| entry.full_url)
.and_then(Url::parse);
let Some(Url::Rest { base, .. }) = from else {
return Err(ReferenceError::RelativeWithoutBase);
};
format!("{base}/{text}")
};
let (unversioned, version, kind) = match Url::parse(&absolute) {
Some(Url::Rest {
base,
kind,
id,
version,
}) => (format!("{base}/{kind}/{id}"), version, Some(kind)),
Some(Url::Urn | Url::Other) => (absolute.clone(), None, None),
None => return Err(ReferenceError::NonCanonical),
};
let named: Vec<usize> = entries
.iter()
.enumerate()
.filter(|(_, entry)| entry.full_url == Some(unversioned.as_str()))
.map(|(index, _)| index)
.collect();
let named: Vec<usize> = match version {
None => named,
Some(version) => {
let versioned: Vec<usize> = named
.iter()
.copied()
.filter(|index| version_of(entries, *index) == Some(version))
.collect();
if versioned.is_empty() && !named.is_empty() {
return Err(ReferenceError::VersionMismatch);
}
versioned
}
};
match named.as_slice() {
[index] => Ok(*index),
[] => Err(ReferenceError::NotInBundle {
kind: kind.map(str::to_owned),
}),
_ => Err(ReferenceError::Ambiguous),
}
}
fn version_of<'d>(entries: &[Entry<'d>], index: usize) -> Option<&'d str> {
entries
.get(index)?
.resource?
.get("meta")?
.get("versionId")?
.as_str()
}
fn carries(resource: &Object, identifier: &Object) -> bool {
let wanted = (
identifier.get("system").and_then(Value::as_str),
identifier.get("value").and_then(Value::as_str),
);
if wanted.1.is_none() {
return false;
}
let carried: Vec<&Value> = match resource.get("identifier") {
Some(Value::Array(items)) => items.iter().collect(),
Some(single) => vec![single],
None => Vec::new(),
};
carried.iter().any(|carried| {
(
carried.get("system").and_then(Value::as_str),
carried.get("value").and_then(Value::as_str),
) == wanted
})
}