Skip to content

Commit

Permalink
feat: Implement PATCH
Browse files Browse the repository at this point in the history
  • Loading branch information
Flix committed Sep 10, 2023
1 parent fd358b8 commit 403b2cb
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 32 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This is a [FHIR](https://www.hl7.org/fhir/) library in its early stages. The mod
- [x] Paging
- [x] Batch operations / Transactions
- [x] Operations
- [ ] Patch
- [x] Patch
- [ ] GraphQL
- [ ] FHIRpath implementation
- [ ] Resource validation using FHIRpath and regular expressions
Expand Down
67 changes: 42 additions & 25 deletions crates/fhir-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
mod error;
mod misc;
mod paging;
mod patch;
mod request;
mod search;
mod transaction;
Expand Down Expand Up @@ -42,7 +43,7 @@ pub use self::{
},
write::ResourceWrite,
};
use self::{paging::Paged, transaction::BatchTransaction};
use self::{paging::Paged, patch::Patch, transaction::BatchTransaction};

/// User agent of this client.
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
Expand Down Expand Up @@ -181,6 +182,30 @@ impl Client {
Ok(resource)
}

/// Create a new FHIR resource on the FHIR server. Returns the resource ID
/// and version ID.
pub async fn create<R: NamedResource + Serialize + Send + Sync>(
&self,
resource: &R,
) -> Result<(String, Option<String>), Error> {
let url = self.url(&[R::TYPE.as_str()]);
let request = self
.0
.client
.post(url)
.header(header::CONTENT_TYPE, HeaderValue::from_static(MIME_TYPE))
.json(resource);

let response = self.request_settings().make_request(request).await?;
if response.status().is_success() {
let (id, version_id) = misc::parse_location(response.headers())?;
let version_id = version_id.or(misc::parse_etag(response.headers()).ok());
Ok((id, version_id))
} else {
Err(Error::from_response(response).await)
}
}

/// Update a FHIR resource (or create it if it did not
/// exist). If conditional update is selected, the resource is only updated
/// if the version ID matches the expectations.
Expand All @@ -192,7 +217,12 @@ impl Client {
let id = resource.id().as_ref().ok_or(Error::MissingId)?;

let url = self.url(&[R::TYPE.as_str(), id]);
let mut request = self.0.client.put(url).json(resource);
let mut request = self
.0
.client
.put(url)
.header(header::CONTENT_TYPE, HeaderValue::from_static(MIME_TYPE))
.json(resource);
if conditional {
let version_id = resource
.meta()
Expand All @@ -214,23 +244,10 @@ impl Client {
}
}

/// Create a new FHIR resource on the FHIR server. Returns the resource ID
/// and version ID.
pub async fn create<R: NamedResource + Serialize + Send + Sync>(
&self,
resource: &R,
) -> Result<(String, Option<String>), Error> {
let url = self.url(&[R::TYPE.as_str()]);
let request = self.0.client.post(url).json(resource);

let response = self.request_settings().make_request(request).await?;
if response.status().is_success() {
let (id, version_id) = misc::parse_location(response.headers())?;
let version_id = version_id.or(misc::parse_etag(response.headers()).ok());
Ok((id, version_id))
} else {
Err(Error::from_response(response).await)
}
/// Begin building a patch request for a FHIR resource on the server via the
/// `FHIRPath Patch` method.
pub fn patch<'a>(&self, resource_type: ResourceType, id: &'a str) -> Patch<'a> {
Patch::new(self.clone(), resource_type, id)
}

/// Delete a FHIR resource on the server.
Expand Down Expand Up @@ -284,13 +301,11 @@ impl Client {
}

/// Start building a new batch request.
#[must_use]
pub fn batch(&self) -> BatchTransaction {
BatchTransaction::new(self.clone(), false)
}

/// Start building a new transaction request.
#[must_use]
pub fn transaction(&self) -> BatchTransaction {
BatchTransaction::new(self.clone(), true)
}
Expand Down Expand Up @@ -357,7 +372,12 @@ impl Client {
.build();

let url = self.url(&["Patient", "$match"]);
let request = self.0.client.post(url).json(&parameters);
let request = self
.0
.client
.post(url)
.header(header::CONTENT_TYPE, HeaderValue::from_static(MIME_TYPE))
.json(&parameters);

let response = self.request_settings().make_request(request).await?;
if response.status().is_success() {
Expand All @@ -368,6 +388,3 @@ impl Client {
}
}
}

#[cfg(test)]
mod tests;
213 changes: 213 additions & 0 deletions crates/fhir-sdk/src/client/patch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//! Patch request building.
use model::resources::{Parameters, ParametersParameter, ParametersParameterValue, ResourceType};
use reqwest::header::{self, HeaderValue};

use super::{model, Client, Error, MIME_TYPE};

/// Builder for a PATCH request for a FHIR resource.
#[derive(Debug, Clone)]
#[must_use = "You probably want to send the PATCH request"]
pub struct Patch<'a> {
/// FHIR client.
client: Client,
/// Resource type to apply the patch to.
resource_type: ResourceType,
/// Resource ID to apply the path to.
id: &'a str,
/// Operations to apply.
operations: Vec<Option<ParametersParameter>>,
}

