use ::serde::Serialize;
use crate::release::Release;
const FHIR_JSON: &str = "application/fhir+json";
pub enum ReleaseClientError<R: Release> {
Http(reqwest::Error),
Outcome {
status: u16,
outcome: Box<R::OperationOutcome>,
},
Status {
status: u16,
body: String,
},
}
impl<R: Release> std::fmt::Debug for ReleaseClientError<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReleaseClientError::Http(e) => f.debug_tuple("Http").field(e).finish(),
ReleaseClientError::Outcome { status, outcome } => f
.debug_struct("Outcome")
.field("status", status)
.field("outcome", outcome)
.finish(),
ReleaseClientError::Status { status, body } => f
.debug_struct("Status")
.field("status", status)
.field("body", body)
.finish(),
}
}
}
impl<R: Release> std::fmt::Display for ReleaseClientError<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReleaseClientError::Http(e) => write!(f, "HTTP error: {e}"),
ReleaseClientError::Outcome { status, .. } => {
write!(f, "FHIR error status {status} (OperationOutcome)")
}
ReleaseClientError::Status { status, .. } => write!(f, "error status {status}"),
}
}
}
impl<R: Release> std::error::Error for ReleaseClientError<R> {}
impl<R: Release> From<reqwest::Error> for ReleaseClientError<R> {
fn from(e: reqwest::Error) -> Self {
ReleaseClientError::Http(e)
}
}
#[derive(Clone)]
pub struct ReleaseClient<R: Release> {
base_url: String,
http: reqwest::Client,
release: std::marker::PhantomData<R>,
}
impl<R: Release> std::fmt::Debug for ReleaseClient<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReleaseClient")
.field("base_url", &self.base_url)
.field("http", &self.http)
.field("release", &R::LABEL)
.finish()
}
}
impl<R: Release> ReleaseClient<R> {
#[must_use]
pub fn new(base_url: impl Into<String>) -> Self {
Self::with_http(base_url, reqwest::Client::new())
}
#[must_use]
pub fn with_http(base_url: impl Into<String>, http: reqwest::Client) -> Self {
let base_url = base_url.into().trim_end_matches('/').to_string();
Self { base_url, http, release: std::marker::PhantomData }
}
async fn send(
&self,
req: reqwest::RequestBuilder,
) -> Result<reqwest::Response, ReleaseClientError<R>> {
let resp = req.header(reqwest::header::ACCEPT, FHIR_JSON).send().await?;
if resp.status().is_success() {
return Ok(resp);
}
let status = resp.status().as_u16();
let body = resp.text().await?;
match ::serde_json::from_str::<R::OperationOutcome>(&body) {
Ok(outcome) => Err(ReleaseClientError::Outcome { status, outcome: Box::new(outcome) }),
Err(_) => Err(ReleaseClientError::Status { status, body }),
}
}
pub async fn read(
&self,
resource_type: &str,
id: &str,
) -> Result<R::Resource, ReleaseClientError<R>> {
let url = format!("{}/{}/{}", self.base_url, resource_type, id);
Ok(self.send(self.http.get(url)).await?.json().await?)
}
pub async fn vread(
&self,
resource_type: &str,
id: &str,
version_id: &str,
) -> Result<R::Resource, ReleaseClientError<R>> {
let url = format!("{}/{}/{}/_history/{}", self.base_url, resource_type, id, version_id);
Ok(self.send(self.http.get(url)).await?.json().await?)
}
pub async fn create<T: Serialize>(
&self,
resource_type: &str,
resource: &T,
) -> Result<R::Resource, ReleaseClientError<R>> {
let url = format!("{}/{}", self.base_url, resource_type);
Ok(self.send(self.http.post(url).json(resource)).await?.json().await?)
}
pub async fn update<T: Serialize>(
&self,
resource_type: &str,
id: &str,
resource: &T,
) -> Result<R::Resource, ReleaseClientError<R>> {
let url = format!("{}/{}/{}", self.base_url, resource_type, id);
Ok(self.send(self.http.put(url).json(resource)).await?.json().await?)
}
pub async fn delete(&self, resource_type: &str, id: &str) -> Result<(), ReleaseClientError<R>> {
let url = format!("{}/{}/{}", self.base_url, resource_type, id);
self.send(self.http.delete(url)).await?;
Ok(())
}
pub async fn search(
&self,
resource_type: &str,
params: &[(&str, &str)],
) -> Result<R::Bundle, ReleaseClientError<R>> {
let url = format!("{}/{}", self.base_url, resource_type);
Ok(self.send(self.http.get(url).query(params)).await?.json().await?)
}
pub async fn capabilities(&self) -> Result<R::CapabilityStatement, ReleaseClientError<R>> {
let url = format!("{}/metadata", self.base_url);
Ok(self.send(self.http.get(url)).await?.json().await?)
}
}
#[cfg(feature = "r5")]
pub type Client = ReleaseClient<crate::r5::R5>;
#[cfg(feature = "r5")]
pub type ClientError = ReleaseClientError<crate::r5::R5>;
#[cfg(test)]
#[cfg(feature = "r5")]
mod tests {
use super::*;
use crate::r5::resources::Resource;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn read_returns_resource() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/Patient/pat-1"))
.respond_with(ResponseTemplate::new(200).set_body_json(::serde_json::json!({
"resourceType": "Patient", "id": "pat-1", "active": true
})))
.mount(&server)
.await;
let client = Client::new(server.uri());
let resource = client.read("Patient", "pat-1").await.unwrap();
match resource {
Resource::Patient(p) => assert_eq!(p.id.unwrap().0, "pat-1"),
other => panic!("expected Patient, got {other:?}"),
}
}
#[tokio::test]
async fn search_returns_bundle() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/Patient"))
.respond_with(ResponseTemplate::new(200).set_body_json(::serde_json::json!({
"resourceType": "Bundle", "type": "searchset",
"entry": [{ "resource": { "resourceType": "Patient", "id": "a" } }]
})))
.mount(&server)
.await;
let client = Client::new(server.uri());
let bundle = client.search("Patient", &[("name", "chalmers")]).await.unwrap();
assert_eq!(bundle.iter_resources().count(), 1);
}
#[tokio::test]
async fn error_status_parses_operation_outcome() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/Patient/missing"))
.respond_with(ResponseTemplate::new(404).set_body_json(::serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{ "severity": "error", "code": "not-found",
"diagnostics": "no such Patient" }]
})))
.mount(&server)
.await;
let client = Client::new(server.uri());
let err = client.read("Patient", "missing").await.unwrap_err();
match err {
ClientError::Outcome { status, outcome } => {
assert_eq!(status, 404);
assert_eq!(outcome.issue.len(), 1);
}
other => panic!("expected Outcome, got {other:?}"),
}
}
}