pub mod error;
mod request;
mod response;
use std::collections::BTreeSet;
use std::fmt;
use std::sync::Arc;
use std::time::{Duration, Instant};
use http::header::{ACCEPT, CONTENT_TYPE};
use http::{HeaderMap, Method};
use url::Url;
use crate::authorizer::{Authorizer, Retry};
use crate::identification::{PseudoBsn, Ura};
use error::{InvalidInput, Malformation, NviError};
pub const LOINC_SYSTEM: &str = "http://loinc.org";
pub const PATIENT_DATA_TYPE: &str = "55188-7";
const FHIR_JSON: &str = "application/fhir+json";
const PAGES: usize = 32;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Localization {
custodians: BTreeSet<Ura>,
}
impl Localization {
#[must_use]
pub fn custodians(&self) -> &BTreeSet<Ura> {
&self.custodians
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.custodians.is_empty()
}
}
#[derive(Clone)]
pub struct NviClient {
endpoint: Url,
http: reqwest::Client,
authorizer: Option<Arc<dyn Authorizer>>,
}
impl NviClient {
pub fn new(base: Url, http: reqwest::Client) -> Result<Self, InvalidInput> {
Ok(Self {
endpoint: request::endpoint(base)?,
http,
authorizer: None,
})
}
#[must_use]
pub fn with_authorizer(mut self, authorizer: Arc<dyn Authorizer>) -> Self {
self.authorizer = Some(authorizer);
self
}
#[must_use]
pub fn endpoint(&self) -> &Url {
&self.endpoint
}
pub async fn localize(
&self,
patient: &PseudoBsn,
timeout: Duration,
) -> Result<Localization, NviError> {
let deadline = Instant::now().checked_add(timeout);
let mut url = request::query(&self.endpoint, patient);
let mut custodians = BTreeSet::new();
for _page in 0..PAGES {
let response = self.send(&url, deadline).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?;
let page = response::page(status, media.as_deref(), &body, patient)?;
custodians.extend(page.custodians);
match page.next {
None => return Ok(Localization { custodians }),
Some(next) => url = request::next(&self.endpoint, &next)?,
}
}
Err(Malformation::TooManyPages { limit: PAGES }.into())
}
async fn send(
&self,
url: &Url,
deadline: Option<Instant>,
) -> Result<reqwest::Response, NviError> {
let target = request::target(url);
let mut resent = false;
loop {
let headers = match &self.authorizer {
Some(authorizer) => authorizer
.authorize(&Method::GET, &target)
.await
.map_err(NviError::Unauthenticated)?,
None => HeaderMap::new(),
};
let remaining = deadline
.and_then(|deadline| deadline.checked_duration_since(Instant::now()))
.filter(|remaining| !remaining.is_zero())
.ok_or(NviError::Timeout)?;
let response = self
.http
.get(url.clone())
.headers(headers)
.header(ACCEPT, FHIR_JSON)
.timeout(remaining)
.send()
.await
.map_err(error::transport)?;
let Some(authorizer) = &self.authorizer else {
return Ok(response);
};
let retry = authorizer.answered(&target, response.status(), response.headers());
if resent || retry == Retry::Done {
return Ok(response);
}
resent = true;
}
}
}
impl fmt::Debug for NviClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NviClient")
.field("endpoint", &request::Redacted(&self.endpoint))
.field("authorizer", &self.authorizer.is_some())
.finish_non_exhaustive()
}
}