#[cfg(feature = "balp")]
pub mod audit;
pub mod error;
pub mod feed;
mod message;
pub mod subscription;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use http::StatusCode;
use http::header::{ACCEPT, CONTENT_TYPE, LOCATION};
use url::Url;
use crate::authorizer::{self, Authorizer};
use crate::outcome;
use error::{InvalidInput, SubscribeError, SubscriptionMalformation};
use ihe_atna::redact::RedactedUrl;
use subscription::{FHIR_JSON, Subscribed, SubscriptionRequest, SubscriptionStatus};
const LIMIT: usize = 1 << 20;
#[derive(Clone)]
pub struct PmirSubscriber {
endpoint: Url,
http: reqwest::Client,
authorizer: Option<Arc<dyn Authorizer>>,
#[cfg(feature = "balp")]
audit: Option<Arc<dyn ihe_atna::balp::AuditRecorder>>,
}
impl PmirSubscriber {
pub fn new(base: Url, http: reqwest::Client) -> Result<Self, InvalidInput> {
let endpoint =
ihe_atna::fhir_base::under_base(base, "Subscription").ok_or(InvalidInput::Base)?;
Ok(Self {
endpoint,
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
}
async fn send(
&self,
request: reqwest::Request,
timeout: Duration,
) -> Result<reqwest::Response, SubscribeError> {
authorizer::send(&self.http, self.authorizer.as_deref(), request, timeout)
.await
.map_err(error::unsent)
}
#[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,
exchange: impl FnOnce(&Result<T, SubscribeError>) -> ihe_atna::balp::Exchange,
result: Result<T, SubscribeError>,
) -> Result<T, SubscribeError> {
if let Some(recorder) = &self.audit {
recorder
.record(exchange(&result))
.await
.map_err(SubscribeError::Audit)?;
}
result
}
#[must_use]
pub fn endpoint(&self) -> &Url {
&self.endpoint
}
pub async fn find(
&self,
request: &SubscriptionRequest,
timeout: Duration,
) -> Result<subscription::Search, SubscribeError> {
let mut url = self.endpoint.clone();
url.query_pairs_mut()
.append_pair("url", request.endpoint().as_str());
let result = self.find_at(&url, request, timeout).await;
#[cfg(feature = "balp")]
let result = self
.audit(|result| audit::search(&self.endpoint, &url, result), result)
.await;
result
}
async fn find_at(
&self,
url: &Url,
request: &SubscriptionRequest,
timeout: Duration,
) -> Result<subscription::Search, SubscribeError> {
let built = self
.http
.get(url.clone())
.header(ACCEPT, FHIR_JSON)
.build()
.map_err(error::transport)?;
let response = self.send(built, timeout).await?;
let status = response.status();
let media = media(&response);
let body = read_body(response).await?;
if matches!(status, StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND) {
return Ok(subscription::Search::Unsupported);
}
if status != StatusCode::OK {
return Err(rejected(status, media.as_deref(), &body));
}
Ok(subscription::Search::Found(subscription::listed(
media.as_deref(),
&body,
request,
&self.endpoint,
)?))
}
pub async fn subscribe(
&self,
request: &SubscriptionRequest,
timeout: Duration,
) -> Result<Subscribed, SubscribeError> {
let result = self.create(request, timeout).await;
#[cfg(feature = "balp")]
let result = self
.audit(
|result| {
let reference = result.as_ref().ok().and_then(|subscribed| {
audit::subscription_reference(subscribed.location())
});
audit::subscription(
audit::subscription_create(),
&self.endpoint,
reference,
Some(request.criteria().text()),
result,
)
},
result,
)
.await;
result
}
async fn create(
&self,
request: &SubscriptionRequest,
timeout: Duration,
) -> Result<Subscribed, SubscribeError> {
let body = serde_json::to_vec(&request.resource()).map_err(SubscribeError::Unwritable)?;
let built = self
.http
.post(self.endpoint.clone())
.header(CONTENT_TYPE, FHIR_JSON)
.header(ACCEPT, FHIR_JSON)
.body(body)
.build()
.map_err(error::transport)?;
let response = self.send(built, timeout).await?;
let status = response.status();
let location = response
.headers()
.get(LOCATION)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let media = media(&response);
let body = read_body(response).await?;
if status != StatusCode::CREATED {
return Err(rejected(status, media.as_deref(), &body));
}
let location = location.ok_or(SubscriptionMalformation::NoLocation)?;
Ok(Subscribed::at(&self.endpoint, &location)?)
}
pub async fn status(
&self,
subscribed: &Subscribed,
timeout: Duration,
) -> Result<SubscriptionStatus, SubscribeError> {
let result = self.read(subscribed, timeout).await;
#[cfg(feature = "balp")]
let result = self
.audit(
|result| {
audit::subscription(
audit::subscription_read(),
&self.endpoint,
audit::subscription_reference(subscribed.location()),
None,
result,
)
},
result,
)
.await;
result
}
async fn read(
&self,
subscribed: &Subscribed,
timeout: Duration,
) -> Result<SubscriptionStatus, SubscribeError> {
let built = self
.http
.get(subscribed.location().clone())
.header(ACCEPT, FHIR_JSON)
.build()
.map_err(error::transport)?;
let response = self.send(built, timeout).await?;
let status = response.status();
let media = media(&response);
let body = read_body(response).await?;
if status != StatusCode::OK {
return Err(rejected(status, media.as_deref(), &body));
}
Ok(subscription::status(media.as_deref(), &body)?)
}
pub async fn unsubscribe(
&self,
subscribed: &Subscribed,
timeout: Duration,
) -> Result<(), SubscribeError> {
let result = self.delete(subscribed, timeout).await;
#[cfg(feature = "balp")]
let result = self
.audit(
|result| {
audit::subscription(
audit::subscription_delete(),
&self.endpoint,
audit::subscription_reference(subscribed.location()),
None,
result,
)
},
result,
)
.await;
result
}
async fn delete(
&self,
subscribed: &Subscribed,
timeout: Duration,
) -> Result<(), SubscribeError> {
let built = self
.http
.delete(subscribed.location().clone())
.header(ACCEPT, FHIR_JSON)
.build()
.map_err(error::transport)?;
let response = self.send(built, timeout).await?;
let status = response.status();
let media = media(&response);
let body = read_body(response).await?;
if matches!(
status,
StatusCode::OK
| StatusCode::ACCEPTED
| StatusCode::NO_CONTENT
| StatusCode::NOT_FOUND
| StatusCode::GONE
) {
return Ok(());
}
Err(rejected(status, media.as_deref(), &body))
}
}
impl fmt::Debug for PmirSubscriber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PmirSubscriber")
.field("endpoint", &RedactedUrl(self.endpoint.as_str()))
.field("authorizer", &self.authorizer.is_some())
.finish_non_exhaustive()
}
}
fn media(response: &reqwest::Response) -> Option<String> {
response
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
}
fn rejected(status: StatusCode, media: Option<&str>, body: &[u8]) -> SubscribeError {
SubscribeError::Rejected {
status,
issues: outcome::issues(media, body),
}
}
async fn read_body(mut response: reqwest::Response) -> Result<Vec<u8>, SubscribeError> {
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(error::transport)? {
if body.len().saturating_add(chunk.len()) > LIMIT {
return Err(SubscriptionMalformation::TooLarge { limit: LIMIT }.into());
}
body.extend_from_slice(&chunk);
}
Ok(body)
}