Skip to main content

fhir_sdk/client/fhir/
search_params.rs

1//! Client search implementation.
2
3use ::std::marker::PhantomData;
4
5use super::super::{misc::escape_search_value, search::SearchParameter};
6use crate::version::FhirVersion;
7
8/// Number search.
9///
10/// Only implements most common functionality. Refer to adding raw queries when
11/// this does not suffice.
12#[derive(Debug, Clone)]
13pub struct NumberSearch<'a, V> {
14	/// Name of the field.
15	name: &'a str,
16	/// Values encoded as string already (will be comma-separated for
17	/// OR-joining).
18	values: Vec<String>,
19	/// Use V.
20	_version: PhantomData<V>,
21}
22
23impl<'a, V: FhirVersion> NumberSearch<'a, V> {
24	/// Start with empty values and add values one at a time.
25	#[must_use]
26	pub const fn new(name: &'a str) -> Self {
27		Self { name, values: Vec::new(), _version: PhantomData }
28	}
29
30	/// Add a value to the number search.
31	#[must_use]
32	pub fn or<T: ToString>(mut self, comparator: Option<V::SearchComparator>, value: T) -> Self {
33		let value = escape_search_value(&value.to_string());
34		if let Some(comparator) = comparator {
35			self.values.push(format!("{comparator}{value}"));
36		} else {
37			self.values.push(value);
38		}
39		self
40	}
41}
42
43impl<V> SearchParameter for NumberSearch<'_, V> {
44	fn into_query(self) -> (String, String) {
45		(self.name.to_owned(), self.values.join(","))
46	}
47}
48
49/// Date search.
50///
51/// Only implements most common functionality. Refer to adding raw queries when
52/// this does not suffice.
53#[derive(Debug, Clone, Copy)]
54pub struct DateSearch<'a, V: FhirVersion> {
55	/// Name of the field.
56	pub name: &'a str,
57	/// Search comparator to compare the date.
58	pub comparator: Option<V::SearchComparator>,
59	/// Value to search for.
60	pub value: &'a str,
61}
62
63impl<V: FhirVersion> SearchParameter for DateSearch<'_, V> {
64	fn into_query(self) -> (String, String) {
65		if let Some(comparator) = self.comparator {
66			(
67				self.name.to_owned(),
68				format!("{}{}", comparator.as_ref(), escape_search_value(self.value)),
69			)
70		} else {
71			(self.name.to_owned(), escape_search_value(self.value))
72		}
73	}
74}
75
76/// String search.
77///
78/// Only implements most common functionality. Refer to adding raw queries when
79/// this does not suffice.
80#[derive(Debug, Clone, Copy)]
81pub enum StringSearch<'a> {
82	/// Standard string search. This is a case-insensitive starts-with search.
83	Standard {
84		/// Name of the field to search on.
85		name: &'a str,
86		/// Value of the field.
87		value: &'a str,
88	},
89	/// Search a string that contains the given value.
90	Contains {
91		/// Name of the field to search on.
92		name: &'a str,
93		/// Value of the field.
94		value: &'a str,
95	},
96	/// Search a string that matches exactly the value, including casing and
97	/// accents.
98	Exact {
99		/// Name of the field to search on.
100		name: &'a str,
101		/// Value of the field.
102		value: &'a str,
103	},
104}
105
106impl SearchParameter for StringSearch<'_> {
107	fn into_query(self) -> (String, String) {
108		let (name, modifier, value) = match self {
109			Self::Standard { name, value } => (name, "", value),
110			Self::Contains { name, value } => (name, ":contains", value),
111			Self::Exact { name, value } => (name, ":exact", value),
112		};
113		(format!("{name}{modifier}"), escape_search_value(value))
114	}
115}
116
117/// Token search, e.g. in `CodeableConcept`s or `identifier`s.
118///
119/// Only implements most common functionality. Refer to adding raw queries when
120/// this does not suffice.
121#[derive(Debug, Clone, Copy)]
122pub enum TokenSearch<'a> {
123	/// Standard token search (or `not` search).
124	Standard {
125		/// Name of the field to search on.
126		name: &'a str,
127		/// System for the value to search on. If this is given as empty string,
128		/// the system must not be present.
129		system: Option<&'a str>,
130		/// Value of the code to search on.
131		code: Option<&'a str>,
132		/// Whether to switch to the `not` modifier.
133		not: bool,
134	},
135	/// Token search with the `of-type` modifier. Only possible on
136	/// `identifier`s.
137	OfType {
138		/// System of type to search on.
139		type_system: Option<&'a str>,
140		/// Code of the type to search on.
141		type_code: Option<&'a str>,
142		/// Value to search on with the given type.
143		value: Option<&'a str>,
144	},
145	/// Token search whether the value is `in` or `not-in` a given `ValueSet`.
146	In {
147		/// Name of the field to search on.
148		name: &'a str,
149		/// `ValueSet` reference URI.
150		value_set: &'a str,
151		/// Whether to switch to `not-in` search.
152		not: bool,
153	},
154	/// Tests the `text` or `display` values.
155	CodeText {
156		/// Name of the field to search on.
157		name: &'a str,
158		/// Text to search for (is a starts-with search).
159		text: &'a str,
160	},
161}
162
163impl SearchParameter for TokenSearch<'_> {
164	fn into_query(self) -> (String, String) {
165		match self {
166			Self::Standard { name, system, code, not } => {
167				let key = if not { format!("{name}:not") } else { name.to_owned() };
168				let value = if let Some(system) = system {
169					format!(
170						"{}|{}",
171						escape_search_value(system),
172						escape_search_value(code.unwrap_or_default())
173					)
174				} else {
175					escape_search_value(code.unwrap_or_default())
176				};
177				(key, value)
178			}
179			Self::OfType { type_system, type_code, value } => (
180				"identifier:of-type".to_owned(),
181				format!(
182					"{}|{}|{}",
183					escape_search_value(type_system.unwrap_or_default()),
184					escape_search_value(type_code.unwrap_or_default()),
185					escape_search_value(value.unwrap_or_default())
186				),
187			),
188			Self::In { name, value_set, not } => {
189				if not {
190					(format!("{name}:not-in"), escape_search_value(value_set))
191				} else {
192					(format!("{name}:in"), escape_search_value(value_set))
193				}
194			}
195			Self::CodeText { name, text } => {
196				(format!("{name}:code-text"), escape_search_value(text))
197			}
198		}
199	}
200}
201
202/// Search in references. Includes chaining, i.e. querying fields of the target
203/// resource.
204///
205/// Only implements most common functionality. Refer to adding raw queries when
206/// this does not suffice.
207#[derive(Debug, Clone, Copy)]
208pub enum ReferenceSearch<'a, V: FhirVersion> {
209	/// Standard reference search by relative reference.
210	Standard {
211		/// Name of the field.
212		name: &'a str,
213		/// Resource type of the resource.
214		resource_type: V::ResourceType,
215		/// ID of the resource the reference should point to.
216		id: &'a str,
217		/// Historic version id to search for.
218		version_id: Option<&'a str>,
219	},
220	/// Standard reference search by absolute URL.
221	Url {
222		/// Name of the field.
223		name: &'a str,
224		/// Reference URL.
225		url: &'a str,
226		/// Specific version id to search for.
227		version_id: Option<&'a str>,
228	},
229	/// Reference search by the `.identifier` field.
230	Identifier {
231		/// Name of the field.
232		name: &'a str,
233		/// Identifier system.
234		system: Option<&'a str>,
235		/// Idenfifier value.
236		value: Option<&'a str>,
237	},
238	/// Reference search with chaining. This means the server searches for
239	/// references that target a resource with the given field and value.
240	Chaining {
241		/// Name of the field.
242		name: &'a str,
243		/// Resource type of the reference.
244		resource_type: Option<V::ResourceType>,
245		/// Target resource field name.
246		target_name: &'a str,
247		/// (Raw) value of the target value. Might be any of the ways of search,
248		/// so could be token search including pipes.
249		value: &'a str,
250	},
251}
252
253impl<V: FhirVersion> SearchParameter for ReferenceSearch<'_, V> {
254	fn into_query(self) -> (String, String) {
255		match self {
256			Self::Standard { name, resource_type, id, version_id } => {
257				let value = if let Some(version_id) = version_id {
258					escape_search_value(&format!("{resource_type}/{id}/_history/{version_id}"))
259				} else {
260					escape_search_value(&format!("{resource_type}/{id}"))
261				};
262				(name.to_owned(), value)
263			}
264			Self::Url { name, url, version_id } => {
265				let value = if let Some(version_id) = version_id {
266					format!("{}|{}", escape_search_value(url), escape_search_value(version_id))
267				} else {
268					escape_search_value(url)
269				};
270				(name.to_owned(), value)
271			}
272			Self::Identifier { name, system, value } => (
273				name.to_owned(),
274				format!(
275					"{}|{}",
276					escape_search_value(system.unwrap_or_default()),
277					escape_search_value(value.unwrap_or_default()),
278				),
279			),
280			Self::Chaining { name, resource_type, target_name, value } => {
281				let key = if let Some(resource_type) = resource_type {
282					format!("{name}:{resource_type}.{target_name}")
283				} else {
284					format!("{name}.{target_name}")
285				};
286				(key, value.to_owned())
287			}
288		}
289	}
290}
291
292/// Search on a quantity.
293///
294/// Only implements most common functionality. Refer to adding raw queries when
295/// this does not suffice.
296#[derive(Debug, Clone, Copy)]
297pub struct QuantitySearch<'a, V: FhirVersion> {
298	/// Name of the field.
299	pub name: &'a str,
300	/// Search comparator to compare the date.
301	pub comparator: Option<V::SearchComparator>,
302	/// Value to search for.
303	pub value: &'a str,
304	/// Optional system.
305	pub system: Option<&'a str>,
306	/// Optional code.
307	pub code: Option<&'a str>,
308}
309
310impl<V: FhirVersion> SearchParameter for QuantitySearch<'_, V> {
311	fn into_query(self) -> (String, String) {
312		let value = if let Some(comparator) = self.comparator {
313			format!("{comparator}{}", escape_search_value(self.value))
314		} else {
315			escape_search_value(self.value)
316		};
317
318		let query_value = if self.system.is_some() || self.code.is_some() {
319			format!(
320				"{value}|{}|{}",
321				escape_search_value(self.system.unwrap_or_default()),
322				escape_search_value(self.code.unwrap_or_default())
323			)
324		} else {
325			value
326		};
327
328		(self.name.to_owned(), query_value)
329	}
330}
331
332/// Search on a URI.
333///
334/// Only implements most common functionality. Refer to adding raw queries when
335/// this does not suffice.
336#[derive(Debug, Clone, Copy)]
337pub enum UriSearch<'a> {
338	/// Standard URI search, that matches exactly.
339	Standard {
340		/// Name of the field.
341		name: &'a str,
342		/// URI.
343		uri: &'a str,
344	},
345	/// Match any URL that is below the given URL path, so contains more URL
346	/// segments.
347	Below {
348		/// Name of the field.
349		name: &'a str,
350		/// URI.
351		uri: &'a str,
352	},
353	/// Match any URL that is above the given URL path, so contains less URL
354	/// segments.
355	Above {
356		/// Name of the field.
357		name: &'a str,
358		/// URI.
359		uri: &'a str,
360	},
361}
362
363impl SearchParameter for UriSearch<'_> {
364	fn into_query(self) -> (String, String) {
365		let (name, modifier, uri) = match self {
366			Self::Standard { name, uri } => (name, "", uri),
367			Self::Below { name, uri } => (name, ":below", uri),
368			Self::Above { name, uri } => (name, ":above", uri),
369		};
370		(format!("{name}{modifier}"), escape_search_value(uri))
371	}
372}
373
374/// Search on any item whether it is a missing field using the `missing`
375/// modifier.
376#[derive(Debug, Clone, Copy)]
377pub struct MissingSearch<'a> {
378	/// Name of the field.
379	pub name: &'a str,
380	/// Whether to search for the absent field (or the present).
381	pub missing: bool,
382}
383
384impl SearchParameter for MissingSearch<'_> {
385	fn into_query(self) -> (String, String) {
386		(format!("{}:missing", self.name), self.missing.to_string())
387	}
388}
389
390#[cfg(test)]
391mod tests {
392	use fhir_model::for_all_versions;
393
394	use super::*;
395	use crate::version::fhir_version;
396
397	macro_rules! make_tests {
398		($version:ident) => {
399			mod $version {
400				use $crate::$version::{codes::SearchComparator, resources::ResourceType};
401
402				use super::*;
403
404				#[test]
405				fn number() {
406					let number = NumberSearch::<fhir_version!($version)>::new("value-quantity")
407						.or(Some(SearchComparator::Lt), 60)
408						.or(Some(SearchComparator::Gt), 100);
409					assert_eq!(
410						number.into_query(),
411						("value-quantity".to_owned(), "lt60,gt100".to_owned())
412					);
413				}
414
415				#[test]
416				fn token() {
417					let token = TokenSearch::Standard {
418						name: "identifier",
419						system: None,
420						code: Some("code"),
421						not: true,
422					};
423					assert_eq!(
424						token.into_query(),
425						("identifier:not".to_owned(), "code".to_owned())
426					);
427
428					let token = TokenSearch::Standard {
429						name: "identifier",
430						system: Some(""),
431						code: Some("code"),
432						not: false,
433					};
434					assert_eq!(token.into_query(), ("identifier".to_owned(), "|code".to_owned()));
435
436					let token = TokenSearch::Standard {
437						name: "identifier",
438						system: Some("system"),
439						code: None,
440						not: false,
441					};
442					assert_eq!(token.into_query(), ("identifier".to_owned(), "system|".to_owned()));
443
444					let token = TokenSearch::OfType {
445						type_system: None,
446						type_code: None,
447						value: Some("value"),
448					};
449					assert_eq!(
450						token.into_query(),
451						("identifier:of-type".to_owned(), "||value".to_owned())
452					);
453				}
454
455				#[test]
456				fn reference() {
457					let reference: ReferenceSearch<'_, fhir_version!($version)> =
458						ReferenceSearch::Chaining {
459							name: "focus",
460							resource_type: Some(ResourceType::Encounter),
461							target_name: "status",
462							value: "in-progress",
463						};
464					assert_eq!(
465						reference.into_query(),
466						("focus:Encounter.status".to_owned(), "in-progress".to_owned())
467					);
468				}
469
470				#[test]
471				fn quantity() {
472					let quantity = QuantitySearch::<fhir_version!($version)> {
473						name: "test",
474						comparator: None,
475						value: "1.0",
476						system: None,
477						code: None,
478					};
479					assert_eq!(quantity.into_query(), ("test".to_owned(), "1.0".to_owned()));
480
481					let quantity = QuantitySearch::<fhir_version!($version)> {
482						name: "test",
483						comparator: None,
484						value: "1.0",
485						system: None,
486						code: Some("g"),
487					};
488					assert_eq!(quantity.into_query(), ("test".to_owned(), "1.0||g".to_owned()));
489				}
490
491				#[test]
492				fn missing() {
493					let missing = MissingSearch { name: "identifier", missing: true };
494					assert_eq!(
495						missing.into_query(),
496						("identifier:missing".to_owned(), "true".to_owned())
497					);
498				}
499			}
500		};
501	}
502	for_all_versions!(make_tests);
503}