mod response;
use fhir_types::r4::endpoint::Endpoint;
use fhir_types::r4::organization::Organization;
use http::header::{ACCEPT, CONTENT_TYPE, DATE};
use std::fmt;
use std::sync::Arc;
use url::Url;
use super::budget::Budget;
use super::error::{InvalidBase, McsdError};
use crate::authorizer::{self, Authorizer};
use ihe_atna::redact::RedactedUrl;
const FHIR_JSON: &str = "application/fhir+json";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum CareService {
Organization,
Endpoint,
}
impl CareService {
#[must_use]
pub const fn resource_type(self) -> &'static str {
match self {
Self::Organization => "Organization",
Self::Endpoint => "Endpoint",
}
}
}
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CareResource {
Organization(Box<Organization>),
Endpoint(Box<Endpoint>),
}
impl CareResource {
#[must_use]
pub fn kind(&self) -> CareService {
match self {
Self::Organization(_) => CareService::Organization,
Self::Endpoint(_) => CareService::Endpoint,
}
}
#[must_use]
pub fn logical_id(&self) -> Option<&str> {
match self {
Self::Organization(resource) => resource.id.as_deref(),
Self::Endpoint(resource) => resource.id.as_deref(),
}
}
#[must_use]
pub fn identified_in(&self, system: &str) -> bool {
let identifiers = match self {
Self::Organization(resource) => &resource.identifier,
Self::Endpoint(resource) => &resource.identifier,
};
identifiers.iter().any(|identifier| {
identifier
.system
.as_ref()
.and_then(|uri| uri.value.as_deref())
== Some(system)
})
}
}
impl fmt::Debug for CareResource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CareResource")
.field("kind", &self.kind())
.field("logical_id", &self.logical_id())
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Match {
full_url: String,
resource: CareResource,
}
impl Match {
#[must_use]
pub fn full_url(&self) -> &str {
&self.full_url
}
#[must_use]
pub fn resource(&self) -> &CareResource {
&self.resource
}
#[must_use]
pub fn into_parts(self) -> (String, CareResource) {
(self.full_url, self.resource)
}
}
impl fmt::Debug for Match {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Match")
.field("full_url", &RedactedUrl(&self.full_url))
.field("resource", &self.resource)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Found {
matches: Vec<Match>,
answered_at: Option<String>,
}
impl Found {
#[must_use]
pub fn matches(&self) -> &[Match] {
&self.matches
}
#[must_use]
pub fn into_matches(self) -> Vec<Match> {
self.matches
}
#[must_use]
pub fn answered_at(&self) -> Option<&str> {
self.answered_at.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Version {
Current {
full_url: String,
resource: CareResource,
},
Deleted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Change {
logical_id: String,
version: Version,
}
impl Change {
#[must_use]
pub fn logical_id(&self) -> &str {
&self.logical_id
}
#[must_use]
pub fn version(&self) -> &Version {
&self.version
}
#[must_use]
pub fn into_parts(self) -> (String, Version) {
(self.logical_id, self.version)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Updates {
changes: Vec<Change>,
answered_at: Option<String>,
}
impl Updates {
#[must_use]
pub fn changes(&self) -> &[Change] {
&self.changes
}
#[must_use]
pub fn into_changes(self) -> Vec<Change> {
self.changes
}
#[must_use]
pub fn answered_at(&self) -> Option<&str> {
self.answered_at.as_deref()
}
}
#[derive(Clone)]
pub struct McsdClient {
base: Url,
organization: Interactions,
endpoint: Interactions,
http: reqwest::Client,
authorizer: Option<Arc<dyn Authorizer>>,
#[cfg(feature = "balp")]
audit: Option<Arc<dyn ihe_atna::balp::AuditRecorder>>,
}
#[derive(Clone)]
struct Interactions {
search: Url,
history: Url,
}
impl Interactions {
fn of(base: &Url, kind: CareService) -> Result<Self, InvalidBase> {
let search = ihe_atna::fhir_base::under_base(base.clone(), kind.resource_type())
.ok_or(InvalidBase)?;
let history = ihe_atna::fhir_base::under_base(
base.clone(),
&format!("{}/_history", kind.resource_type()),
)
.ok_or(InvalidBase)?;
Ok(Self { search, history })
}
}
impl McsdClient {
pub fn new(base: Url, http: reqwest::Client) -> Result<Self, InvalidBase> {
let base = ihe_atna::fhir_base::under_base(base, "").ok_or(InvalidBase)?;
Ok(Self {
organization: Interactions::of(&base, CareService::Organization)?,
endpoint: Interactions::of(&base, CareService::Endpoint)?,
base,
http,
authorizer: None,
#[cfg(feature = "balp")]
audit: None,
})
}
#[must_use]
pub fn with_authorizer(mut self, authorizer: Arc<dyn Authorizer>) -> Self {
self.authorizer = Some(authorizer);
self
}
#[cfg(feature = "balp")]
#[must_use]
pub fn audited(mut self, recorder: Arc<dyn ihe_atna::balp::AuditRecorder>) -> Self {
self.audit = Some(recorder);
self
}
#[cfg(feature = "balp")]
async fn audit<T>(
&self,
kind: ihe_atna::balp::EventKind,
request: &Url,
result: Result<T, McsdError>,
) -> Result<T, McsdError> {
if let Some(recorder) = &self.audit {
let exchange = super::audit::exchange(kind, &self.base, request, &result);
recorder.record(exchange).await.map_err(McsdError::Audit)?;
}
result
}
fn interactions(&self, kind: CareService) -> &Interactions {
match kind {
CareService::Organization => &self.organization,
CareService::Endpoint => &self.endpoint,
}
}
#[must_use]
pub fn base(&self) -> &Url {
&self.base
}
pub async fn find(
&self,
kind: CareService,
parameters: &[(&str, &str)],
budget: &mut Budget,
) -> Result<Found, McsdError> {
let mut url = self.interactions(kind).search.clone();
if !parameters.is_empty() {
url.query_pairs_mut().extend_pairs(parameters);
}
let result = self.find_pages(url.clone(), kind, budget).await;
#[cfg(feature = "balp")]
let result = self.audit(super::audit::query(), &url, result).await;
result
}
async fn find_pages(
&self,
url: Url,
kind: CareService,
budget: &mut Budget,
) -> Result<Found, McsdError> {
let mut matches = Vec::new();
let mut answered_at = None;
let mut next = Some(url);
let mut first = true;
while let Some(url) = next.take() {
let page = self.page(url, budget).await?;
if first {
answered_at = page.date;
first = false;
}
let read = response::searchset(&page.body, kind, &self.base)?;
budget.entries(read.entries)?;
matches.extend(read.matches);
next = read.next;
}
Ok(Found {
matches,
answered_at,
})
}
pub async fn updates(
&self,
kind: CareService,
since: &str,
budget: &mut Budget,
) -> Result<Updates, McsdError> {
let mut url = self.interactions(kind).history.clone();
url.query_pairs_mut().append_pair("_since", since);
let result = self.history_pages(url.clone(), kind, budget).await;
#[cfg(feature = "balp")]
let result = self.audit(super::audit::updates(), &url, result).await;
result
}
async fn history_pages(
&self,
url: Url,
kind: CareService,
budget: &mut Budget,
) -> Result<Updates, McsdError> {
let mut changes = Vec::new();
let mut answered_at = None;
let mut next = Some(url);
let mut first = true;
while let Some(url) = next.take() {
let page = self.page(url, budget).await?;
if first {
answered_at = page.date;
first = false;
}
let read = response::history(&page.body, kind, &self.base)?;
budget.entries(read.changes.len())?;
changes.extend(read.changes);
next = read.next;
}
Ok(Updates {
changes,
answered_at,
})
}
async fn page(&self, url: Url, budget: &mut Budget) -> Result<response::Page, McsdError> {
budget.page()?;
if url.origin() != self.base.origin() {
return Err(McsdError::ForeignPage);
}
let request = self
.http
.get(url)
.header(ACCEPT, FHIR_JSON)
.build()
.map_err(response::transport)?;
let answer = authorizer::send(
&self.http,
self.authorizer.as_deref(),
request,
budget.remaining()?,
)
.await
.map_err(response::unsent)?;
let status = answer.status();
let media = answer
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let date = answer
.headers()
.get(DATE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let body = response::body(answer, budget).await?;
response::page(status, media.as_deref(), body, date)
}
}
impl fmt::Debug for McsdClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("McsdClient")
.field("base", &RedactedUrl(self.base.as_str()))
.field("authorizer", &self.authorizer.is_some())
.finish_non_exhaustive()
}
}