Skip to main content

helios_persistence/types/
search_params.rs

1//! FHIR search parameter types.
2//!
3//! This module defines types for representing FHIR search parameters,
4//! including parameter types, modifiers, and prefixes.
5
6use std::collections::HashMap;
7use std::fmt;
8use std::str::FromStr;
9
10use serde::{Deserialize, Serialize};
11
12/// FHIR search parameter types.
13///
14/// See: https://build.fhir.org/search.html#ptypes
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "lowercase")]
17pub enum SearchParamType {
18    #[default]
19    /// A simple string, like a name or description.
20    String,
21    /// A search against a URI.
22    Uri,
23    /// A search for a number.
24    Number,
25    /// A search for a date, dateTime, or period.
26    Date,
27    /// A quantity, with a number and units.
28    Quantity,
29    /// A code from a code system or value set.
30    Token,
31    /// A reference to another resource.
32    Reference,
33    /// A composite search parameter that combines others.
34    Composite,
35    /// Special search parameters (_id, _lastUpdated, etc.).
36    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/// Search modifiers that can be applied to search parameters.
75///
76/// See: https://build.fhir.org/search.html#modifiers
77#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78#[serde(rename_all = "lowercase")]
79pub enum SearchModifier {
80    /// Exact string match (string parameters).
81    Exact,
82    /// Contains substring (string parameters).
83    Contains,
84    /// Text search (token parameters).
85    Text,
86    /// Negation - exclude matches.
87    Not,
88    /// Match if value is missing.
89    Missing,
90    /// Match codes above in hierarchy (token parameters).
91    Above,
92    /// Match codes below in hierarchy (token parameters).
93    Below,
94    /// Match codes in a value set (token parameters).
95    In,
96    /// Match codes not in a value set (token parameters).
97    NotIn,
98    /// Match on identifier (reference parameters).
99    Identifier,
100    /// Specify reference type (reference parameters).
101    Type(String),
102    /// Match on type (token parameters for polymorphic elements).
103    OfType,
104    /// Match on code only (token parameters).
105    CodeOnly,
106    /// Iterate through results (_include modifier).
107    Iterate,
108    /// Advanced text search with synonyms and linguistic matching (FHIR v6.0.0).
109    ///
110    /// This modifier enables sophisticated text matching that may include:
111    /// - Synonym expansion
112    /// - Linguistic stemming
113    /// - Fuzzy matching
114    ///
115    /// Requires external terminology service integration.
116    TextAdvanced,
117    /// Match on code text/display value (token parameters, FHIR v6.0.0).
118    ///
119    /// Searches the text/display value of a CodeableConcept or Coding
120    /// rather than the code itself.
121    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    /// Parses a modifier string, returning None for unknown modifiers.
149    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                // Check if it's a resource type modifier
168                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    /// Returns true if this modifier is valid for the given parameter type.
178    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,     // Valid for all types
185            SearchModifier::Missing => true, // Valid for all types
186            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, // Only for _include/_revinclude
198            SearchModifier::TextAdvanced => {
199                param_type == SearchParamType::String || param_type == SearchParamType::Token
200            }
201            SearchModifier::CodeText => param_type == SearchParamType::Token,
202        }
203    }
204}
205
206/// Comparison prefixes for search parameters.
207///
208/// See: https://build.fhir.org/search.html#prefix
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
210#[serde(rename_all = "lowercase")]
211pub enum SearchPrefix {
212    /// Equal (default).
213    #[default]
214    Eq,
215    /// Not equal.
216    Ne,
217    /// Greater than.
218    Gt,
219    /// Less than.
220    Lt,
221    /// Greater than or equal.
222    Ge,
223    /// Less than or equal.
224    Le,
225    /// Starts after.
226    Sa,
227    /// Ends before.
228    Eb,
229    /// Approximately equal.
230    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    /// Extracts a prefix from the beginning of a value string.
270    ///
271    /// Returns the prefix and the remaining value.
272    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    /// Returns true if this prefix is valid for the given parameter type.
283    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/// A parsed search parameter with its value.
304#[derive(Debug, Clone, Serialize, Deserialize, Default)]
305pub struct SearchParameter {
306    /// The parameter name (e.g., "name", "identifier").
307    #[serde(default)]
308    pub name: String,
309
310    /// The parameter type.
311    #[serde(default)]
312    pub param_type: SearchParamType,
313
314    /// Modifier, if any.
315    #[serde(default)]
316    pub modifier: Option<SearchModifier>,
317
318    /// The search value(s). Multiple values are ORed.
319    #[serde(default)]
320    pub values: Vec<SearchValue>,
321
322    /// Chained parameters (e.g., patient.name=Smith).
323    #[serde(default)]
324    pub chain: Vec<ChainedParameter>,
325
326    /// Components for composite parameters.
327    /// Each component defines the type and expression for extracting component values.
328    #[serde(default)]
329    pub components: Vec<CompositeSearchComponent>,
330}
331
332/// Component definition for a composite search parameter.
333///
334/// Used when building composite search queries to define how each
335/// component of the composite value should be matched.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct CompositeSearchComponent {
338    /// The parameter type of this component (token, quantity, date, etc.).
339    pub param_type: SearchParamType,
340    /// The parameter name/code for this component.
341    pub param_name: String,
342}
343
344/// A single search value with optional prefix.
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct SearchValue {
347    /// The comparison prefix.
348    pub prefix: SearchPrefix,
349
350    /// The value to search for.
351    pub value: String,
352}
353
354impl SearchValue {
355    /// Creates a new search value with the given prefix and value.
356    pub fn new(prefix: SearchPrefix, value: impl Into<String>) -> Self {
357        Self {
358            prefix,
359            value: value.into(),
360        }
361    }
362
363    /// Creates a search value with the default (eq) prefix.
364    pub fn eq(value: impl Into<String>) -> Self {
365        Self::new(SearchPrefix::Eq, value)
366    }
367
368    /// Parses a value string, extracting any prefix.
369    pub fn parse(s: &str) -> Self {
370        let (prefix, value) = SearchPrefix::extract(s);
371        Self::new(prefix, value)
372    }
373
374    /// Creates a token search value with optional system and code.
375    ///
376    /// Format: `[system]|[code]` or just `[code]`
377    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    /// Creates a token search value with system only (no code).
386    ///
387    /// Format: `[system]|`
388    pub fn token_system_only(system: impl Into<String>) -> Self {
389        Self::eq(format!("{}|", system.into()))
390    }
391
392    /// Creates a boolean search value.
393    pub fn boolean(value: bool) -> Self {
394        Self::eq(value.to_string())
395    }
396
397    /// Creates a string search value (alias for eq).
398    pub fn string(value: impl Into<String>) -> Self {
399        Self::eq(value)
400    }
401
402    /// Creates a search value for :of-type modifier with three-part format.
403    ///
404    /// Format: `[type-system]|[type-code]|[value]`
405    ///
406    /// This is used with the :of-type modifier to search typed identifiers.
407    /// The format specifies the identifier type (system and code) followed
408    /// by the identifier value to match.
409    ///
410    /// # Example
411    ///
412    /// ```ignore
413    /// // Search for SSN identifier with value "123-45-6789"
414    /// SearchValue::of_type(
415    ///     "http://terminology.hl7.org/CodeSystem/v2-0203",
416    ///     "SS",
417    ///     "123-45-6789"
418    /// )
419    /// ```
420    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/// A chained search parameter.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ChainedParameter {
437    /// The reference parameter being chained through.
438    pub reference_param: String,
439
440    /// Optional type modifier on the reference.
441    pub target_type: Option<String>,
442
443    /// The target parameter on the referenced resource.
444    pub target_param: String,
445}
446
447/// A reverse chained parameter (_has).
448///
449/// Supports nested `_has` queries like:
450/// `Patient?_has:Observation:subject:_has:Provenance:target:agent=X`
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct ReverseChainedParameter {
453    /// The resource type that references this resource.
454    pub source_type: String,
455
456    /// The reference parameter on the source type.
457    pub reference_param: String,
458
459    /// The search parameter on the source type.
460    /// For nested `_has`, this may be empty or "_has" indicating nesting.
461    pub search_param: String,
462
463    /// The search value (None if this is a nested `_has` with further chaining).
464    pub value: Option<SearchValue>,
465
466    /// Nested reverse chain for multi-level `_has` queries.
467    pub nested: Option<Box<ReverseChainedParameter>>,
468}
469
470impl ReverseChainedParameter {
471    /// Creates a new terminal (non-nested) reverse chain parameter.
472    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    /// Creates a nested reverse chain parameter.
488    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    /// Returns the depth of nesting (1 for non-nested, 2+ for nested).
503    pub fn depth(&self) -> usize {
504        match &self.nested {
505            Some(inner) => 1 + inner.depth(),
506            None => 1,
507        }
508    }
509
510    /// Returns true if this is a terminal (non-nested) reverse chain.
511    pub fn is_terminal(&self) -> bool {
512        self.nested.is_none()
513    }
514}
515
516/// Configuration for chain query limits.
517#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct ChainConfig {
519    /// Maximum depth for forward chained parameters.
520    /// Default: 4, Maximum: 8
521    pub max_forward_depth: usize,
522
523    /// Maximum depth for reverse chained parameters (_has).
524    /// Default: 4, Maximum: 8
525    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    /// Creates a new chain configuration with specified depths.
539    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    /// Validates that forward chain depth is within limits.
547    pub fn validate_forward_depth(&self, depth: usize) -> bool {
548        depth <= self.max_forward_depth
549    }
550
551    /// Validates that reverse chain depth is within limits.
552    pub fn validate_reverse_depth(&self, depth: usize) -> bool {
553        depth <= self.max_reverse_depth
554    }
555}
556
557/// Include directive for _include and _revinclude.
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct IncludeDirective {
560    /// The type of include.
561    pub include_type: IncludeType,
562
563    /// The source resource type.
564    pub source_type: String,
565
566    /// The search parameter (reference) to follow.
567    pub search_param: String,
568
569    /// Optional target resource type filter.
570    pub target_type: Option<String>,
571
572    /// Whether to iterate (follow includes of included resources).
573    pub iterate: bool,
574}
575
576/// Type of include operation.
577#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
578pub enum IncludeType {
579    /// Forward include (_include).
580    Include,
581    /// Reverse include (_revinclude).
582    Revinclude,
583}
584
585/// Sort direction for _sort parameter.
586#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
587pub enum SortDirection {
588    /// Ascending order.
589    #[default]
590    Ascending,
591    /// Descending order.
592    Descending,
593}
594
595/// A sort directive.
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct SortDirective {
598    /// The parameter to sort by.
599    pub parameter: String,
600    /// The sort direction.
601    pub direction: SortDirection,
602}
603
604impl SortDirective {
605    /// Parses a sort parameter value (e.g., "-date" for descending).
606    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/// A complete search query with all parameters.
622#[derive(Debug, Clone, Default, Serialize, Deserialize)]
623pub struct SearchQuery {
624    /// The resource type being searched.
625    pub resource_type: String,
626
627    /// Standard search parameters.
628    pub parameters: Vec<SearchParameter>,
629
630    /// Reverse chain parameters (_has).
631    pub reverse_chains: Vec<ReverseChainedParameter>,
632
633    /// Include directives.
634    pub includes: Vec<IncludeDirective>,
635
636    /// Sort directives.
637    pub sort: Vec<SortDirective>,
638
639    /// Result count limit (_count).
640    pub count: Option<u32>,
641
642    /// Offset for pagination.
643    pub offset: Option<u32>,
644
645    /// Cursor for keyset pagination.
646    pub cursor: Option<String>,
647
648    /// Whether to include total count (_total).
649    pub total: Option<TotalMode>,
650
651    /// Summary mode (_summary).
652    pub summary: Option<SummaryMode>,
653
654    /// Elements to include (_elements).
655    pub elements: Vec<String>,
656
657    /// Raw query parameters for debugging.
658    pub raw_params: HashMap<String, Vec<String>>,
659}
660
661/// Mode for _total parameter.
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
663#[serde(rename_all = "lowercase")]
664pub enum TotalMode {
665    /// No total.
666    None,
667    /// Estimated total.
668    Estimate,
669    /// Accurate total.
670    Accurate,
671}
672
673/// Mode for _summary parameter.
674#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
675#[serde(rename_all = "lowercase")]
676pub enum SummaryMode {
677    /// Return summary elements only.
678    True,
679    /// Return full resource.
680    False,
681    /// Return text narrative only.
682    Text,
683    /// Return data elements only (no text).
684    Data,
685    /// Return count only.
686    Count,
687}
688
689impl SearchQuery {
690    /// Creates a new search query for the given resource type.
691    pub fn new(resource_type: impl Into<String>) -> Self {
692        Self {
693            resource_type: resource_type.into(),
694            ..Default::default()
695        }
696    }
697
698    /// Adds a search parameter.
699    pub fn with_parameter(mut self, param: SearchParameter) -> Self {
700        self.parameters.push(param);
701        self
702    }
703
704    /// Adds an include directive.
705    pub fn with_include(mut self, include: IncludeDirective) -> Self {
706        self.includes.push(include);
707        self
708    }
709
710    /// Adds a sort directive.
711    pub fn with_sort(mut self, sort: SortDirective) -> Self {
712        self.sort.push(sort);
713        self
714    }
715
716    /// Sets the count limit.
717    pub fn with_count(mut self, count: u32) -> Self {
718        self.count = Some(count);
719        self
720    }
721
722    /// Sets the cursor for keyset pagination.
723    pub fn with_cursor(mut self, cursor: String) -> Self {
724        self.cursor = Some(cursor);
725        self
726    }
727
728    /// Returns true if this query uses any features that require special backend support.
729    pub fn requires_advanced_features(&self) -> bool {
730        // Chained parameters
731        if self.parameters.iter().any(|p| !p.chain.is_empty()) {
732            return true;
733        }
734
735        // Reverse chains
736        if !self.reverse_chains.is_empty() {
737            return true;
738        }
739
740        // Includes
741        if !self.includes.is_empty() {
742            return true;
743        }
744
745        // Terminology modifiers
746        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}