#[cfg(feature = "balp")]
pub mod audit;
pub mod error;
pub mod input;
pub mod matches;
pub mod query;
mod request;
mod response;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use http::header::{ACCEPT, CONTENT_TYPE};
use url::Url;
use crate::authorizer::{self, Authorizer};
use error::{InvalidInput, PdqmError};
use ihe_atna::redact::RedactedUrl;
use ihe_atna::user::OnBehalfOf;
use input::MatchInput;
use matches::{MatchResult, Page, SearchResult};
use query::PatientQuery;
const FHIR_JSON: &str = "application/fhir+json";
#[derive(Clone)]
pub struct PdqmClient {
base: Url,
endpoint: Url,
match_endpoint: Url,
http: reqwest::Client,
authorizer: Option<Arc<dyn Authorizer>>,
#[cfg(feature = "balp")]
audit: Option<Arc<dyn ihe_atna::balp::AuditRecorder>>,
}
impl PdqmClient {
pub fn new(base: Url, http: reqwest::Client) -> Result<Self, InvalidInput> {
let base = request::base(base)?;
Ok(Self {
endpoint: request::endpoint(base.clone())?,
match_endpoint: request::match_endpoint(base.clone())?,
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(
&self,
request: impl FnOnce() -> secrecy::SecretString,
on_behalf: &OnBehalfOf,
result: Result<SearchResult, PdqmError>,
deadline: Option<tokio::time::Instant>,
) -> Result<SearchResult, PdqmError> {
use ihe_atna::balp::recording::{Late, Recorded, within};
if let Some(recorder) = &self.audit {
let exchange = audit::exchange(&self.base, request(), on_behalf, &result);
match within(deadline, recorder.record(exchange)).await {
Recorded::Refused(error) => return Err(PdqmError::Audit(error)),
Recorded::Late if result.is_ok() => {
return Err(PdqmError::Audit(ihe_atna::balp::AuditError(Box::new(
Late::new(),
))));
}
Recorded::Accepted | Recorded::Late => {}
}
}
result
}
#[must_use]
pub fn endpoint(&self) -> &Url {
&self.endpoint
}
#[must_use]
pub fn match_endpoint(&self) -> &Url {
&self.match_endpoint
}
pub async fn match_patient(
&self,
input: &MatchInput,
#[cfg_attr(
not(feature = "balp"),
expect(
unused_variables,
reason = "only an audited client names whom it acted for"
)
)]
on_behalf: &OnBehalfOf,
timeout: Duration,
) -> Result<MatchResult, PdqmError> {
#[cfg(feature = "balp")]
let deadline = ihe_atna::balp::recording::deadline(timeout);
let body = secrecy::SecretString::from(
serde_json::to_string(&input.parameters()).map_err(PdqmError::Unwritable)?,
);
let result = self.post_match(&body, timeout).await;
#[cfg(feature = "balp")]
let result = self
.audit_match((input, &body), on_behalf, result, deadline)
.await;
result
}
#[cfg(feature = "balp")]
async fn audit_match(
&self,
(input, body): (&MatchInput, &secrecy::SecretString),
on_behalf: &OnBehalfOf,
result: Result<MatchResult, PdqmError>,
deadline: Option<tokio::time::Instant>,
) -> Result<MatchResult, PdqmError> {
use ihe_atna::balp::recording::{Late, Recorded, within};
use secrecy::ExposeSecret as _;
if let Some(recorder) = &self.audit {
let request = secrecy::SecretString::from(format!(
"POST {}\nContent-Type: {FHIR_JSON}\n\n{}",
ihe_atna::balp::request_text(&self.match_endpoint).expose_secret(),
body.expose_secret()
));
let exchange = audit::match_exchange(
&self.base,
(request, input.sole_identifier()),
on_behalf,
&result,
);
match within(deadline, recorder.record(exchange)).await {
Recorded::Refused(error) => return Err(PdqmError::Audit(error)),
Recorded::Late if result.is_ok() => {
return Err(PdqmError::Audit(ihe_atna::balp::AuditError(Box::new(
Late::new(),
))));
}
Recorded::Accepted | Recorded::Late => {}
}
}
result
}
async fn post_match(
&self,
body: &secrecy::SecretString,
timeout: Duration,
) -> Result<MatchResult, PdqmError> {
use secrecy::ExposeSecret as _;
let body = body.expose_secret().as_bytes().to_vec();
let built = self
.http
.post(self.match_endpoint.clone())
.header(ACCEPT, FHIR_JSON)
.header(CONTENT_TYPE, FHIR_JSON)
.body(body)
.build()
.map_err(error::transport)?;
let response = self.send(built, timeout).await?;
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_match(status, media.as_deref(), &body)
}
pub async fn search(
&self,
query: &PatientQuery,
#[cfg_attr(
not(feature = "balp"),
expect(
unused_variables,
reason = "only an audited client names whom it acted for"
)
)]
on_behalf: &OnBehalfOf,
timeout: Duration,
) -> Result<SearchResult, PdqmError> {
#[cfg(feature = "balp")]
let deadline = ihe_atna::balp::recording::deadline(timeout);
let result = self.post(query, timeout).await;
#[cfg(feature = "balp")]
let result = self
.audit(
|| {
secrecy::SecretString::from(format!(
"POST {}\nContent-Type: {}\n\n{}",
secrecy::ExposeSecret::expose_secret(&ihe_atna::balp::request_text(
&self.endpoint
)),
request::FORM,
query.form()
))
},
on_behalf,
result,
deadline,
)
.await;
result
}
async fn post(
&self,
query: &PatientQuery,
timeout: Duration,
) -> Result<SearchResult, PdqmError> {
let built = self
.http
.post(self.endpoint.clone())
.header(ACCEPT, FHIR_JSON)
.header(CONTENT_TYPE, request::FORM)
.body(query.form())
.build()
.map_err(error::transport)?;
let response = self.send(built, timeout).await?;
self.answer(response, query.names_domain()).await
}
pub async fn next_page(
&self,
page: &Page,
#[cfg_attr(
not(feature = "balp"),
expect(
unused_variables,
reason = "only an audited client names whom it acted for"
)
)]
on_behalf: &OnBehalfOf,
timeout: Duration,
) -> Result<SearchResult, PdqmError> {
if page.url().origin() != self.endpoint.origin() {
return Err(PdqmError::ForeignPage);
}
#[cfg(feature = "balp")]
let deadline = ihe_atna::balp::recording::deadline(timeout);
let result = self.get(page, timeout).await;
#[cfg(feature = "balp")]
let result = self
.audit(
|| ihe_atna::balp::request_text(page.url()),
on_behalf,
result,
deadline,
)
.await;
result
}
async fn get(&self, page: &Page, timeout: Duration) -> Result<SearchResult, PdqmError> {
let built = self
.http
.get(page.url().clone())
.header(ACCEPT, FHIR_JSON)
.build()
.map_err(error::transport)?;
let response = self.send(built, timeout).await?;
self.answer(response, false).await
}
async fn send(
&self,
request: reqwest::Request,
timeout: Duration,
) -> Result<reqwest::Response, PdqmError> {
authorizer::send(&self.http, self.authorizer.as_deref(), request, timeout)
.await
.map_err(error::unsent)
}
async fn answer(
&self,
response: reqwest::Response,
names_domain: bool,
) -> Result<SearchResult, PdqmError> {
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, &self.base, names_domain)
}
}
impl fmt::Debug for PdqmClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PdqmClient")
.field("base", &RedactedUrl(self.base.as_str()))
.field("endpoint", &RedactedUrl(self.endpoint.as_str()))
.field("authorizer", &self.authorizer.is_some())
.finish_non_exhaustive()
}
}