1use std::collections::HashMap;
10use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14
15pub use helios_fhir::search::SearchParamType;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum SearchModifier {
23 Exact,
25 Contains,
27 Text,
29 Not,
31 Missing,
33 Above,
35 Below,
37 In,
39 NotIn,
41 Identifier,
43 Type(String),
45 OfType,
47 Iterate,
49 TextAdvanced,
58 CodeText,
63}
64
65impl fmt::Display for SearchModifier {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 SearchModifier::Exact => write!(f, "exact"),
69 SearchModifier::Contains => write!(f, "contains"),
70 SearchModifier::Text => write!(f, "text"),
71 SearchModifier::Not => write!(f, "not"),
72 SearchModifier::Missing => write!(f, "missing"),
73 SearchModifier::Above => write!(f, "above"),
74 SearchModifier::Below => write!(f, "below"),
75 SearchModifier::In => write!(f, "in"),
76 SearchModifier::NotIn => write!(f, "not-in"),
77 SearchModifier::Identifier => write!(f, "identifier"),
78 SearchModifier::Type(t) => write!(f, "{}", t),
79 SearchModifier::OfType => write!(f, "ofType"),
80 SearchModifier::Iterate => write!(f, "iterate"),
81 SearchModifier::TextAdvanced => write!(f, "text-advanced"),
82 SearchModifier::CodeText => write!(f, "code-text"),
83 }
84 }
85}
86
87impl SearchModifier {
88 pub fn parse(s: &str) -> Option<Self> {
90 match s.to_lowercase().as_str() {
91 "exact" => Some(SearchModifier::Exact),
92 "contains" => Some(SearchModifier::Contains),
93 "text" => Some(SearchModifier::Text),
94 "not" => Some(SearchModifier::Not),
95 "missing" => Some(SearchModifier::Missing),
96 "above" => Some(SearchModifier::Above),
97 "below" => Some(SearchModifier::Below),
98 "in" => Some(SearchModifier::In),
99 "not-in" => Some(SearchModifier::NotIn),
100 "identifier" => Some(SearchModifier::Identifier),
101 "of-type" | "oftype" => Some(SearchModifier::OfType),
105 "iterate" => Some(SearchModifier::Iterate),
106 "text-advanced" => Some(SearchModifier::TextAdvanced),
107 "code-text" => Some(SearchModifier::CodeText),
108 _ => {
109 if s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
111 Some(SearchModifier::Type(s.to_string()))
112 } else {
113 None
114 }
115 }
116 }
117 }
118
119 pub fn is_valid_for(&self, param_type: SearchParamType) -> bool {
121 match self {
122 SearchModifier::Exact => param_type == SearchParamType::String,
123 SearchModifier::Contains => matches!(
126 param_type,
127 SearchParamType::String | SearchParamType::Reference | SearchParamType::Uri
128 ),
129 SearchModifier::Text => matches!(
132 param_type,
133 SearchParamType::String | SearchParamType::Token | SearchParamType::Reference
134 ),
135 SearchModifier::Not => param_type == SearchParamType::Token,
139 SearchModifier::Missing => true, SearchModifier::Above | SearchModifier::Below => matches!(
144 param_type,
145 SearchParamType::Token | SearchParamType::Uri | SearchParamType::Reference
146 ),
147 SearchModifier::In | SearchModifier::NotIn => param_type == SearchParamType::Token,
151 SearchModifier::Identifier | SearchModifier::Type(_) => {
152 param_type == SearchParamType::Reference
153 }
154 SearchModifier::OfType => param_type == SearchParamType::Token,
155 SearchModifier::Iterate => false, SearchModifier::TextAdvanced => matches!(
159 param_type,
160 SearchParamType::Token | SearchParamType::Reference
161 ),
162 SearchModifier::CodeText => matches!(
165 param_type,
166 SearchParamType::Token | SearchParamType::Reference
167 ),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
176#[serde(rename_all = "lowercase")]
177pub enum SearchPrefix {
178 #[default]
180 Eq,
181 Ne,
183 Gt,
185 Lt,
187 Ge,
189 Le,
191 Sa,
193 Eb,
195 Ap,
197}
198
199impl fmt::Display for SearchPrefix {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 match self {
202 SearchPrefix::Eq => write!(f, "eq"),
203 SearchPrefix::Ne => write!(f, "ne"),
204 SearchPrefix::Gt => write!(f, "gt"),
205 SearchPrefix::Lt => write!(f, "lt"),
206 SearchPrefix::Ge => write!(f, "ge"),
207 SearchPrefix::Le => write!(f, "le"),
208 SearchPrefix::Sa => write!(f, "sa"),
209 SearchPrefix::Eb => write!(f, "eb"),
210 SearchPrefix::Ap => write!(f, "ap"),
211 }
212 }
213}
214
215impl FromStr for SearchPrefix {
216 type Err = String;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 match s.to_lowercase().as_str() {
220 "eq" => Ok(SearchPrefix::Eq),
221 "ne" => Ok(SearchPrefix::Ne),
222 "gt" => Ok(SearchPrefix::Gt),
223 "lt" => Ok(SearchPrefix::Lt),
224 "ge" => Ok(SearchPrefix::Ge),
225 "le" => Ok(SearchPrefix::Le),
226 "sa" => Ok(SearchPrefix::Sa),
227 "eb" => Ok(SearchPrefix::Eb),
228 "ap" => Ok(SearchPrefix::Ap),
229 _ => Err(format!("unknown search prefix: {}", s)),
230 }
231 }
232}
233
234impl SearchPrefix {
235 pub fn extract(value: &str) -> (Self, &str) {
239 if let Some(prefix) = value.get(..2) {
243 if let Ok(p) = prefix.parse() {
244 return (p, &value[2..]);
245 }
246 }
247 (SearchPrefix::Eq, value)
248 }
249
250 pub fn is_valid_for(&self, param_type: SearchParamType) -> bool {
252 match self {
253 SearchPrefix::Eq | SearchPrefix::Ne => true,
254 SearchPrefix::Gt | SearchPrefix::Lt | SearchPrefix::Ge | SearchPrefix::Le => {
255 matches!(
256 param_type,
257 SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity
258 )
259 }
260 SearchPrefix::Sa | SearchPrefix::Eb => matches!(
263 param_type,
264 SearchParamType::Date | SearchParamType::Quantity
265 ),
266 SearchPrefix::Ap => {
267 matches!(
268 param_type,
269 SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity
270 )
271 }
272 }
273 }
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize, Default)]
278pub struct SearchParameter {
279 #[serde(default)]
281 pub name: String,
282
283 #[serde(default)]
285 pub param_type: SearchParamType,
286
287 #[serde(default)]
289 pub modifier: Option<SearchModifier>,
290
291 #[serde(default)]
293 pub values: Vec<SearchValue>,
294
295 #[serde(default)]
297 pub chain: Vec<ChainedParameter>,
298
299 #[serde(default)]
302 pub components: Vec<CompositeSearchComponent>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct CompositeSearchComponent {
311 pub param_type: SearchParamType,
313 pub param_name: String,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct SearchValue {
320 pub prefix: SearchPrefix,
322
323 pub value: String,
325}
326
327impl SearchValue {
328 pub fn new(prefix: SearchPrefix, value: impl Into<String>) -> Self {
330 Self {
331 prefix,
332 value: value.into(),
333 }
334 }
335
336 pub fn eq(value: impl Into<String>) -> Self {
338 Self::new(SearchPrefix::Eq, value)
339 }
340
341 pub fn parse(s: &str) -> Self {
343 let (prefix, value) = SearchPrefix::extract(s);
344 Self::new(prefix, value)
345 }
346
347 pub fn token(system: Option<&str>, code: impl Into<String>) -> Self {
351 let code = code.into();
352 match system {
353 Some(sys) => Self::eq(format!("{}|{}", sys, code)),
354 None => Self::eq(code),
355 }
356 }
357
358 pub fn token_system_only(system: impl Into<String>) -> Self {
362 Self::eq(format!("{}|", system.into()))
363 }
364
365 pub fn boolean(value: bool) -> Self {
367 Self::eq(value.to_string())
368 }
369
370 pub fn string(value: impl Into<String>) -> Self {
372 Self::eq(value)
373 }
374
375 pub fn of_type(
394 type_system: impl Into<String>,
395 type_code: impl Into<String>,
396 value: impl Into<String>,
397 ) -> Self {
398 Self::eq(format!(
399 "{}|{}|{}",
400 type_system.into(),
401 type_code.into(),
402 value.into()
403 ))
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ChainedParameter {
410 pub reference_param: String,
412
413 pub target_type: Option<String>,
415
416 pub target_param: String,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct ReverseChainedParameter {
426 pub source_type: String,
428
429 pub reference_param: String,
431
432 pub search_param: String,
435
436 pub value: Option<SearchValue>,
438
439 pub nested: Option<Box<ReverseChainedParameter>>,
441}
442
443impl ReverseChainedParameter {
444 pub fn terminal(
446 source_type: impl Into<String>,
447 reference_param: impl Into<String>,
448 search_param: impl Into<String>,
449 value: SearchValue,
450 ) -> Self {
451 Self {
452 source_type: source_type.into(),
453 reference_param: reference_param.into(),
454 search_param: search_param.into(),
455 value: Some(value),
456 nested: None,
457 }
458 }
459
460 pub fn nested(
462 source_type: impl Into<String>,
463 reference_param: impl Into<String>,
464 inner: ReverseChainedParameter,
465 ) -> Self {
466 Self {
467 source_type: source_type.into(),
468 reference_param: reference_param.into(),
469 search_param: String::new(),
470 value: None,
471 nested: Some(Box::new(inner)),
472 }
473 }
474
475 pub fn depth(&self) -> usize {
477 match &self.nested {
478 Some(inner) => 1 + inner.depth(),
479 None => 1,
480 }
481 }
482
483 pub fn is_terminal(&self) -> bool {
485 self.nested.is_none()
486 }
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct ChainConfig {
492 pub max_forward_depth: usize,
495
496 pub max_reverse_depth: usize,
499}
500
501impl Default for ChainConfig {
502 fn default() -> Self {
503 Self {
504 max_forward_depth: 4,
505 max_reverse_depth: 4,
506 }
507 }
508}
509
510impl ChainConfig {
511 pub fn new(max_forward_depth: usize, max_reverse_depth: usize) -> Self {
513 Self {
514 max_forward_depth: max_forward_depth.min(8),
515 max_reverse_depth: max_reverse_depth.min(8),
516 }
517 }
518
519 pub fn validate_forward_depth(&self, depth: usize) -> bool {
521 depth <= self.max_forward_depth
522 }
523
524 pub fn validate_reverse_depth(&self, depth: usize) -> bool {
526 depth <= self.max_reverse_depth
527 }
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct IncludeDirective {
533 pub include_type: IncludeType,
535
536 pub source_type: String,
538
539 pub search_param: String,
541
542 pub target_type: Option<String>,
544
545 pub iterate: bool,
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
551pub enum IncludeType {
552 Include,
554 Revinclude,
556}
557
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
560pub enum SortDirection {
561 #[default]
563 Ascending,
564 Descending,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct SortDirective {
571 pub parameter: String,
573 pub direction: SortDirection,
575 #[serde(default)]
580 pub param_type: Option<SearchParamType>,
581}
582
583impl SortDirective {
584 pub fn parse(s: &str) -> Self {
586 if let Some(stripped) = s.strip_prefix('-') {
587 Self {
588 parameter: stripped.to_string(),
589 direction: SortDirection::Descending,
590 param_type: None,
591 }
592 } else {
593 Self {
594 parameter: s.to_string(),
595 direction: SortDirection::Ascending,
596 param_type: None,
597 }
598 }
599 }
600
601 pub fn with_param_type(mut self, param_type: Option<SearchParamType>) -> Self {
603 self.param_type = param_type;
604 self
605 }
606}
607
608#[derive(Debug, Clone, Default, Serialize, Deserialize)]
610pub struct SearchQuery {
611 pub resource_type: String,
613
614 pub parameters: Vec<SearchParameter>,
616
617 pub reverse_chains: Vec<ReverseChainedParameter>,
619
620 pub list: Vec<String>,
626
627 pub contained: ContainedMode,
630
631 pub contained_return: ContainedReturn,
634
635 pub includes: Vec<IncludeDirective>,
637
638 pub sort: Vec<SortDirective>,
640
641 pub count: Option<u32>,
643
644 pub offset: Option<u32>,
646
647 pub cursor: Option<String>,
649
650 pub total: Option<TotalMode>,
652
653 pub summary: Option<SummaryMode>,
655
656 pub elements: Vec<String>,
658
659 pub compartment: Option<CompartmentMembership>,
664
665 pub raw_params: HashMap<String, Vec<String>>,
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
675pub struct CompartmentMembership {
676 pub params: Vec<String>,
679 pub reference: String,
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
686#[serde(rename_all = "lowercase")]
687pub enum ContainedMode {
688 #[default]
690 Off,
691 On,
693 Both,
695}
696
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
700#[serde(rename_all = "lowercase")]
701pub enum ContainedReturn {
702 #[default]
704 Container,
705 Contained,
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
711#[serde(rename_all = "lowercase")]
712pub enum TotalMode {
713 None,
715 Estimate,
717 Accurate,
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
723#[serde(rename_all = "lowercase")]
724pub enum SummaryMode {
725 True,
727 False,
729 Text,
731 Data,
733 Count,
735}
736
737pub fn strip_reference_version(reference: &str) -> &str {
742 match reference.find("/_history/") {
743 Some(i) => &reference[..i],
744 None => reference,
745 }
746}
747
748impl SearchQuery {
749 pub fn new(resource_type: impl Into<String>) -> Self {
751 Self {
752 resource_type: resource_type.into(),
753 ..Default::default()
754 }
755 }
756
757 pub fn wants_total(&self) -> bool {
763 matches!(self.total, Some(TotalMode::Estimate | TotalMode::Accurate))
764 }
765
766 pub fn with_parameter(mut self, param: SearchParameter) -> Self {
768 self.parameters.push(param);
769 self
770 }
771
772 pub fn with_include(mut self, include: IncludeDirective) -> Self {
774 self.includes.push(include);
775 self
776 }
777
778 pub fn with_sort(mut self, sort: SortDirective) -> Self {
780 self.sort.push(sort);
781 self
782 }
783
784 pub fn with_count(mut self, count: u32) -> Self {
786 self.count = Some(count);
787 self
788 }
789
790 pub fn with_cursor(mut self, cursor: String) -> Self {
792 self.cursor = Some(cursor);
793 self
794 }
795
796 pub fn requires_advanced_features(&self) -> bool {
798 if self.parameters.iter().any(|p| !p.chain.is_empty()) {
800 return true;
801 }
802
803 if !self.reverse_chains.is_empty() {
805 return true;
806 }
807
808 if !self.includes.is_empty() {
810 return true;
811 }
812
813 if self.parameters.iter().any(|p| {
815 matches!(
816 p.modifier,
817 Some(SearchModifier::Above)
818 | Some(SearchModifier::Below)
819 | Some(SearchModifier::In)
820 | Some(SearchModifier::NotIn)
821 )
822 }) {
823 return true;
824 }
825
826 false
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 #[test]
835 fn test_search_param_type_display() {
836 assert_eq!(SearchParamType::String.to_string(), "string");
837 assert_eq!(SearchParamType::Token.to_string(), "token");
838 assert_eq!(SearchParamType::Reference.to_string(), "reference");
839 }
840
841 #[test]
842 fn test_search_param_type_parse() {
843 assert_eq!(
844 "string".parse::<SearchParamType>().unwrap(),
845 SearchParamType::String
846 );
847 assert_eq!(
848 "TOKEN".parse::<SearchParamType>().unwrap(),
849 SearchParamType::Token
850 );
851 }
852
853 #[test]
854 fn test_search_modifier_parse() {
855 assert_eq!(SearchModifier::parse("exact"), Some(SearchModifier::Exact));
856 assert_eq!(
857 SearchModifier::parse("contains"),
858 Some(SearchModifier::Contains)
859 );
860 assert_eq!(
861 SearchModifier::parse("Patient"),
862 Some(SearchModifier::Type("Patient".to_string()))
863 );
864 assert_eq!(
867 SearchModifier::parse("of-type"),
868 Some(SearchModifier::OfType)
869 );
870 assert_eq!(
871 SearchModifier::parse("ofType"),
872 Some(SearchModifier::OfType)
873 );
874 assert_eq!(SearchModifier::parse("unknown"), None);
875 }
876
877 #[test]
878 fn test_search_modifier_validity() {
879 assert!(SearchModifier::Exact.is_valid_for(SearchParamType::String));
880 assert!(!SearchModifier::Exact.is_valid_for(SearchParamType::Token));
881 assert!(SearchModifier::Contains.is_valid_for(SearchParamType::String));
883 assert!(SearchModifier::Contains.is_valid_for(SearchParamType::Reference));
884 assert!(SearchModifier::Contains.is_valid_for(SearchParamType::Uri));
885 assert!(!SearchModifier::Contains.is_valid_for(SearchParamType::Token));
886 assert!(SearchModifier::Text.is_valid_for(SearchParamType::Token));
887 assert!(SearchModifier::Text.is_valid_for(SearchParamType::String));
889 assert!(!SearchModifier::Text.is_valid_for(SearchParamType::Uri));
890 assert!(SearchModifier::Not.is_valid_for(SearchParamType::Token));
892 assert!(!SearchModifier::Not.is_valid_for(SearchParamType::String));
893 assert!(SearchModifier::Missing.is_valid_for(SearchParamType::String));
895 assert!(SearchModifier::Missing.is_valid_for(SearchParamType::Reference));
896 assert!(SearchModifier::In.is_valid_for(SearchParamType::Token));
898 assert!(!SearchModifier::In.is_valid_for(SearchParamType::Uri));
899 assert!(SearchModifier::NotIn.is_valid_for(SearchParamType::Token));
900 assert!(!SearchModifier::NotIn.is_valid_for(SearchParamType::Uri));
901 assert!(SearchModifier::Above.is_valid_for(SearchParamType::Uri));
903 assert!(SearchModifier::Below.is_valid_for(SearchParamType::Token));
904 assert!(SearchModifier::Above.is_valid_for(SearchParamType::Reference));
905 assert!(SearchModifier::Below.is_valid_for(SearchParamType::Reference));
906 assert!(!SearchModifier::Above.is_valid_for(SearchParamType::String));
907 }
908
909 #[test]
910 fn test_search_prefix_extract() {
911 assert_eq!(
912 SearchPrefix::extract("gt2020-01-01"),
913 (SearchPrefix::Gt, "2020-01-01")
914 );
915 assert_eq!(
916 SearchPrefix::extract("2020-01-01"),
917 (SearchPrefix::Eq, "2020-01-01")
918 );
919 assert_eq!(SearchPrefix::extract("le100"), (SearchPrefix::Le, "100"));
920 }
921
922 #[test]
923 fn test_search_prefix_validity() {
924 assert!(SearchPrefix::Gt.is_valid_for(SearchParamType::Number));
925 assert!(SearchPrefix::Gt.is_valid_for(SearchParamType::Date));
926 assert!(!SearchPrefix::Gt.is_valid_for(SearchParamType::String));
927 assert!(SearchPrefix::Sa.is_valid_for(SearchParamType::Date));
928 assert!(SearchPrefix::Sa.is_valid_for(SearchParamType::Quantity));
930 assert!(SearchPrefix::Eb.is_valid_for(SearchParamType::Quantity));
931 assert!(!SearchPrefix::Sa.is_valid_for(SearchParamType::Number));
932 }
933
934 #[test]
935 fn test_search_value_parse() {
936 let value = SearchValue::parse("gt100");
937 assert_eq!(value.prefix, SearchPrefix::Gt);
938 assert_eq!(value.value, "100");
939
940 let value2 = SearchValue::parse("Smith");
941 assert_eq!(value2.prefix, SearchPrefix::Eq);
942 assert_eq!(value2.value, "Smith");
943 }
944
945 #[test]
946 fn test_sort_directive_parse() {
947 let asc = SortDirective::parse("date");
948 assert_eq!(asc.parameter, "date");
949 assert_eq!(asc.direction, SortDirection::Ascending);
950
951 let desc = SortDirective::parse("-date");
952 assert_eq!(desc.parameter, "date");
953 assert_eq!(desc.direction, SortDirection::Descending);
954 }
955
956 #[test]
957 fn test_search_query_builder() {
958 let query = SearchQuery::new("Patient")
959 .with_count(10)
960 .with_sort(SortDirective::parse("-_lastUpdated"));
961
962 assert_eq!(query.resource_type, "Patient");
963 assert_eq!(query.count, Some(10));
964 assert_eq!(query.sort.len(), 1);
965 }
966
967 #[test]
968 fn test_requires_advanced_features() {
969 let simple = SearchQuery::new("Patient");
970 assert!(!simple.requires_advanced_features());
971
972 let with_include = SearchQuery::new("Patient").with_include(IncludeDirective {
973 include_type: IncludeType::Include,
974 source_type: "Patient".to_string(),
975 search_param: "organization".to_string(),
976 target_type: None,
977 iterate: false,
978 });
979 assert!(with_include.requires_advanced_features());
980 }
981}