Skip to main content

helios_persistence/types/
search_capabilities.rs

1//! Enhanced search capability types for FHIR search.
2//!
3//! This module provides comprehensive capability enums and structs for declaring
4//! what FHIR search features a backend supports. These types enable:
5//!
6//! - Generating accurate CapabilityStatements
7//! - Validating search queries before execution
8//! - Capability-based query routing in polyglot deployments
9
10use std::collections::HashSet;
11
12use serde::{Deserialize, Serialize};
13
14use super::SearchParamType;
15
16/// Special search parameters that apply across resource types.
17///
18/// These are the FHIR special parameters that have consistent behavior
19/// regardless of resource type.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub enum SpecialSearchParam {
23    /// `_id` - Resource id (without base URL)
24    Id,
25    /// `_lastUpdated` - When the resource was last changed
26    LastUpdated,
27    /// `_tag` - Tags applied to this resource
28    Tag,
29    /// `_profile` - Profiles the resource claims to conform to
30    Profile,
31    /// `_security` - Security labels applied to this resource
32    Security,
33    /// `_text` - Search on narrative content
34    Text,
35    /// `_content` - Search on entire resource content
36    Content,
37    /// `_list` - Search for resources in a List
38    List,
39    /// `_has` - Reverse chained search
40    Has,
41    /// `_type` - Resource type filter (for multi-type searches)
42    Type,
43    /// `_query` - Custom named query
44    Query,
45    /// `_filter` - Filter expression
46    Filter,
47    /// `_source` - Source of resource (meta.source)
48    Source,
49}
50
51impl SpecialSearchParam {
52    /// Returns the parameter name as used in FHIR URLs.
53    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    /// Returns the parameter type for this special parameter.
72    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    /// All defined special parameters.
91    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/// Include/revinclude capability variants.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub enum IncludeCapability {
120    /// `_include` - Forward include of referenced resources
121    Include,
122    /// `_revinclude` - Reverse include of resources that reference results
123    Revinclude,
124    /// `_include:iterate` - Recursive includes
125    IncludeIterate,
126    /// `_revinclude:iterate` - Recursive reverse includes
127    RevincludeIterate,
128    /// `_include=*` - Wildcard includes (all references)
129    IncludeWildcard,
130    /// `_revinclude=*` - Wildcard reverse includes
131    RevincludeWildcard,
132}
133
134impl IncludeCapability {
135    /// Returns the modifier suffix for this capability.
136    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    /// Returns whether this is an include (true) or revinclude (false).
148    pub fn is_include(&self) -> bool {
149        matches!(
150            self,
151            IncludeCapability::Include
152                | IncludeCapability::IncludeIterate
153                | IncludeCapability::IncludeWildcard
154        )
155    }
156}
157
158/// Chaining capability variants.
159#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub enum ChainingCapability {
162    /// Forward chaining (e.g., `Observation?patient.name=Smith`)
163    ForwardChain,
164    /// Reverse chaining via `_has` (e.g., `Patient?_has:Observation:patient:code=xyz`)
165    ReverseChain,
166    /// Maximum chain depth supported
167    MaxDepth(u8),
168}
169
170impl ChainingCapability {
171    /// Returns the maximum depth for MaxDepth variant.
172    pub fn max_depth(&self) -> Option<u8> {
173        match self {
174            ChainingCapability::MaxDepth(d) => Some(*d),
175            _ => None,
176        }
177    }
178}
179
180/// Pagination capability variants.
181#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub enum PaginationCapability {
184    /// `_count` parameter support
185    Count,
186    /// Offset-based pagination (`_offset`)
187    Offset,
188    /// Cursor-based pagination (opaque page tokens)
189    Cursor,
190    /// Maximum page size supported
191    MaxPageSize(u32),
192    /// Default page size when not specified
193    DefaultPageSize(u32),
194}
195
196impl PaginationCapability {
197    /// Returns the page size value for MaxPageSize or DefaultPageSize variants.
198    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/// Result mode capability variants.
209///
210/// These control what data is returned in search results.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub enum ResultModeCapability {
214    /// `_summary` parameter support (any mode)
215    Summary,
216    /// `_summary=true` - Summary elements only
217    SummaryTrue,
218    /// `_summary=text` - Text narrative only
219    SummaryText,
220    /// `_summary=data` - Data elements only (no text)
221    SummaryData,
222    /// `_summary=count` - Count only, no resources
223    SummaryCount,
224    /// `_summary=false` - Full resource (default)
225    SummaryFalse,
226    /// `_elements` parameter support
227    Elements,
228    /// `_total` parameter support (any mode)
229    Total,
230    /// `_total=none` - No total count
231    TotalNone,
232    /// `_total=estimate` - Estimated total count
233    TotalEstimate,
234    /// `_total=accurate` - Accurate total count
235    TotalAccurate,
236    /// `_contained` parameter support
237    Contained,
238    /// `_containedType` parameter support
239    ContainedType,
240}
241
242/// Component of a composite search parameter.
243#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
244pub struct CompositeComponent {
245    /// Definition URL of the component parameter.
246    pub definition: String,
247    /// FHIRPath expression for extracting this component.
248    pub expression: String,
249}
250
251impl CompositeComponent {
252    /// Creates a new composite component.
253    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/// Full capability declaration for a search parameter.
262///
263/// This provides complete information about what features are supported
264/// for a specific search parameter on a specific resource type.
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct SearchParamFullCapability {
267    /// The parameter name (e.g., "name", "identifier").
268    pub name: String,
269
270    /// The parameter type.
271    pub param_type: SearchParamType,
272
273    /// Canonical URL of the SearchParameter definition.
274    pub definition: Option<String>,
275
276    /// Supported modifiers for this parameter.
277    pub modifiers: HashSet<String>,
278
279    /// Supported comparison prefixes for this parameter.
280    pub prefixes: HashSet<String>,
281
282    /// Chaining capability for reference parameters.
283    pub chaining: Option<ChainingCapability>,
284
285    /// Target resource types (for reference parameters).
286    pub target_types: Vec<String>,
287
288    /// Components (for composite parameters).
289    pub components: Vec<CompositeComponent>,
290
291    /// Whether this parameter is required in capability statements.
292    pub shall_support: bool,
293}
294
295impl SearchParamFullCapability {
296    /// Creates a new capability for a search parameter.
297    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    /// Returns default prefixes for a parameter type.
312    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    /// Sets the definition URL.
337    pub fn with_definition(mut self, url: impl Into<String>) -> Self {
338        self.definition = Some(url.into());
339        self
340    }
341
342    /// Adds supported modifiers.
343    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    /// Sets chaining capability.
353    pub fn with_chaining(mut self, chaining: ChainingCapability) -> Self {
354        self.chaining = Some(chaining);
355        self
356    }
357
358    /// Sets target types for reference parameters.
359    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    /// Sets components for composite parameters.
369    pub fn with_components(mut self, components: Vec<CompositeComponent>) -> Self {
370        self.components = components;
371        self
372    }
373
374    /// Marks this parameter as SHALL support.
375    pub fn shall(mut self) -> Self {
376        self.shall_support = true;
377        self
378    }
379
380    /// Returns whether a specific modifier is supported.
381    pub fn supports_modifier(&self, modifier: &str) -> bool {
382        self.modifiers.contains(modifier)
383    }
384
385    /// Returns whether a specific prefix is supported.
386    pub fn supports_prefix(&self, prefix: &str) -> bool {
387        self.prefixes.contains(prefix)
388    }
389
390    /// Returns whether this is a composite parameter.
391    pub fn is_composite(&self) -> bool {
392        self.param_type == SearchParamType::Composite && !self.components.is_empty()
393    }
394
395    /// Returns whether this is a reference parameter with chaining support.
396    pub fn supports_chaining(&self) -> bool {
397        self.chaining.is_some()
398    }
399}
400
401/// Date precision for search parameter values.
402///
403/// Used to track the precision of date values for proper range matching.
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
405#[serde(rename_all = "lowercase")]
406pub enum DatePrecision {
407    /// Year only (e.g., "2024")
408    Year,
409    /// Year and month (e.g., "2024-01")
410    Month,
411    /// Full date (e.g., "2024-01-15")
412    Day,
413    /// Date and time to hours (e.g., "2024-01-15T10")
414    Hour,
415    /// Date and time to minutes (e.g., "2024-01-15T10:30")
416    Minute,
417    /// Date and time to seconds (e.g., "2024-01-15T10:30:00")
418    Second,
419    /// Full precision with milliseconds
420    Millisecond,
421}
422
423impl DatePrecision {
424    /// Parse precision from an ISO date string.
425    pub fn from_date_string(s: &str) -> Self {
426        // Remove timezone suffix for length calculation
427        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    /// Returns the SQL date format for this precision.
442    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/// Search strategy - determines HOW searches are executed.
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
471#[serde(rename_all = "camelCase")]
472pub enum SearchStrategy {
473    /// Pre-computed indexes.
474    /// - Values extracted at write time, stored in search_index table.
475    /// - Fast queries, slower writes.
476    /// - Requires reindexing for new SearchParameters.
477    #[default]
478    PrecomputedIndex,
479
480    /// Query-time JSONB/JSON evaluation.
481    /// - FHIRPath evaluated at query time against stored JSON.
482    /// - Immediate SearchParameter activation, no reindexing needed.
483    /// - Slower queries for complex expressions.
484    QueryTimeEvaluation,
485
486    /// Hybrid: use indexes where available, fallback to JSONB.
487    /// - Best of both worlds.
488    /// - Pre-computed for common params, JSONB for custom/new params.
489    Hybrid {
490        /// Parameter codes that have pre-computed indexes.
491        indexed_params: Vec<String>,
492    },
493}
494
495/// Indexing mode - determines WHEN indexes are updated (for PrecomputedIndex strategy).
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
497#[serde(rename_all = "camelCase")]
498pub enum IndexingMode {
499    /// Synchronous indexing during create/update (default).
500    /// - Resources are searchable immediately.
501    /// - Slightly slower write operations.
502    #[default]
503    Inline,
504
505    /// Asynchronous indexing via event stream (future).
506    /// - Resources eventually searchable.
507    /// - Faster write operations.
508    /// - Requires Kafka infrastructure.
509    Async,
510
511    /// Hybrid: inline for critical params, async for others.
512    HybridAsync {
513        /// Parameter codes to index inline.
514        inline_params: Vec<String>,
515    },
516}
517
518/// JSONB query capabilities for a backend.
519#[derive(Debug, Clone, Default, Serialize, Deserialize)]
520pub struct JsonbCapabilities {
521    /// JSON path extraction (PostgreSQL ->/->>).
522    pub path_extraction: bool,
523    /// JSON array iteration (SQLite json_each, PostgreSQL jsonb_array_elements).
524    pub array_iteration: bool,
525    /// JSON containment operator (PostgreSQL @>).
526    pub containment_operator: bool,
527    /// GIN indexing support.
528    pub gin_index: bool,
529}
530
531impl JsonbCapabilities {
532    /// SQLite capabilities (JSON1 extension).
533    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    /// PostgreSQL capabilities (JSONB).
543    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}