use std::collections::BTreeSet;
use std::fmt;
use fhir_types::codec::{Json, Path, Value};
use fhir_types::r4::bundle::Bundle;
use fhir_types::r4::endpoint::Endpoint;
use fhir_types::r4::extension::ExtensionValue;
use fhir_types::r4::identifier::Identifier;
use fhir_types::r4::organization::Organization;
use fhir_types::r4::reference::Reference;
use fhir_types::r4::resource::Resource;
use super::error::DirectoryError;
use ihe_atna::redact::RedactedUrl;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Directory {
organizations: Vec<DirectoryOrganization>,
endpoints: Vec<DirectoryEndpoint>,
}
#[derive(Clone, PartialEq, Eq)]
pub struct DirectoryOrganization {
entry: usize,
full_url: Option<String>,
resource: Organization,
}
#[derive(Clone, PartialEq, Eq)]
pub struct DirectoryEndpoint {
entry: usize,
full_url: Option<String>,
resource: Endpoint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Resolution<'a, T> {
Found(&'a T),
Outside(&'a str),
NotLiteral,
}
impl Directory {
pub fn from_json(body: &[u8]) -> Result<Self, DirectoryError> {
let value: Value =
serde_json::from_slice(body).map_err(|error| DirectoryError::NotJson {
line: error.line(),
column: error.column(),
})?;
let object = value.as_object().ok_or(DirectoryError::NotABundle)?;
if object.get("resourceType").and_then(Value::as_str) != Some("Bundle") {
return Err(DirectoryError::NotABundle);
}
let bundle =
Bundle::from_json(object, &mut Path::root("Bundle")).map_err(DirectoryError::Decode)?;
let kind = bundle.r#type.value.as_deref();
if !matches!(kind, Some("collection" | "searchset")) {
return Err(DirectoryError::BundleType {
found: kind.map(str::to_owned),
});
}
let mut builder = Builder::default();
for (index, entry) in bundle.entry.into_iter().enumerate() {
let full_url = entry
.full_url
.and_then(|full_url| full_url.value)
.filter(|full_url| !full_url.is_empty());
match entry.resource {
Some(Resource::Organization(resource)) => {
builder.organization(index, full_url, *resource)?;
}
Some(Resource::Endpoint(resource)) => {
builder.endpoint(index, full_url, *resource)?;
}
Some(_) => return Err(DirectoryError::UnexpectedEntry { index }),
None => return Err(DirectoryError::NoResource { index }),
}
}
Ok(builder.directory)
}
pub(crate) fn from_resources(
organizations: impl IntoIterator<Item = (String, Organization)>,
endpoints: impl IntoIterator<Item = (String, Endpoint)>,
) -> Result<Self, DirectoryError> {
let mut builder = Builder::default();
let mut index = 0_usize;
for (full_url, resource) in organizations {
builder.organization(index, Some(full_url), resource)?;
index = index.saturating_add(1);
}
for (full_url, resource) in endpoints {
builder.endpoint(index, Some(full_url), resource)?;
index = index.saturating_add(1);
}
Ok(builder.directory)
}
#[must_use]
pub fn organizations(&self) -> &[DirectoryOrganization] {
&self.organizations
}
#[must_use]
pub fn endpoints(&self) -> &[DirectoryEndpoint] {
&self.endpoints
}
#[must_use]
pub fn managing_organization<'a>(
&'a self,
endpoint: &'a DirectoryEndpoint,
) -> Option<Resolution<'a, DirectoryOrganization>> {
let reference = endpoint.resource.managing_organization.as_ref()?;
Some(resolve(reference, endpoint, &self.organizations))
}
pub fn endpoints_of<'a>(
&'a self,
organization: &'a DirectoryOrganization,
) -> impl Iterator<Item = Resolution<'a, DirectoryEndpoint>> {
organization
.resource
.endpoint
.iter()
.map(move |reference| resolve(reference, organization, &self.endpoints))
}
}
#[derive(Default)]
struct Builder {
directory: Directory,
full_urls: BTreeSet<String>,
organization_ids: BTreeSet<String>,
endpoint_ids: BTreeSet<String>,
}
impl Builder {
fn organization(
&mut self,
index: usize,
full_url: Option<String>,
resource: Organization,
) -> Result<(), DirectoryError> {
self.full_url(index, full_url.as_ref())?;
if !resource.modifier_extension.is_empty() {
return Err(DirectoryError::ModifierExtension { index });
}
if let Some(id) = &resource.id
&& !self.organization_ids.insert(id.clone())
{
return Err(DirectoryError::DuplicateId { index });
}
self.directory.organizations.push(DirectoryOrganization {
entry: index,
full_url,
resource,
});
Ok(())
}
fn endpoint(
&mut self,
index: usize,
full_url: Option<String>,
resource: Endpoint,
) -> Result<(), DirectoryError> {
self.full_url(index, full_url.as_ref())?;
if !resource.modifier_extension.is_empty() {
return Err(DirectoryError::ModifierExtension { index });
}
if let Some(id) = &resource.id
&& !self.endpoint_ids.insert(id.clone())
{
return Err(DirectoryError::DuplicateId { index });
}
self.directory.endpoints.push(DirectoryEndpoint {
entry: index,
full_url,
resource,
});
Ok(())
}
fn full_url(&mut self, index: usize, full_url: Option<&String>) -> Result<(), DirectoryError> {
match full_url {
Some(full_url) if !self.full_urls.insert(full_url.clone()) => {
Err(DirectoryError::DuplicateFullUrl { index })
}
_ => Ok(()),
}
}
}
impl DirectoryOrganization {
#[must_use]
pub fn entry(&self) -> usize {
self.entry
}
#[must_use]
pub fn full_url(&self) -> Option<&str> {
self.full_url.as_deref()
}
#[must_use]
pub fn logical_id(&self) -> Option<&str> {
self.resource.id.as_deref()
}
pub fn identifier_values<'a>(
&'a self,
system: &'a str,
) -> impl Iterator<Item = Option<&'a str>> {
values(&self.resource.identifier, system)
}
pub fn identifiers(&self) -> impl Iterator<Item = (Option<&str>, Option<&str>)> {
self.resource.identifier.iter().map(|identifier| {
(
identifier
.system
.as_ref()
.and_then(|uri| uri.value.as_deref()),
identifier
.value
.as_ref()
.and_then(|value| value.value.as_deref()),
)
})
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.resource.name.as_ref()?.value.as_deref()
}
#[must_use]
pub fn active(&self) -> Option<bool> {
self.resource.active.as_ref()?.value
}
#[must_use]
pub fn resource(&self) -> &Organization {
&self.resource
}
}
impl DirectoryEndpoint {
#[must_use]
pub fn entry(&self) -> usize {
self.entry
}
#[must_use]
pub fn full_url(&self) -> Option<&str> {
self.full_url.as_deref()
}
#[must_use]
pub fn logical_id(&self) -> Option<&str> {
self.resource.id.as_deref()
}
pub fn identifier_values<'a>(
&'a self,
system: &'a str,
) -> impl Iterator<Item = Option<&'a str>> {
values(&self.resource.identifier, system)
}
#[must_use]
pub fn status(&self) -> Option<&str> {
self.resource.status.value.as_deref()
}
#[must_use]
pub fn connection_type_system(&self) -> Option<&str> {
self.resource
.connection_type
.system
.as_ref()?
.value
.as_deref()
}
#[must_use]
pub fn connection_type_code(&self) -> Option<&str> {
self.resource
.connection_type
.code
.as_ref()?
.value
.as_deref()
}
pub fn extension_codes<'a>(&'a self, url: &'a str) -> impl Iterator<Item = Option<&'a str>> {
self.resource
.extension
.iter()
.filter(move |extension| extension.url == url)
.map(|extension| match &extension.value {
Some(ExtensionValue::Code(code)) => code.value.as_deref(),
_ => None,
})
}
pub fn extension_canonicals<'a>(
&'a self,
url: &'a str,
) -> impl Iterator<Item = Option<&'a str>> {
self.extensions(url).map(|value| match value {
Some(ExtensionValue::Canonical(canonical)) => canonical.value.as_deref(),
_ => None,
})
}
pub fn extension_uris<'a>(&'a self, url: &'a str) -> impl Iterator<Item = Option<&'a str>> {
self.extensions(url).map(|value| match value {
Some(ExtensionValue::Uri(uri)) => uri.value.as_deref(),
_ => None,
})
}
pub fn payload_type_codes<'a>(
&'a self,
system: &'a str,
) -> impl Iterator<Item = Option<&'a str>> {
self.resource
.payload_type
.iter()
.flat_map(|concept| concept.coding.iter())
.filter(move |coding| {
coding.system.as_ref().and_then(|uri| uri.value.as_deref()) == Some(system)
})
.map(|coding| coding.code.as_ref()?.value.as_deref())
}
fn extensions<'a>(&'a self, url: &'a str) -> impl Iterator<Item = Option<&'a ExtensionValue>> {
self.resource
.extension
.iter()
.filter(move |extension| extension.url == url)
.map(|extension| extension.value.as_ref())
}
#[must_use]
pub fn address(&self) -> Option<&str> {
self.resource.address.value.as_deref()
}
#[must_use]
pub fn resource(&self) -> &Endpoint {
&self.resource
}
}
impl fmt::Debug for DirectoryOrganization {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DirectoryOrganization")
.field("entry", &self.entry)
.field("full_url", &self.full_url.as_deref().map(RedactedUrl))
.field("logical_id", &self.logical_id())
.field("name", &self.name())
.field("active", &self.active())
.finish_non_exhaustive()
}
}
impl fmt::Debug for DirectoryEndpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DirectoryEndpoint")
.field("entry", &self.entry)
.field("full_url", &self.full_url.as_deref().map(RedactedUrl))
.field("logical_id", &self.logical_id())
.field("status", &self.status())
.field("connection_type_system", &self.connection_type_system())
.field("connection_type_code", &self.connection_type_code())
.field("address", &self.address().map(RedactedUrl))
.finish_non_exhaustive()
}
}
fn resolve<'a, T: Held>(
reference: &'a Reference,
referrer: &impl Held,
held: &'a [T],
) -> Resolution<'a, T> {
let Some(literal) = reference
.reference
.as_ref()
.and_then(|reference| reference.value.as_deref())
else {
return Resolution::NotLiteral;
};
let absolute = if literal.contains(':') {
Some(literal.to_owned())
} else if is_relative(literal) {
referrer
.full_url()
.and_then(|full_url| restful_root(full_url, referrer.kind()))
.map(|root| format!("{root}/{literal}"))
} else {
None
};
absolute
.and_then(|absolute| {
held.iter()
.find(|resource| resource.full_url() == Some(absolute.as_str()))
})
.map_or(Resolution::Outside(literal), Resolution::Found)
}
fn is_relative(literal: &str) -> bool {
literal
.split_once('/')
.is_some_and(|(kind, id)| !kind.is_empty() && !id.is_empty() && !id.contains('/'))
}
fn restful_root<'a>(full_url: &'a str, kind: &str) -> Option<&'a str> {
let (rest, id) = full_url.rsplit_once('/')?;
let (root, found) = rest.rsplit_once('/')?;
let restful = found == kind
&& !id.is_empty()
&& (root.starts_with("https://") || root.starts_with("http://"));
restful.then_some(root)
}
trait Held {
fn full_url(&self) -> Option<&str>;
fn kind(&self) -> &'static str;
}
impl Held for DirectoryOrganization {
fn full_url(&self) -> Option<&str> {
self.full_url.as_deref()
}
fn kind(&self) -> &'static str {
"Organization"
}
}
impl Held for DirectoryEndpoint {
fn full_url(&self) -> Option<&str> {
self.full_url.as_deref()
}
fn kind(&self) -> &'static str {
"Endpoint"
}
}
fn values<'a>(
identifiers: &'a [Identifier],
system: &'a str,
) -> impl Iterator<Item = Option<&'a str>> {
identifiers
.iter()
.filter(move |identifier| {
identifier
.system
.as_ref()
.and_then(|uri| uri.value.as_deref())
== Some(system)
})
.map(|identifier| identifier.value.as_ref()?.value.as_deref())
}