impl<'a> Patch<'a> {
/// Start building a new Patch request.
pub fn new(client: Client, resource_type: ResourceType, id: &'a str) -> Self {
Self { client, resource_type, id, operations: Vec::new() }
}

/// Add an `add` operation to the list of operations. The value must have
/// the `name` field set to `value` and then either set a `value[x]` or
/// `part`.
pub fn add(
mut self,
path: impl Into<String>,
name: impl Into<String>,
value: ParametersParameter,
) -> Self {
let parameter = ParametersParameter::builder()
.name("operation".to_owned())
.part(vec![
Some(
ParametersParameter::builder()
.name("type".to_owned())
.value(ParametersParameterValue::Code("add".to_owned()))
.build(),
),
Some(
ParametersParameter::builder()
.name("path".to_owned())
.value(ParametersParameterValue::String(path.into()))
.build(),
),
Some(
ParametersParameter::builder()
.name("name".to_owned())
.value(ParametersParameterValue::String(name.into()))
.build(),
),
Some(value),
])
.build();

self.operations.push(Some(parameter));
self
}

/// Add an `insert` operation to the list of operations. The value must have
/// the `name` field set to `value` and then either set a `value[x]` or
/// `part`.
pub fn insert(
mut self,
path: impl Into<String>,
value: ParametersParameter,
index: i32,
) -> Self {
let parameter = ParametersParameter::builder()
.name("operation".to_owned())
.part(vec![
Some(
ParametersParameter::builder()
.name("type".to_owned())
.value(ParametersParameterValue::Code("insert".to_owned()))
.build(),
),
Some(
ParametersParameter::builder()
.name("path".to_owned())
.value(ParametersParameterValue::String(path.into()))
.build(),
),
Some(
ParametersParameter::builder()
.name("index".to_owned())
.value(ParametersParameterValue::Integer(index))
.build(),
),
Some(value),
])
.build();

self.operations.push(Some(parameter));
self
}

/// Add an `delete` operation to the list of operations.
pub fn delete(mut self, path: impl Into<String>) -> Self {
let parameter = ParametersParameter::builder()
.name("operation".to_owned())
.part(vec![
Some(
ParametersParameter::builder()
.name("type".to_owned())
.value(ParametersParameterValue::Code("delete".to_owned()))
.build(),
),
Some(
ParametersParameter::builder()
.name("path".to_owned())
.value(ParametersParameterValue::String(path.into()))
.build(),
),
])
.build();

self.operations.push(Some(parameter));
self
}

/// Add an `replace` operation to the list of operations. The value must
/// have the `name` field set to `value` and then either set a `value[x]` or
/// `part`.
pub fn replace(mut self, path: impl Into<String>, value: ParametersParameter) -> Self {
let parameter = ParametersParameter::builder()
.name("operation".to_owned())
.part(vec![
Some(
ParametersParameter::builder()
.name("type".to_owned())
.value(ParametersParameterValue::Code("replace".to_owned()))
.build(),
),
Some(
ParametersParameter::builder()
.name("path".to_owned())
.value(ParametersParameterValue::String(path.into()))
.build(),
),
Some(value),
])
.build();

self.operations.push(Some(parameter));
self
}

/// Add an `move` operation to the list of operations. The value must
/// have the `name` field set to `value` and then either set a `value[x]` or
/// `part`.
pub fn r#move(mut self, path: impl Into<String>, source: i32, destination: i32) -> Self {
let parameter = ParametersParameter::builder()
.name("operation".to_owned())
.part(vec![
Some(
ParametersParameter::builder()
.name("type".to_owned())
.value(ParametersParameterValue::Code("move".to_owned()))
.build(),
),
Some(
ParametersParameter::builder()
.name("path".to_owned())
.value(ParametersParameterValue::String(path.into()))
.build(),
),
Some(
ParametersParameter::builder()
.name("source".to_owned())
.value(ParametersParameterValue::Integer(source))
.build(),
),
Some(
ParametersParameter::builder()
.name("destination".to_owned())
.value(ParametersParameterValue::Integer(destination))
.build(),
),
])
.build();

self.operations.push(Some(parameter));
self
}

/// Patch the resource on the FHIR server.
pub async fn send(self) -> Result<(), Error> {
let parameters = Parameters::builder().parameter(self.operations).build();

let url = self.client.url(&[self.resource_type.as_str(), self.id]);
let request = self
.client
.0
.client
.patch(url)
.header(header::CONTENT_TYPE, HeaderValue::from_static(MIME_TYPE))
.json(&parameters);

let response = self.client.request_settings().make_request(request).await?;
if response.status().is_success() {
Ok(())
} else {
Err(Error::from_response(response).await)
}
}
}
1 change: 0 additions & 1 deletion crates/fhir-sdk/src/client/tests.rs

This file was deleted.

12 changes: 10 additions & 2 deletions crates/fhir-sdk/src/client/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ use model::{
codes::{BundleType, HTTPVerb},
resources::{Bundle, BundleEntry, BundleEntryRequest, Resource, ResourceType},
};
use reqwest::header::{self, HeaderValue};
use uuid::Uuid;

use super::{model, Client, Error};
use super::{model, Client, Error, MIME_TYPE};

/// A batch/transaction request builder.
#[derive(Debug, Clone)]
#[must_use = "You probably want to send the batch/transaction"]
pub struct BatchTransaction {
/// The FHIR client.
client: Client,
Expand Down Expand Up @@ -128,7 +130,13 @@ impl BatchTransaction {
.build();

let url = self.client.url(&[]);
let request = self.client.0.client.post(url).json(&bundle);
let request = self
.client
.0
.client
.post(url)
.header(header::CONTENT_TYPE, HeaderValue::from_static(MIME_TYPE))
.json(&bundle);

let response = self.client.request_settings().make_request(request).await?;
if response.status().is_success() {
Expand Down
Loading

0 comments on commit 403b2cb

Please sign in to comment.