1use std::collections::HashSet;
11
12use serde::{Deserialize, Serialize};
13
14use super::SearchParamType;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub enum SpecialSearchParam {
23 Id,
25 LastUpdated,
27 Tag,
29 Profile,
31 Security,
33 Text,
35 Content,
37 List,
39 Has,
41 Type,
43 Query,
45 Filter,
47 Source,
49}
50
51impl SpecialSearchParam {
52 pub fn name(&self) -> &'static str {
54 match self {
55 SpecialSearchParam::Id => "_id",
56 SpecialSearchParam::LastUpdated => "_lastUpdated",
57 SpecialSearchParam::Tag => "_tag",
58 SpecialSearchParam::Profile => "_profile",
59 SpecialSearchParam::Security => "_security",
60 SpecialSearchParam::Text => "_text",
61 SpecialSearchParam::Content => "_content",
62 SpecialSearchParam::List => "_list",
63 SpecialSearchParam::Has => "_has",
64 SpecialSearchParam::Type => "_type",
65 SpecialSearchParam::Query => "_query",
66 SpecialSearchParam::Filter => "_filter",
67 SpecialSearchParam::Source => "_source",
68 }
69 }
70
71 pub fn param_type(&self) -> SearchParamType {
73 match self {
74 SpecialSearchParam::Id => SearchParamType::Token,
75 SpecialSearchParam::LastUpdated => SearchParamType::Date,
76 SpecialSearchParam::Tag => SearchParamType::Token,
77 SpecialSearchParam::Profile => SearchParamType::Uri,
78 SpecialSearchParam::Security => SearchParamType::Token,
79 SpecialSearchParam::Text => SearchParamType::Special,
80 SpecialSearchParam::Content => SearchParamType::Special,
81 SpecialSearchParam::List => SearchParamType::Reference,
82 SpecialSearchParam::Has => SearchParamType::Special,
83 SpecialSearchParam::Type => SearchParamType::Token,
84 SpecialSearchParam::Query => SearchParamType::Special,
85 SpecialSearchParam::Filter => SearchParamType::Special,
86 SpecialSearchParam::Source => SearchParamType::Uri,
87 }
88 }
89
90 pub fn all() -> &'static [SpecialSearchParam] {
92 &[
93 SpecialSearchParam::Id,
94 SpecialSearchParam::LastUpdated,
95 SpecialSearchParam::Tag,
96 SpecialSearchParam::Profile,
97 SpecialSearchParam::Security,
98 SpecialSearchParam::Text,
99 SpecialSearchParam::Content,
100 SpecialSearchParam::List,
101 SpecialSearchParam::Has,
102 SpecialSearchParam::Type,
103 SpecialSearchParam::Query,
104 SpecialSearchParam::Filter,
105 SpecialSearchParam::Source,
106 ]
107 }
108}
109
110impl std::fmt::Display for SpecialSearchParam {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 write!(f, "{}", self.name())
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub enum IncludeCapability {
120 Include,
122 Revinclude,
124 IncludeIterate,
126 RevincludeIterate,
128 IncludeWildcard,
130 RevincludeWildcard,
132}
133
134impl IncludeCapability {
135 pub fn modifier(&self) -> Option<&'static str> {
137 match self {
138 IncludeCapability::Include => None,
139 IncludeCapability::Revinclude => None,
140 IncludeCapability::IncludeIterate => Some("iterate"),
141 IncludeCapability::RevincludeIterate => Some("iterate"),
142 IncludeCapability::IncludeWildcard => None,
143 IncludeCapability::RevincludeWildcard => None,
144 }
145 }
146
147 pub fn is_include(&self) -> bool {
149 matches!(
150 self,
151 IncludeCapability::Include
152 | IncludeCapability::IncludeIterate
153 | IncludeCapability::IncludeWildcard
154 )
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub enum ChainingCapability {
162 ForwardChain,
164 ReverseChain,
166 MaxDepth(u8),
168}
169
170impl ChainingCapability {
171 pub fn max_depth(&self) -> Option<u8> {
173 match self {
174 ChainingCapability::MaxDepth(d) => Some(*d),
175 _ => None,
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub enum PaginationCapability {
184 Count,
186 Offset,
188 Cursor,
190 MaxPageSize(u32),
192 DefaultPageSize(u32),
194}
195
196impl PaginationCapability {
197 pub fn page_size(&self) -> Option<u32> {
199 match self {
200 PaginationCapability::MaxPageSize(s) | PaginationCapability::DefaultPageSize(s) => {
201 Some(*s)
202 }
203 _ => None,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub enum ResultModeCapability {
214 Summary,
216 SummaryTrue,
218 SummaryText,
220 SummaryData,
222 SummaryCount,
224 SummaryFalse,
226 Elements,
228 Total,
230 TotalNone,
232 TotalEstimate,
234 TotalAccurate,
236 Contained,
238 ContainedType,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
244pub struct CompositeComponent {
245 pub definition: String,
247 pub expression: String,
249}
250
251impl CompositeComponent {
252 pub fn new(definition: impl Into<String>, expression: impl Into<String>) -> Self {
254 Self {
255 definition: definition.into(),
256 expression: expression.into(),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct SearchParamFullCapability {
267 pub name: String,
269
270 pub param_type: SearchParamType,
272
273 pub definition: Option<String>,
275
276 pub modifiers: HashSet<String>,
278
279 pub prefixes: HashSet<String>,
281
282 pub chaining: Option<ChainingCapability>,
284
285 pub target_types: Vec<String>,
287
288 pub components: Vec<CompositeComponent>,
290
291 pub shall_support: bool,
293}
294
295impl SearchParamFullCapability {
296 pub fn new(name: impl Into<String>, param_type: SearchParamType) -> Self {
298 Self {
299 name: name.into(),
300 param_type,
301 definition: None,
302 modifiers: HashSet::new(),
303 prefixes: Self::default_prefixes(param_type),
304 chaining: None,
305 target_types: Vec::new(),
306 components: Vec::new(),
307 shall_support: false,
308 }
309 }
310
311 fn default_prefixes(param_type: SearchParamType) -> HashSet<String> {
313 let mut prefixes = HashSet::new();
314 prefixes.insert("eq".to_string());
315
316 match param_type {
317 SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity => {
318 prefixes.insert("ne".to_string());
319 prefixes.insert("gt".to_string());
320 prefixes.insert("lt".to_string());
321 prefixes.insert("ge".to_string());
322 prefixes.insert("le".to_string());
323 }
324 _ => {}
325 }
326
327 if param_type == SearchParamType::Date {
328 prefixes.insert("sa".to_string());
329 prefixes.insert("eb".to_string());
330 prefixes.insert("ap".to_string());
331 }
332
333 prefixes
334 }
335
336 pub fn with_definition(mut self, url: impl Into<String>) -> Self {
338 self.definition = Some(url.into());
339 self
340 }
341
342 pub fn with_modifiers<I, S>(mut self, modifiers: I) -> Self
344 where
345 I: IntoIterator<Item = S>,
346 S: Into<String>,
347 {
348 self.modifiers = modifiers.into_iter().map(Into::into).collect();
349 self
350 }
351
352 pub fn with_chaining(mut self, chaining: ChainingCapability) -> Self {
354 self.chaining = Some(chaining);
355 self
356 }
357
358 pub fn with_targets<I, S>(mut self, targets: I) -> Self
360 where
361 I: IntoIterator<Item = S>,
362 S: Into<String>,
363 {
364 self.target_types = targets.into_iter().map(Into::into).collect();
365 self
366 }
367
368 pub fn with_components(mut self, components: Vec<CompositeComponent>) -> Self {
370 self.components = components;
371 self
372 }
373
374 pub fn shall(mut self) -> Self {
376 self.shall_support = true;
377 self
378 }
379
380 pub fn supports_modifier(&self, modifier: &str) -> bool {
382 self.modifiers.contains(modifier)
383 }
384
385 pub fn supports_prefix(&self, prefix: &str) -> bool {
387 self.prefixes.contains(prefix)
388 }
389
390 pub fn is_composite(&self) -> bool {
392 self.param_type == SearchParamType::Composite && !self.components.is_empty()
393 }
394
395 pub fn supports_chaining(&self) -> bool {
397 self.chaining.is_some()
398 }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
405#[serde(rename_all = "lowercase")]
406pub enum DatePrecision {
407 Year,
409 Month,
411 Day,
413 Hour,
415 Minute,
417 Second,
419 Millisecond,
421}
422
423impl DatePrecision {
424 pub fn from_date_string(s: &str) -> Self {
426 let base = s.split('+').next().unwrap_or(s);
428 let base = base.split('Z').next().unwrap_or(base);
429
430 match base.len() {
431 4 => DatePrecision::Year,
432 7 => DatePrecision::Month,
433 10 => DatePrecision::Day,
434 13 => DatePrecision::Hour,
435 16 => DatePrecision::Minute,
436 19 => DatePrecision::Second,
437 _ => DatePrecision::Millisecond,
438 }
439 }
440
441 pub fn sql_format(&self) -> &'static str {
443 match self {
444 DatePrecision::Year => "%Y",
445 DatePrecision::Month => "%Y-%m",
446 DatePrecision::Day => "%Y-%m-%d",
447 DatePrecision::Hour => "%Y-%m-%dT%H",
448 DatePrecision::Minute => "%Y-%m-%dT%H:%M",
449 DatePrecision::Second => "%Y-%m-%dT%H:%M:%S",
450 DatePrecision::Millisecond => "%Y-%m-%dT%H:%M:%S%.f",
451 }
452 }
453}
454
455impl std::fmt::Display for DatePrecision {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 match self {
458 DatePrecision::Year => write!(f, "year"),
459 DatePrecision::Month => write!(f, "month"),
460 DatePrecision::Day => write!(f, "day"),
461 DatePrecision::Hour => write!(f, "hour"),
462 DatePrecision::Minute => write!(f, "minute"),
463 DatePrecision::Second => write!(f, "second"),
464 DatePrecision::Millisecond => write!(f, "millisecond"),
465 }
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
471#[serde(rename_all = "camelCase")]
472pub enum SearchStrategy {
473 #[default]
478 PrecomputedIndex,
479
480 QueryTimeEvaluation,
485
486 Hybrid {
490 indexed_params: Vec<String>,
492 },
493}
494
495#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
497#[serde(rename_all = "camelCase")]
498pub enum IndexingMode {
499 #[default]
503 Inline,
504
505 Async,
510
511 HybridAsync {
513 inline_params: Vec<String>,
515 },
516}
517
518#[derive(Debug, Clone, Default, Serialize, Deserialize)]
520pub struct JsonbCapabilities {
521 pub path_extraction: bool,
523 pub array_iteration: bool,
525 pub containment_operator: bool,
527 pub gin_index: bool,
529}
530
531impl JsonbCapabilities {
532 pub fn sqlite() -> Self {
534 Self {
535 path_extraction: true,
536 array_iteration: true,
537 containment_operator: false,
538 gin_index: false,
539 }
540 }
541
542 pub fn postgresql() -> Self {
544 Self {
545 path_extraction: true,
546 array_iteration: true,
547 containment_operator: true,
548 gin_index: true,
549 }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn test_special_search_param() {
559 assert_eq!(SpecialSearchParam::Id.name(), "_id");
560 assert_eq!(
561 SpecialSearchParam::LastUpdated.param_type(),
562 SearchParamType::Date
563 );
564 assert_eq!(SpecialSearchParam::all().len(), 13);
565 }
566
567 #[test]
568 fn test_include_capability() {
569 assert!(IncludeCapability::Include.is_include());
570 assert!(!IncludeCapability::Revinclude.is_include());
571 assert_eq!(
572 IncludeCapability::IncludeIterate.modifier(),
573 Some("iterate")
574 );
575 }
576
577 #[test]
578 fn test_chaining_capability() {
579 assert_eq!(ChainingCapability::MaxDepth(3).max_depth(), Some(3));
580 assert_eq!(ChainingCapability::ForwardChain.max_depth(), None);
581 }
582
583 #[test]
584 fn test_pagination_capability() {
585 assert_eq!(
586 PaginationCapability::MaxPageSize(100).page_size(),
587 Some(100)
588 );
589 assert_eq!(PaginationCapability::Cursor.page_size(), None);
590 }
591
592 #[test]
593 fn test_search_param_full_capability() {
594 let cap = SearchParamFullCapability::new("name", SearchParamType::String)
595 .with_modifiers(vec!["exact", "contains"])
596 .with_definition("http://hl7.org/fhir/SearchParameter/Patient-name");
597
598 assert_eq!(cap.name, "name");
599 assert!(cap.supports_modifier("exact"));
600 assert!(!cap.supports_modifier("above"));
601 assert!(cap.supports_prefix("eq"));
602 }
603
604 #[test]
605 fn test_date_precision() {
606 assert_eq!(DatePrecision::from_date_string("2024"), DatePrecision::Year);
607 assert_eq!(
608 DatePrecision::from_date_string("2024-01"),
609 DatePrecision::Month
610 );
611 assert_eq!(
612 DatePrecision::from_date_string("2024-01-15"),
613 DatePrecision::Day
614 );
615 assert_eq!(
616 DatePrecision::from_date_string("2024-01-15T10:30:00"),
617 DatePrecision::Second
618 );
619 }
620
621 #[test]
622 fn test_composite_component() {
623 let comp = CompositeComponent::new(
624 "http://hl7.org/fhir/SearchParameter/Observation-code",
625 "Observation.code",
626 );
627 assert!(!comp.definition.is_empty());
628 assert!(!comp.expression.is_empty());
629 }
630
631 #[test]
632 fn test_search_strategy_default() {
633 assert_eq!(SearchStrategy::default(), SearchStrategy::PrecomputedIndex);
634 }
635
636 #[test]
637 fn test_indexing_mode_default() {
638 assert_eq!(IndexingMode::default(), IndexingMode::Inline);
639 }
640
641 #[test]
642 fn test_jsonb_capabilities() {
643 let sqlite = JsonbCapabilities::sqlite();
644 assert!(sqlite.path_extraction);
645 assert!(!sqlite.containment_operator);
646
647 let pg = JsonbCapabilities::postgresql();
648 assert!(pg.containment_operator);
649 assert!(pg.gin_index);
650 }
651}