1use std::collections::HashMap;
7use std::fmt;
8use std::str::FromStr;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "lowercase")]
17pub enum SearchParamType {
18 #[default]
19 String,
21 Uri,
23 Number,
25 Date,
27 Quantity,
29 Token,
31 Reference,
33 Composite,
35 Special,
37}
38
39impl fmt::Display for SearchParamType {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 SearchParamType::String => write!(f, "string"),
43 SearchParamType::Uri => write!(f, "uri"),
44 SearchParamType::Number => write!(f, "number"),
45 SearchParamType::Date => write!(f, "date"),
46 SearchParamType::Quantity => write!(f, "quantity"),
47 SearchParamType::Token => write!(f, "token"),
48 SearchParamType::Reference => write!(f, "reference"),
49 SearchParamType::Composite => write!(f, "composite"),
50 SearchParamType::Special => write!(f, "special"),
51 }
52 }
53}
54
55impl FromStr for SearchParamType {
56 type Err = String;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 match s.to_lowercase().as_str() {
60 "string" => Ok(SearchParamType::String),
61 "uri" => Ok(SearchParamType::Uri),
62 "number" => Ok(SearchParamType::Number),
63 "date" => Ok(SearchParamType::Date),
64 "quantity" => Ok(SearchParamType::Quantity),
65 "token" => Ok(SearchParamType::Token),
66 "reference" => Ok(SearchParamType::Reference),
67 "composite" => Ok(SearchParamType::Composite),
68 "special" => Ok(SearchParamType::Special),
69 _ => Err(format!("unknown search parameter type: {}", s)),
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78#[serde(rename_all = "lowercase")]
79pub enum SearchModifier {
80 Exact,
82 Contains,
84 Text,
86 Not,
88 Missing,
90 Above,
92 Below,
94 In,
96 NotIn,
98 Identifier,
100 Type(String),
102 OfType,
104 CodeOnly,
106 Iterate,
108 TextAdvanced,
117 CodeText,
122}
123
124impl fmt::Display for SearchModifier {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self {
127 SearchModifier::Exact => write!(f, "exact"),
128 SearchModifier::Contains => write!(f, "contains"),
129 SearchModifier::Text => write!(f, "text"),
130 SearchModifier::Not => write!(f, "not"),
131 SearchModifier::Missing => write!(f, "missing"),
132 SearchModifier::Above => write!(f, "above"),
133 SearchModifier::Below => write!(f, "below"),
134 SearchModifier::In => write!(f, "in"),
135 SearchModifier::NotIn => write!(f, "not-in"),
136 SearchModifier::Identifier => write!(f, "identifier"),
137 SearchModifier::Type(t) => write!(f, "{}", t),
138 SearchModifier::OfType => write!(f, "ofType"),
139 SearchModifier::CodeOnly => write!(f, "code"),
140 SearchModifier::Iterate => write!(f, "iterate"),
141 SearchModifier::TextAdvanced => write!(f, "text-advanced"),
142 SearchModifier::CodeText => write!(f, "code-text"),
143 }
144 }
145}
146
147impl SearchModifier {
148 pub fn parse(s: &str) -> Option<Self> {
150 match s.to_lowercase().as_str() {
151 "exact" => Some(SearchModifier::Exact),
152 "contains" => Some(SearchModifier::Contains),
153 "text" => Some(SearchModifier::Text),
154 "not" => Some(SearchModifier::Not),
155 "missing" => Some(SearchModifier::Missing),
156 "above" => Some(SearchModifier::Above),
157 "below" => Some(SearchModifier::Below),
158 "in" => Some(SearchModifier::In),
159 "not-in" => Some(SearchModifier::NotIn),
160 "identifier" => Some(SearchModifier::Identifier),
161 "oftype" => Some(SearchModifier::OfType),
162 "code" => Some(SearchModifier::CodeOnly),
163 "iterate" => Some(SearchModifier::Iterate),
164 "text-advanced" => Some(SearchModifier::TextAdvanced),
165 "code-text" => Some(SearchModifier::CodeText),
166 _ => {
167 if s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
169 Some(SearchModifier::Type(s.to_string()))
170 } else {
171 None
172 }
173 }
174 }
175 }
176
177 pub fn is_valid_for(&self, param_type: SearchParamType) -> bool {
179 match self {
180 SearchModifier::Exact | SearchModifier::Contains => {
181 param_type == SearchParamType::String
182 }
183 SearchModifier::Text => param_type == SearchParamType::Token,
184 SearchModifier::Not => true, SearchModifier::Missing => true, SearchModifier::Above
187 | SearchModifier::Below
188 | SearchModifier::In
189 | SearchModifier::NotIn => {
190 param_type == SearchParamType::Token || param_type == SearchParamType::Uri
191 }
192 SearchModifier::Identifier | SearchModifier::Type(_) => {
193 param_type == SearchParamType::Reference
194 }
195 SearchModifier::OfType => param_type == SearchParamType::Token,
196 SearchModifier::CodeOnly => param_type == SearchParamType::Token,
197 SearchModifier::Iterate => false, SearchModifier::TextAdvanced => {
199 param_type == SearchParamType::String || param_type == SearchParamType::Token
200 }
201 SearchModifier::CodeText => param_type == SearchParamType::Token,
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
210#[serde(rename_all = "lowercase")]
211pub enum SearchPrefix {
212 #[default]
214 Eq,
215 Ne,
217 Gt,
219 Lt,
221 Ge,
223 Le,
225 Sa,
227 Eb,
229 Ap,
231}
232
233impl fmt::Display for SearchPrefix {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 match self {
236 SearchPrefix::Eq => write!(f, "eq"),
237 SearchPrefix::Ne => write!(f, "ne"),
238 SearchPrefix::Gt => write!(f, "gt"),
239 SearchPrefix::Lt => write!(f, "lt"),
240 SearchPrefix::Ge => write!(f, "ge"),
241 SearchPrefix::Le => write!(f, "le"),
242 SearchPrefix::Sa => write!(f, "sa"),
243 SearchPrefix::Eb => write!(f, "eb"),
244 SearchPrefix::Ap => write!(f, "ap"),
245 }
246 }
247}
248
249impl FromStr for SearchPrefix {
250 type Err = String;
251
252 fn from_str(s: &str) -> Result<Self, Self::Err> {
253 match s.to_lowercase().as_str() {
254 "eq" => Ok(SearchPrefix::Eq),
255 "ne" => Ok(SearchPrefix::Ne),
256 "gt" => Ok(SearchPrefix::Gt),
257 "lt" => Ok(SearchPrefix::Lt),
258 "ge" => Ok(SearchPrefix::Ge),
259 "le" => Ok(SearchPrefix::Le),
260 "sa" => Ok(SearchPrefix::Sa),
261 "eb" => Ok(SearchPrefix::Eb),
262 "ap" => Ok(SearchPrefix::Ap),
263 _ => Err(format!("unknown search prefix: {}", s)),
264 }
265 }
266}
267
268impl SearchPrefix {
269 pub fn extract(value: &str) -> (Self, &str) {
273 if value.len() >= 2 {
274 let prefix = &value[..2];
275 if let Ok(p) = prefix.parse() {
276 return (p, &value[2..]);
277 }
278 }
279 (SearchPrefix::Eq, value)
280 }
281
282 pub fn is_valid_for(&self, param_type: SearchParamType) -> bool {
284 match self {
285 SearchPrefix::Eq | SearchPrefix::Ne => true,
286 SearchPrefix::Gt | SearchPrefix::Lt | SearchPrefix::Ge | SearchPrefix::Le => {
287 matches!(
288 param_type,
289 SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity
290 )
291 }
292 SearchPrefix::Sa | SearchPrefix::Eb => param_type == SearchParamType::Date,
293 SearchPrefix::Ap => {
294 matches!(
295 param_type,
296 SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity
297 )
298 }
299 }
300 }
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, Default)]
305pub struct SearchParameter {
306 #[serde(default)]
308 pub name: String,
309
310 #[serde(default)]
312 pub param_type: SearchParamType,
313
314 #[serde(default)]
316 pub modifier: Option<SearchModifier>,
317
318 #[serde(default)]
320 pub values: Vec<SearchValue>,
321
322 #[serde(default)]
324 pub chain: Vec<ChainedParameter>,
325
326 #[serde(default)]
329 pub components: Vec<CompositeSearchComponent>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct CompositeSearchComponent {
338 pub param_type: SearchParamType,
340 pub param_name: String,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct SearchValue {
347 pub prefix: SearchPrefix,
349
350 pub value: String,
352}
353
354impl SearchValue {
355 pub fn new(prefix: SearchPrefix, value: impl Into<String>) -> Self {
357 Self {
358 prefix,
359 value: value.into(),
360 }
361 }
362
363 pub fn eq(value: impl Into<String>) -> Self {
365 Self::new(SearchPrefix::Eq, value)
366 }
367
368 pub fn parse(s: &str) -> Self {
370 let (prefix, value) = SearchPrefix::extract(s);
371 Self::new(prefix, value)
372 }
373
374 pub fn token(system: Option<&str>, code: impl Into<String>) -> Self {
378 let code = code.into();
379 match system {
380 Some(sys) => Self::eq(format!("{}|{}", sys, code)),
381 None => Self::eq(code),
382 }
383 }
384
385 pub fn token_system_only(system: impl Into<String>) -> Self {
389 Self::eq(format!("{}|", system.into()))
390 }
391
392 pub fn boolean(value: bool) -> Self {
394 Self::eq(value.to_string())
395 }
396
397 pub fn string(value: impl Into<String>) -> Self {
399 Self::eq(value)
400 }
401
402 pub fn of_type(
421 type_system: impl Into<String>,
422 type_code: impl Into<String>,
423 value: impl Into<String>,
424 ) -> Self {
425 Self::eq(format!(
426 "{}|{}|{}",
427 type_system.into(),
428 type_code.into(),
429 value.into()
430 ))
431 }
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ChainedParameter {
437 pub reference_param: String,
439
440 pub target_type: Option<String>,
442
443 pub target_param: String,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct ReverseChainedParameter {
453 pub source_type: String,
455
456 pub reference_param: String,
458
459 pub search_param: String,
462
463 pub value: Option<SearchValue>,
465
466 pub nested: Option<Box<ReverseChainedParameter>>,
468}
469
470impl ReverseChainedParameter {
471 pub fn terminal(
473 source_type: impl Into<String>,
474 reference_param: impl Into<String>,
475 search_param: impl Into<String>,
476 value: SearchValue,
477 ) -> Self {
478 Self {
479 source_type: source_type.into(),
480 reference_param: reference_param.into(),
481 search_param: search_param.into(),
482 value: Some(value),
483 nested: None,
484 }
485 }
486
487 pub fn nested(
489 source_type: impl Into<String>,
490 reference_param: impl Into<String>,
491 inner: ReverseChainedParameter,
492 ) -> Self {
493 Self {
494 source_type: source_type.into(),
495 reference_param: reference_param.into(),
496 search_param: String::new(),
497 value: None,
498 nested: Some(Box::new(inner)),
499 }
500 }
501
502 pub fn depth(&self) -> usize {
504 match &self.nested {
505 Some(inner) => 1 + inner.depth(),
506 None => 1,
507 }
508 }
509
510 pub fn is_terminal(&self) -> bool {
512 self.nested.is_none()
513 }
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct ChainConfig {
519 pub max_forward_depth: usize,
522
523 pub max_reverse_depth: usize,
526}
527
528impl Default for ChainConfig {
529 fn default() -> Self {
530 Self {
531 max_forward_depth: 4,
532 max_reverse_depth: 4,
533 }
534 }
535}
536
537impl ChainConfig {
538 pub fn new(max_forward_depth: usize, max_reverse_depth: usize) -> Self {
540 Self {
541 max_forward_depth: max_forward_depth.min(8),
542 max_reverse_depth: max_reverse_depth.min(8),
543 }
544 }
545
546 pub fn validate_forward_depth(&self, depth: usize) -> bool {
548 depth <= self.max_forward_depth
549 }
550
551 pub fn validate_reverse_depth(&self, depth: usize) -> bool {
553 depth <= self.max_reverse_depth
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct IncludeDirective {
560 pub include_type: IncludeType,
562
563 pub source_type: String,
565
566 pub search_param: String,
568
569 pub target_type: Option<String>,
571
572 pub iterate: bool,
574}
575
576#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
578pub enum IncludeType {
579 Include,
581 Revinclude,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
587pub enum SortDirection {
588 #[default]
590 Ascending,
591 Descending,
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct SortDirective {
598 pub parameter: String,
600 pub direction: SortDirection,
602}
603
604impl SortDirective {
605 pub fn parse(s: &str) -> Self {
607 if let Some(stripped) = s.strip_prefix('-') {
608 Self {
609 parameter: stripped.to_string(),
610 direction: SortDirection::Descending,
611 }
612 } else {
613 Self {
614 parameter: s.to_string(),
615 direction: SortDirection::Ascending,
616 }
617 }
618 }
619}
620
621#[derive(Debug, Clone, Default, Serialize, Deserialize)]
623pub struct SearchQuery {
624 pub resource_type: String,
626
627 pub parameters: Vec<SearchParameter>,
629
630 pub reverse_chains: Vec<ReverseChainedParameter>,
632
633 pub includes: Vec<IncludeDirective>,
635
636 pub sort: Vec<SortDirective>,
638
639 pub count: Option<u32>,
641
642 pub offset: Option<u32>,
644
645 pub cursor: Option<String>,
647
648 pub total: Option<TotalMode>,
650
651 pub summary: Option<SummaryMode>,
653
654 pub elements: Vec<String>,
656
657 pub raw_params: HashMap<String, Vec<String>>,
659}
660
661#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
663#[serde(rename_all = "lowercase")]
664pub enum TotalMode {
665 None,
667 Estimate,
669 Accurate,
671}
672
673#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
675#[serde(rename_all = "lowercase")]
676pub enum SummaryMode {
677 True,
679 False,
681 Text,
683 Data,
685 Count,
687}
688
689impl SearchQuery {
690 pub fn new(resource_type: impl Into<String>) -> Self {
692 Self {
693 resource_type: resource_type.into(),
694 ..Default::default()
695 }
696 }
697
698 pub fn with_parameter(mut self, param: SearchParameter) -> Self {
700 self.parameters.push(param);
701 self
702 }
703
704 pub fn with_include(mut self, include: IncludeDirective) -> Self {
706 self.includes.push(include);
707 self
708 }
709
710 pub fn with_sort(mut self, sort: SortDirective) -> Self {
712 self.sort.push(sort);
713 self
714 }
715
716 pub fn with_count(mut self, count: u32) -> Self {
718 self.count = Some(count);
719 self
720 }
721
722 pub fn with_cursor(mut self, cursor: String) -> Self {
724 self.cursor = Some(cursor);
725 self
726 }
727
728 pub fn requires_advanced_features(&self) -> bool {
730 if self.parameters.iter().any(|p| !p.chain.is_empty()) {
732 return true;
733 }
734
735 if !self.reverse_chains.is_empty() {
737 return true;
738 }
739
740 if !self.includes.is_empty() {
742 return true;
743 }
744
745 if self.parameters.iter().any(|p| {
747 matches!(
748 p.modifier,
749 Some(SearchModifier::Above)
750 | Some(SearchModifier::Below)
751 | Some(SearchModifier::In)
752 | Some(SearchModifier::NotIn)
753 )
754 }) {
755 return true;
756 }
757
758 false
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765
766 #[test]
767 fn test_search_param_type_display() {
768 assert_eq!(SearchParamType::String.to_string(), "string");
769 assert_eq!(SearchParamType::Token.to_string(), "token");
770 assert_eq!(SearchParamType::Reference.to_string(), "reference");
771 }
772
773 #[test]
774 fn test_search_param_type_parse() {
775 assert_eq!(
776 "string".parse::<SearchParamType>().unwrap(),
777 SearchParamType::String
778 );
779 assert_eq!(
780 "TOKEN".parse::<SearchParamType>().unwrap(),
781 SearchParamType::Token
782 );
783 }
784
785 #[test]
786 fn test_search_modifier_parse() {
787 assert_eq!(SearchModifier::parse("exact"), Some(SearchModifier::Exact));
788 assert_eq!(
789 SearchModifier::parse("contains"),
790 Some(SearchModifier::Contains)
791 );
792 assert_eq!(
793 SearchModifier::parse("Patient"),
794 Some(SearchModifier::Type("Patient".to_string()))
795 );
796 assert_eq!(SearchModifier::parse("unknown"), None);
797 }
798
799 #[test]
800 fn test_search_modifier_validity() {
801 assert!(SearchModifier::Exact.is_valid_for(SearchParamType::String));
802 assert!(!SearchModifier::Exact.is_valid_for(SearchParamType::Token));
803 assert!(SearchModifier::Text.is_valid_for(SearchParamType::Token));
804 assert!(SearchModifier::Not.is_valid_for(SearchParamType::String));
805 assert!(SearchModifier::Not.is_valid_for(SearchParamType::Token));
806 }
807
808 #[test]
809 fn test_search_prefix_extract() {
810 assert_eq!(
811 SearchPrefix::extract("gt2020-01-01"),
812 (SearchPrefix::Gt, "2020-01-01")
813 );
814 assert_eq!(
815 SearchPrefix::extract("2020-01-01"),
816 (SearchPrefix::Eq, "2020-01-01")
817 );
818 assert_eq!(SearchPrefix::extract("le100"), (SearchPrefix::Le, "100"));
819 }
820
821 #[test]
822 fn test_search_prefix_validity() {
823 assert!(SearchPrefix::Gt.is_valid_for(SearchParamType::Number));
824 assert!(SearchPrefix::Gt.is_valid_for(SearchParamType::Date));
825 assert!(!SearchPrefix::Gt.is_valid_for(SearchParamType::String));
826 assert!(SearchPrefix::Sa.is_valid_for(SearchParamType::Date));
827 assert!(!SearchPrefix::Sa.is_valid_for(SearchParamType::Number));
828 }
829
830 #[test]
831 fn test_search_value_parse() {
832 let value = SearchValue::parse("gt100");
833 assert_eq!(value.prefix, SearchPrefix::Gt);
834 assert_eq!(value.value, "100");
835
836 let value2 = SearchValue::parse("Smith");
837 assert_eq!(value2.prefix, SearchPrefix::Eq);
838 assert_eq!(value2.value, "Smith");
839 }
840
841 #[test]
842 fn test_sort_directive_parse() {
843 let asc = SortDirective::parse("date");
844 assert_eq!(asc.parameter, "date");
845 assert_eq!(asc.direction, SortDirection::Ascending);
846
847 let desc = SortDirective::parse("-date");
848 assert_eq!(desc.parameter, "date");
849 assert_eq!(desc.direction, SortDirection::Descending);
850 }
851
852 #[test]
853 fn test_search_query_builder() {
854 let query = SearchQuery::new("Patient")
855 .with_count(10)
856 .with_sort(SortDirective::parse("-_lastUpdated"));
857
858 assert_eq!(query.resource_type, "Patient");
859 assert_eq!(query.count, Some(10));
860 assert_eq!(query.sort.len(), 1);
861 }
862
863 #[test]
864 fn test_requires_advanced_features() {
865 let simple = SearchQuery::new("Patient");
866 assert!(!simple.requires_advanced_features());
867
868 let with_include = SearchQuery::new("Patient").with_include(IncludeDirective {
869 include_type: IncludeType::Include,
870 source_type: "Patient".to_string(),
871 search_param: "organization".to_string(),
872 target_type: None,
873 iterate: false,
874 });
875 assert!(with_include.requires_advanced_features());
876 }
877}