fhir_sdk/client/fhir/
crud.rs

1//! FHIR CRUD API interactions.
2
3use fhir_model::{ParsedReference, WrongResourceType};
4use reqwest::{
5	StatusCode, Url,
6	header::{self, HeaderValue},
7};
8use serde::{Serialize, de::DeserializeOwned};
9
10use super::{
11	Client, Error, SearchParameters, misc,
12	paging::Page,
13	patch::{PatchViaFhir, PatchViaJson},
14	transaction::BatchTransaction,
15};
16use crate::{
17	client::misc::make_uuid_header_value,
18	extensions::{AnyResource, GenericResource, ReferenceExt},
19	version::FhirVersion,
20};
21
22impl<V: FhirVersion> Client<V>
23where
24	(StatusCode, V::OperationOutcome): Into<Error>,
25{
26	/// Get the server's capabilities. Fails if the respective FHIR version is
27	/// not supported at all.
28	pub async fn capabilities(&self) -> Result<V::CapabilityStatement, Error> {
29		let url = self.url(&["metadata"]);
30		let request = self.0.client.get(url).header(header::ACCEPT, V::MIME_TYPE);
31
32		let response = self.run_request(request).await?;
33		if response.status().is_success() {
34			let capability_statement: V::CapabilityStatement = response.json().await?;
35			Ok(capability_statement)
36		} else {
37			Err(Error::from_response::<V>(response).await)
38		}
39	}
40
41	/// Read any resource from any URL.
42	pub(crate) async fn read_generic<R: DeserializeOwned>(
43		&self,
44		url: Url,
45		correlation_id: Option<HeaderValue>,
46	) -> Result<Option<R>, Error> {
47		let mut request = self.0.client.get(url).header(header::ACCEPT, V::MIME_TYPE);
48		if let Some(correlation_id) = correlation_id {
49			request = request.header("X-Correlation-Id", correlation_id);
50		}
51
52		let response = self.run_request(request).await?;
53		if response.status().is_success() {
54			let resource: R = response.json().await?;
55			Ok(Some(resource))
56		} else if [StatusCode::NOT_FOUND, StatusCode::GONE].contains(&response.status()) {
57			Ok(None)
58		} else {
59			Err(Error::from_response::<V>(response).await)
60		}
61	}
62
63	/// Read the current version of a specific FHIR resource.
64	pub async fn read<R: AnyResource<V> + DeserializeOwned>(
65		&self,
66		id: &str,
67	) -> Result<Option<R>, Error> {
68		let url = self.url(&[R::TYPE_STR, id]);
69		self.read_generic(url, None).await
70	}
71
72	/// Read a specific version of a specific FHIR resource.
73	pub async fn read_version<R: AnyResource<V> + DeserializeOwned>(
74		&self,
75		id: &str,
76		version_id: &str,
77	) -> Result<Option<R>, Error> {
78		let url = self.url(&[R::TYPE_STR, id, "_history", version_id]);
79		self.read_generic(url, None).await
80	}
81
82	/// Read the resource that is targeted in the reference.
83	pub async fn read_referenced(&self, reference: &V::Reference) -> Result<V::Resource, Error> {
84		let parsed_reference = reference.parse().ok_or(Error::MissingReference)?;
85		let url = match parsed_reference {
86			ParsedReference::Local { .. } => return Err(Error::LocalReference),
87			ParsedReference::Relative { resource_type, id, version_id } => {
88				if let Some(version_id) = version_id {
89					self.url(&[resource_type, id, "_history", version_id])
90				} else {
91					self.url(&[resource_type, id])
92				}
93			}
94			ParsedReference::Absolute { url, .. } => {
95				url.parse().map_err(|_| Error::UrlParse(url.to_owned()))?
96			}
97		};
98
99		let resource: V::Resource = self
100			.read_generic(url.clone(), None)
101			.await?
102			.ok_or_else(|| Error::ResourceNotFound(url.to_string()))?;
103		if let Some(resource_type) = reference.r#type() {
104			if resource.resource_type_str() != resource_type {
105				return Err(Error::WrongResourceType(
106					resource.resource_type_str().to_owned(),
107					resource_type.to_owned(),
108				));
109			}
110		}
111
112		Ok(resource)
113	}
114
115	/// Retrieve the history of the specified resource type or a specific resource.
116	pub async fn history<R>(&self, id: Option<&str>) -> Result<Page<V, R>, Error>
117	where
118		R: AnyResource<V> + TryFrom<V::Resource, Error = WrongResourceType> + 'static,
119		for<'a> &'a R: TryFrom<&'a V::Resource>,
120	{
121		let correlation_id = make_uuid_header_value();
122
123		let url = {
124			if let Some(id) = id {
125				self.url(&[R::TYPE_STR, id, "_history"])
126			} else {
127				self.url(&[R::TYPE_STR, "_history"])
128			}
129		};
130		let request = self
131			.0
132			.client
133			.get(url)
134			.header(header::ACCEPT, V::MIME_TYPE)
135			.header("X-Correlation-Id", correlation_id.clone());
136
137		let response = self.run_request(request).await?;
138		if response.status().is_success() {
139			let bundle: V::Bundle = response.json().await?;
140			Ok(Page::new(self.clone(), bundle, correlation_id))
141		} else {
142			Err(Error::from_response::<V>(response).await)
143		}
144	}
145
146	/// Inner function to create any resource for any resource type.
147	pub(crate) async fn create_generic<R: Serialize + Send + Sync>(
148		&self,
149		resource_type: &str,
150		resource: &R,
151	) -> Result<(String, Option<String>), Error> {
152		let url = self.url(&[resource_type]);
153		let request = self
154			.0
155			.client
156			.post(url)
157			.header(header::ACCEPT, V::MIME_TYPE)
158			.header(header::CONTENT_TYPE, V::MIME_TYPE)
159			.json(resource);
160
161		let response = self.run_request(request).await?;
162		if response.status().is_success() {
163			let (id, version_id) = misc::parse_location(response.headers())?;
164			let version_id = version_id.or_else(|| misc::parse_etag(response.headers()).ok());
165			Ok((id, version_id))
166		} else {
167			Err(Error::from_response::<V>(response).await)
168		}
169	}
170
171	/// Create a new FHIR resource on the FHIR server. Returns the resource ID
172	/// and version ID.
173	pub async fn create<R: AnyResource<V> + Serialize + Send + Sync>(
174		&self,
175		resource: &R,
176	) -> Result<(String, Option<String>), Error> {
177		self.create_generic(R::TYPE_STR, resource).await
178	}
179
180	/// Inner function to update any resource for any resource type.
181	pub(crate) async fn update_generic<R: Serialize + Send + Sync>(
182		&self,
183		resource_type: &str,
184		id: &str,
185		resource: &R,
186		version_id: Option<&str>,
187	) -> Result<(bool, String), Error> {
188		let url = self.url(&[resource_type, id]);
189		let mut request = self
190			.0
191			.client
192			.put(url)
193			.header(header::ACCEPT, V::MIME_TYPE)
194			.header(header::CONTENT_TYPE, V::MIME_TYPE)
195			.json(resource);
196		if let Some(version_id) = version_id {
197			let if_match = HeaderValue::from_str(&format!("W/\"{version_id}\""))
198				.map_err(|_| Error::MissingVersionId)?;
199			request = request.header(header::IF_MATCH, if_match);
200		}
201
202		let response = self.run_request(request).await?;
203		if response.status().is_success() {
204			let created = response.status() == StatusCode::CREATED;
205			let version_id = misc::parse_etag(response.headers())?;
206			Ok((created, version_id))
207		} else {
208			Err(Error::from_response::<V>(response).await)
209		}
210	}
211
212	/// Update a FHIR resource (or create it if it did not
213	/// exist). If conditional update is selected, the resource is only updated
214	/// if the version ID matches the expectations.
215	pub async fn update<R: AnyResource<V> + Serialize + Send + Sync>(
216		&self,
217		resource: &R,
218		conditional: bool,
219	) -> Result<(bool, String), Error> {
220		let id = resource.id().ok_or(Error::MissingId)?;
221		let version_id = conditional
222			.then(|| resource.version_id().ok_or(Error::MissingVersionId))
223			.transpose()?;
224		self.update_generic(R::TYPE_STR, id, resource, version_id).await
225	}
226
227	/// Delete a FHIR resource on the server.
228	pub async fn delete(&self, resource_type: V::ResourceType, id: &str) -> Result<(), Error> {
229		let url = self.url(&[resource_type.as_ref(), id]);
230		let request = self.0.client.delete(url).header(header::ACCEPT, V::MIME_TYPE);
231
232		let response = self.run_request(request).await?;
233		if response.status().is_success() {
234			Ok(())
235		} else {
236			Err(Error::from_response::<V>(response).await)
237		}
238	}
239
240	/// Search for FHIR resources of any type given the query parameters.
241	pub async fn search_all(
242		&self,
243		queries: SearchParameters,
244	) -> Result<Page<V, V::Resource>, Error> {
245		// TODO: Use POST for long queries?
246
247		let correlation_id = make_uuid_header_value();
248
249		let url = self.url(&[]);
250		let request = self
251			.0
252			.client
253			.get(url)
254			.query(&queries.into_queries())
255			.header(header::ACCEPT, V::MIME_TYPE)
256			.header("X-Correlation-Id", correlation_id.clone());
257
258		let response = self.run_request(request).await?;
259		if response.status().is_success() {
260			let bundle: V::Bundle = response.json().await?;
261			Ok(Page::new(self.clone(), bundle, correlation_id))
262		} else {
263			Err(Error::from_response::<V>(response).await)
264		}
265	}
266
267	/// Search for FHIR resources of a given type given the query parameters.
268	pub async fn search<R>(&self, queries: SearchParameters) -> Result<Page<V, R>, Error>
269	where
270		R: AnyResource<V> + TryFrom<V::Resource, Error = WrongResourceType> + 'static,
271		for<'a> &'a R: TryFrom<&'a V::Resource>,
272	{
273		// TODO: Use POST for long queries?
274
275		let correlation_id = make_uuid_header_value();
276
277		let url = self.url(&[R::TYPE_STR]);
278		let request = self
279			.0
280			.client
281			.get(url)
282			.query(&queries.into_queries())
283			.header(header::ACCEPT, V::MIME_TYPE)
284			.header("X-Correlation-Id", correlation_id.clone());
285
286		let response = self.run_request(request).await?;
287		if response.status().is_success() {
288			let bundle: V::Bundle = response.json().await?;
289			Ok(Page::new(self.clone(), bundle, correlation_id))
290		} else {
291			Err(Error::from_response::<V>(response).await)
292		}
293	}
294
295	/// Search for FHIR resources via a custom request. This allows sending POST requests instead of
296	/// GET when necessary. You can construct the request yourself to any URL and send any data.
297	/// The endpoint is expected to send a FHIR-conform bundle.
298	///
299	/// You can specify the expected search entry type via the type parameter. This can be either
300	/// the generic resource or a specific resource.
301	///
302	/// Keep in mind that mismatching origins to the base URL are rejected if not explicitly allowed
303	/// via the flag in the builder ([ClientBuilder::allow_origin_mismatch]). Similarly, if the
304	/// server responds with a different major FHIR version than the client is configured for, the
305	/// response is rejected if not explicitly allowed via the flag in the builder
306	/// ([ClientBuilder::allow_version_mismatch]).
307	pub async fn search_custom<R, F>(&self, make_request: F) -> Result<Page<V, R>, Error>
308	where
309		R: TryFrom<V::Resource> + Send + Sync + 'static,
310		for<'a> &'a R: TryFrom<&'a V::Resource>,
311		F: FnOnce(&reqwest::Client) -> reqwest::RequestBuilder + Send,
312	{
313		let mut request_builder = (make_request)(&self.0.client);
314		let (client, request_result) = request_builder.build_split();
315		let mut request = request_result?;
316		let correlation_id = request
317			.headers_mut()
318			.entry("X-Correlation-Id")
319			.or_insert_with(make_uuid_header_value)
320			.clone();
321		request_builder = reqwest::RequestBuilder::from_parts(client, request);
322
323		let response = self.run_request(request_builder).await?;
324		if response.status().is_success() {
325			let bundle: V::Bundle = response.json().await?;
326			Ok(Page::new(self.clone(), bundle, correlation_id))
327		} else {
328			Err(Error::from_response::<V>(response).await)
329		}
330	}
331
332	/// Begin building a patch request for a FHIR resource on the server via the
333	/// `FHIRPath Patch` method.
334	pub fn patch_via_fhir<'a>(
335		&self,
336		resource_type: V::ResourceType,
337		id: &'a str,
338	) -> PatchViaFhir<'a, V> {
339		PatchViaFhir::new(self.clone(), resource_type, id)
340	}
341
342	/// Begin building a patch request for a FHIR resource on the server via the
343	/// [`JSON Patch`](https://datatracker.ietf.org/doc/html/rfc6902) method.
344	pub fn patch_via_json<'a>(
345		&self,
346		resource_type: V::ResourceType,
347		id: &'a str,
348	) -> PatchViaJson<'a, V> {
349		PatchViaJson::new(self.clone(), resource_type, id)
350	}
351
352	/// Start building a new batch request.
353	pub fn batch(&self) -> BatchTransaction<V> {
354		BatchTransaction::new(self.clone(), false)
355	}
356
357	/// Start building a new transaction request.
358	pub fn transaction(&self) -> BatchTransaction<V> {
359		BatchTransaction::new(self.clone(), true)
360	}
361}