#[cfg(feature = "balp")]
pub mod audit;
pub mod error;
pub mod identifier;
mod request;
mod response;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use http::header::{ACCEPT, CONTENT_TYPE};
use secrecy::ExposeSecret;
use url::Url;
use crate::authorizer::{self, Authorizer};
use error::{InvalidInput, PixmError};
use identifier::{CrossReference, SourceIdentifier, TargetSystem};
use ihe_atna::redact::RedactedUrl;
use ihe_atna::user::OnBehalfOf;
use request::Request;
const FHIR_JSON: &str = "application/fhir+json";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Invocation {
#[default]
Get,
Post,
}
#[derive(Clone)]
pub struct PixmClient {
endpoint: Url,
invocation: Invocation,
http: reqwest::Client,
authorizer: Option<Arc<dyn Authorizer>>,
#[cfg(feature = "balp")]
audit: Option<Arc<dyn ihe_atna::balp::AuditRecorder>>,
}
impl PixmClient {
pub fn new(base: Url, http: reqwest::Client) -> Result<Self, InvalidInput> {
Ok(Self {
endpoint: request::endpoint(base)?,
invocation: Invocation::default(),
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
}
#[must_use]
pub fn invoked_by(mut self, invocation: Invocation) -> Self {
self.invocation = invocation;
self
}
#[must_use]
pub fn invocation(&self) -> Invocation {
self.invocation
}
#[cfg(feature = "balp")]
#[must_use]
pub fn audited(mut self, recorder: Arc<dyn ihe_atna::balp::AuditRecorder>) -> Self {
self.audit = Some(recorder);
self
}
#[must_use]
pub fn endpoint(&self) -> &Url {
&self.endpoint
}
pub async fn cross_reference(
&self,
source: &SourceIdentifier,
targets: &[TargetSystem],
#[cfg_attr(
not(feature = "balp"),
expect(
unused_variables,
reason = "only an audited client names whom it acted for"
)
)]
on_behalf: &OnBehalfOf,
timeout: Duration,
) -> Result<CrossReference, PixmError> {
#[cfg(feature = "balp")]
let deadline = ihe_atna::balp::recording::deadline(timeout);
let request = request::request(&self.endpoint, self.invocation, source, targets)
.map_err(PixmError::Unwritable)?;
let result = self.ask(&request, source, targets, timeout).await;
#[cfg(feature = "balp")]
if let Some(recorder) = &self.audit {
use ihe_atna::balp::recording::{Late, Recorded, within};
let exchange = audit::exchange(&self.endpoint, (&request, source), on_behalf, &result);
match within(deadline, recorder.record(exchange)).await {
Recorded::Refused(error) => return Err(PixmError::Audit(error)),
Recorded::Late if result.is_ok() => {
return Err(PixmError::Audit(ihe_atna::balp::AuditError(Box::new(
Late::new(),
))));
}
Recorded::Accepted | Recorded::Late => {}
}
}
result
}
async fn ask(
&self,
request: &Request,
source: &SourceIdentifier,
targets: &[TargetSystem],
timeout: Duration,
) -> Result<CrossReference, PixmError> {
let builder = match request {
Request::Get(url) => self.http.get(url.clone()),
Request::Post(body) => self
.http
.post(self.endpoint.clone())
.header(CONTENT_TYPE, FHIR_JSON)
.body(body.expose_secret().as_bytes().to_vec()),
};
let built = builder
.header(ACCEPT, FHIR_JSON)
.build()
.map_err(error::transport)?;
let response = authorizer::send(&self.http, self.authorizer.as_deref(), built, timeout)
.await
.map_err(error::unsent)?;
let status = response.status();
let media = response
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let body = response::body(response).await?;
response::read(status, media.as_deref(), &body, source, targets)
}
}
impl fmt::Debug for PixmClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PixmClient")
.field("endpoint", &RedactedUrl(self.endpoint.as_str()))
.field("invocation", &self.invocation)
.field("authorizer", &self.authorizer.is_some())
.finish_non_exhaustive()
}
}