Skip to main content

helios_persistence/core/
capabilities.rs

1//! Storage capabilities and capability statement generation.
2//!
3//! This module defines traits for runtime capability discovery and
4//! generation of FHIR CapabilityStatement fragments.
5
6use std::collections::{HashMap, HashSet};
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::types::{
12    ChainingCapability, IncludeCapability, PaginationCapability, ResultModeCapability,
13    SearchParamFullCapability, SearchParamType, SearchQuery, SpecialSearchParam,
14};
15
16/// Supported FHIR interactions for a resource type.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Interaction {
20    /// read - Read the current state of the resource.
21    Read,
22    /// vread - Read a specific version.
23    Vread,
24    /// update - Update an existing resource.
25    Update,
26    /// patch - Partial update of a resource.
27    Patch,
28    /// delete - Delete a resource.
29    Delete,
30    /// history-instance - Retrieve history for a resource instance.
31    HistoryInstance,
32    /// history-type - Retrieve history for a resource type.
33    HistoryType,
34    /// create - Create a new resource.
35    Create,
36    /// search-type - Search for resources of a type.
37    SearchType,
38}
39
40impl std::fmt::Display for Interaction {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Interaction::Read => write!(f, "read"),
44            Interaction::Vread => write!(f, "vread"),
45            Interaction::Update => write!(f, "update"),
46            Interaction::Patch => write!(f, "patch"),
47            Interaction::Delete => write!(f, "delete"),
48            Interaction::HistoryInstance => write!(f, "history-instance"),
49            Interaction::HistoryType => write!(f, "history-type"),
50            Interaction::Create => write!(f, "create"),
51            Interaction::SearchType => write!(f, "search-type"),
52        }
53    }
54}
55
56/// Supported system-level interactions.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum SystemInteraction {
60    /// transaction - Process a transaction bundle.
61    Transaction,
62    /// batch - Process a batch bundle.
63    Batch,
64    /// history-system - Retrieve history for all resources.
65    HistorySystem,
66    /// search-system - Search across all resource types.
67    SearchSystem,
68}
69
70impl std::fmt::Display for SystemInteraction {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            SystemInteraction::Transaction => write!(f, "transaction"),
74            SystemInteraction::Batch => write!(f, "batch"),
75            SystemInteraction::HistorySystem => write!(f, "history-system"),
76            SystemInteraction::SearchSystem => write!(f, "search-system"),
77        }
78    }
79}
80
81/// Information about a supported search parameter.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SearchParamCapability {
84    /// The parameter name.
85    pub name: String,
86    /// The parameter type.
87    pub param_type: SearchParamType,
88    /// Supported modifiers for this parameter.
89    pub modifiers: Vec<String>,
90    /// Whether chaining is supported for reference parameters.
91    pub supports_chaining: bool,
92    /// Documentation for this parameter.
93    pub documentation: Option<String>,
94}
95
96impl SearchParamCapability {
97    /// Creates a new search parameter capability.
98    pub fn new(name: impl Into<String>, param_type: SearchParamType) -> Self {
99        Self {
100            name: name.into(),
101            param_type,
102            modifiers: Vec::new(),
103            supports_chaining: false,
104            documentation: None,
105        }
106    }
107
108    /// Adds supported modifiers.
109    pub fn with_modifiers(mut self, modifiers: Vec<&str>) -> Self {
110        self.modifiers = modifiers.into_iter().map(String::from).collect();
111        self
112    }
113
114    /// Enables chaining support.
115    pub fn with_chaining(mut self) -> Self {
116        self.supports_chaining = true;
117        self
118    }
119
120    /// Adds documentation.
121    pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
122        self.documentation = Some(doc.into());
123        self
124    }
125}
126
127/// Capabilities for a specific resource type.
128#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129pub struct ResourceCapabilities {
130    /// The resource type.
131    pub resource_type: String,
132    /// Supported interactions.
133    pub interactions: HashSet<Interaction>,
134    /// Supported search parameters.
135    pub search_params: Vec<SearchParamCapability>,
136    /// Whether _include is supported.
137    pub supports_include: bool,
138    /// Whether _revinclude is supported.
139    pub supports_revinclude: bool,
140    /// Supported _include targets.
141    pub include_targets: Vec<String>,
142    /// Supported _revinclude targets.
143    pub revinclude_targets: Vec<String>,
144    /// Whether conditional create is supported.
145    pub conditional_create: bool,
146    /// Whether conditional update is supported.
147    pub conditional_update: bool,
148    /// Whether conditional delete is supported.
149    pub conditional_delete: bool,
150    /// Additional documentation.
151    pub documentation: Option<String>,
152}
153
154impl ResourceCapabilities {
155    /// Creates capabilities for a resource type.
156    pub fn new(resource_type: impl Into<String>) -> Self {
157        Self {
158            resource_type: resource_type.into(),
159            ..Default::default()
160        }
161    }
162
163    /// Adds supported interactions.
164    pub fn with_interactions(
165        mut self,
166        interactions: impl IntoIterator<Item = Interaction>,
167    ) -> Self {
168        self.interactions.extend(interactions);
169        self
170    }
171
172    /// Adds all CRUD interactions.
173    pub fn with_crud(mut self) -> Self {
174        self.interactions.insert(Interaction::Read);
175        self.interactions.insert(Interaction::Create);
176        self.interactions.insert(Interaction::Update);
177        self.interactions.insert(Interaction::Delete);
178        self
179    }
180
181    /// Adds version support.
182    pub fn with_versioning(mut self) -> Self {
183        self.interactions.insert(Interaction::Vread);
184        self.interactions.insert(Interaction::HistoryInstance);
185        self
186    }
187
188    /// Adds search support.
189    pub fn with_search(mut self, params: Vec<SearchParamCapability>) -> Self {
190        self.interactions.insert(Interaction::SearchType);
191        self.search_params = params;
192        self
193    }
194
195    /// Enables _include support.
196    pub fn with_include(mut self, targets: Vec<&str>) -> Self {
197        self.supports_include = true;
198        self.include_targets = targets.into_iter().map(String::from).collect();
199        self
200    }
201
202    /// Enables _revinclude support.
203    pub fn with_revinclude(mut self, targets: Vec<&str>) -> Self {
204        self.supports_revinclude = true;
205        self.revinclude_targets = targets.into_iter().map(String::from).collect();
206        self
207    }
208
209    /// Enables conditional operations.
210    pub fn with_conditional_ops(mut self) -> Self {
211        self.conditional_create = true;
212        self.conditional_update = true;
213        self.conditional_delete = true;
214        self
215    }
216}
217
218/// Overall storage capabilities.
219#[derive(Debug, Clone, Default, Serialize, Deserialize)]
220pub struct StorageCapabilities {
221    /// Capabilities by resource type.
222    pub resources: HashMap<String, ResourceCapabilities>,
223    /// Supported system-level interactions.
224    pub system_interactions: HashSet<SystemInteraction>,
225    /// Whether system-level history is supported.
226    pub supports_system_history: bool,
227    /// Whether system-level search is supported.
228    pub supports_system_search: bool,
229    /// Supported sort parameters.
230    pub supported_sorts: Vec<String>,
231    /// Whether total counts are supported.
232    pub supports_total: bool,
233    /// Maximum page size.
234    pub max_page_size: Option<u32>,
235    /// Default page size.
236    pub default_page_size: u32,
237    /// Backend name.
238    pub backend_name: String,
239    /// Backend version.
240    pub backend_version: Option<String>,
241}
242
243impl StorageCapabilities {
244    /// Creates new storage capabilities.
245    pub fn new(backend_name: impl Into<String>) -> Self {
246        Self {
247            backend_name: backend_name.into(),
248            default_page_size: 20,
249            ..Default::default()
250        }
251    }
252
253    /// Adds resource capabilities.
254    pub fn with_resource(mut self, caps: ResourceCapabilities) -> Self {
255        self.resources.insert(caps.resource_type.clone(), caps);
256        self
257    }
258
259    /// Adds system interactions.
260    pub fn with_system_interactions(
261        mut self,
262        interactions: impl IntoIterator<Item = SystemInteraction>,
263    ) -> Self {
264        self.system_interactions.extend(interactions);
265        self
266    }
267
268    /// Enables system history.
269    pub fn with_system_history(mut self) -> Self {
270        self.supports_system_history = true;
271        self.system_interactions
272            .insert(SystemInteraction::HistorySystem);
273        self
274    }
275
276    /// Enables system search.
277    pub fn with_system_search(mut self) -> Self {
278        self.supports_system_search = true;
279        self.system_interactions
280            .insert(SystemInteraction::SearchSystem);
281        self
282    }
283
284    /// Enables transaction support.
285    pub fn with_transactions(mut self) -> Self {
286        self.system_interactions
287            .insert(SystemInteraction::Transaction);
288        self.system_interactions.insert(SystemInteraction::Batch);
289        self
290    }
291
292    /// Sets pagination limits.
293    pub fn with_pagination(mut self, default: u32, max: Option<u32>) -> Self {
294        self.default_page_size = default;
295        self.max_page_size = max;
296        self
297    }
298
299    /// Adds supported sort parameters.
300    pub fn with_sorts(mut self, sorts: Vec<&str>) -> Self {
301        self.supported_sorts = sorts.into_iter().map(String::from).collect();
302        self
303    }
304
305    /// Enables total count support.
306    pub fn with_total_support(mut self) -> Self {
307        self.supports_total = true;
308        self
309    }
310
311    /// Generates a FHIR CapabilityStatement rest resource for this storage.
312    pub fn to_capability_rest(&self) -> Value {
313        let mut resources = Vec::new();
314
315        for caps in self.resources.values() {
316            let mut resource = serde_json::json!({
317                "type": caps.resource_type,
318                "interaction": caps.interactions.iter().map(|i| {
319                    serde_json::json!({"code": i.to_string()})
320                }).collect::<Vec<_>>(),
321            });
322
323            if !caps.search_params.is_empty() {
324                resource["searchParam"] = serde_json::json!(
325                    caps.search_params
326                        .iter()
327                        .map(|sp| {
328                            serde_json::json!({
329                                "name": sp.name,
330                                "type": sp.param_type.to_string(),
331                            })
332                        })
333                        .collect::<Vec<_>>()
334                );
335            }
336
337            if caps.conditional_create {
338                resource["conditionalCreate"] = serde_json::json!(true);
339            }
340            if caps.conditional_update {
341                resource["conditionalUpdate"] = serde_json::json!(true);
342            }
343            if caps.conditional_delete {
344                resource["conditionalDelete"] = serde_json::json!("single");
345            }
346
347            resources.push(resource);
348        }
349
350        let mut rest = serde_json::json!({
351            "mode": "server",
352            "resource": resources,
353        });
354
355        if !self.system_interactions.is_empty() {
356            rest["interaction"] = serde_json::json!(
357                self.system_interactions
358                    .iter()
359                    .map(|i| { serde_json::json!({"code": i.to_string()}) })
360                    .collect::<Vec<_>>()
361            );
362        }
363
364        rest
365    }
366}
367
368/// Trait for storage backends to declare their capabilities.
369pub trait CapabilityProvider {
370    /// Returns the capabilities of this storage backend.
371    fn capabilities(&self) -> StorageCapabilities;
372
373    /// Checks if a specific resource type interaction is supported.
374    fn supports_interaction(&self, resource_type: &str, interaction: Interaction) -> bool {
375        self.capabilities()
376            .resources
377            .get(resource_type)
378            .map(|r| r.interactions.contains(&interaction))
379            .unwrap_or(false)
380    }
381
382    /// Checks if a system interaction is supported.
383    fn supports_system_interaction(&self, interaction: SystemInteraction) -> bool {
384        self.capabilities()
385            .system_interactions
386            .contains(&interaction)
387    }
388
389    /// Gets the capabilities for a specific resource type.
390    fn resource_capabilities(&self, resource_type: &str) -> Option<ResourceCapabilities> {
391        self.capabilities().resources.get(resource_type).cloned()
392    }
393}
394
395// ============================================================================
396// Enhanced Search Capabilities
397// ============================================================================
398
399/// Comprehensive search capabilities for a resource type.
400///
401/// This provides detailed information about what search features are supported
402/// for a specific resource type, including all parameters, modifiers, and
403/// special capabilities.
404#[derive(Debug, Clone, Default, Serialize, Deserialize)]
405pub struct ResourceSearchCapabilities {
406    /// The resource type these capabilities apply to.
407    pub resource_type: String,
408
409    /// Full capability information for each search parameter.
410    pub search_params: Vec<SearchParamFullCapability>,
411
412    /// Supported special parameters (_id, _lastUpdated, etc.).
413    pub special_params: HashSet<SpecialSearchParam>,
414
415    /// Include/revinclude capabilities.
416    pub include_capabilities: HashSet<IncludeCapability>,
417
418    /// Chaining capabilities.
419    pub chaining_capabilities: HashSet<ChainingCapability>,
420
421    /// Pagination capabilities.
422    pub pagination_capabilities: HashSet<PaginationCapability>,
423
424    /// Result mode capabilities (_summary, _elements, _total).
425    pub result_mode_capabilities: HashSet<ResultModeCapability>,
426}
427
428impl ResourceSearchCapabilities {
429    /// Creates new search capabilities for a resource type.
430    pub fn new(resource_type: impl Into<String>) -> Self {
431        Self {
432            resource_type: resource_type.into(),
433            ..Default::default()
434        }
435    }
436
437    /// Adds a search parameter capability.
438    pub fn with_param(mut self, param: SearchParamFullCapability) -> Self {
439        self.search_params.push(param);
440        self
441    }
442
443    /// Adds multiple search parameter capabilities.
444    pub fn with_param_list(mut self, params: Vec<SearchParamFullCapability>) -> Self {
445        self.search_params.extend(params);
446        self
447    }
448
449    /// Adds special parameter support.
450    pub fn with_special_params<I>(mut self, params: I) -> Self
451    where
452        I: IntoIterator<Item = SpecialSearchParam>,
453    {
454        self.special_params.extend(params);
455        self
456    }
457
458    /// Adds include capabilities.
459    pub fn with_include_capabilities<I>(mut self, caps: I) -> Self
460    where
461        I: IntoIterator<Item = IncludeCapability>,
462    {
463        self.include_capabilities.extend(caps);
464        self
465    }
466
467    /// Adds chaining capabilities.
468    pub fn with_chaining_capabilities<I>(mut self, caps: I) -> Self
469    where
470        I: IntoIterator<Item = ChainingCapability>,
471    {
472        self.chaining_capabilities.extend(caps);
473        self
474    }
475
476    /// Adds pagination capabilities.
477    pub fn with_pagination_capabilities<I>(mut self, caps: I) -> Self
478    where
479        I: IntoIterator<Item = PaginationCapability>,
480    {
481        self.pagination_capabilities.extend(caps);
482        self
483    }
484
485    /// Adds result mode capabilities.
486    pub fn with_result_mode_capabilities<I>(mut self, caps: I) -> Self
487    where
488        I: IntoIterator<Item = ResultModeCapability>,
489    {
490        self.result_mode_capabilities.extend(caps);
491        self
492    }
493
494    /// Returns the capability for a specific parameter by name.
495    pub fn get_param(&self, name: &str) -> Option<&SearchParamFullCapability> {
496        self.search_params.iter().find(|p| p.name == name)
497    }
498
499    /// Returns whether a special parameter is supported.
500    pub fn supports_special(&self, param: SpecialSearchParam) -> bool {
501        self.special_params.contains(&param)
502    }
503
504    /// Returns whether a specific include capability is supported.
505    pub fn supports_include(&self, cap: IncludeCapability) -> bool {
506        self.include_capabilities.contains(&cap)
507    }
508
509    /// Returns whether chaining is supported.
510    pub fn supports_chaining(&self) -> bool {
511        self.chaining_capabilities
512            .contains(&ChainingCapability::ForwardChain)
513    }
514
515    /// Returns whether reverse chaining (_has) is supported.
516    pub fn supports_reverse_chaining(&self) -> bool {
517        self.chaining_capabilities
518            .contains(&ChainingCapability::ReverseChain)
519    }
520}
521
522/// Global search capabilities across all resource types.
523#[derive(Debug, Clone, Default, Serialize, Deserialize)]
524pub struct GlobalSearchCapabilities {
525    /// Common special parameters supported for all types.
526    pub common_special_params: HashSet<SpecialSearchParam>,
527
528    /// Common include capabilities for all types.
529    pub common_include_capabilities: HashSet<IncludeCapability>,
530
531    /// Common pagination capabilities.
532    pub common_pagination_capabilities: HashSet<PaginationCapability>,
533
534    /// Common result mode capabilities.
535    pub common_result_mode_capabilities: HashSet<ResultModeCapability>,
536
537    /// Maximum chain depth supported.
538    pub max_chain_depth: Option<u8>,
539
540    /// Whether system-level search is supported.
541    pub supports_system_search: bool,
542
543    /// Supported common sort parameters (_id, _lastUpdated).
544    pub common_sort_params: Vec<String>,
545}
546
547impl GlobalSearchCapabilities {
548    /// Creates new global search capabilities.
549    pub fn new() -> Self {
550        Self::default()
551    }
552
553    /// Adds common special parameters.
554    pub fn with_special_params<I>(mut self, params: I) -> Self
555    where
556        I: IntoIterator<Item = SpecialSearchParam>,
557    {
558        self.common_special_params.extend(params);
559        self
560    }
561
562    /// Adds common pagination capabilities.
563    pub fn with_pagination<I>(mut self, caps: I) -> Self
564    where
565        I: IntoIterator<Item = PaginationCapability>,
566    {
567        self.common_pagination_capabilities.extend(caps);
568        self
569    }
570
571    /// Sets max chain depth.
572    pub fn with_max_chain_depth(mut self, depth: u8) -> Self {
573        self.max_chain_depth = Some(depth);
574        self
575    }
576
577    /// Enables system search.
578    pub fn with_system_search(mut self) -> Self {
579        self.supports_system_search = true;
580        self
581    }
582}
583
584/// Error returned when a search query uses unsupported features.
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct UnsupportedSearchFeature {
587    /// Type of unsupported feature.
588    pub feature_type: UnsupportedFeatureType,
589    /// Description of the unsupported feature.
590    pub description: String,
591    /// The parameter or feature that caused the error.
592    pub parameter: Option<String>,
593    /// Suggested alternative, if any.
594    pub suggestion: Option<String>,
595}
596
597/// Types of unsupported search features.
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
599#[serde(rename_all = "camelCase")]
600pub enum UnsupportedFeatureType {
601    /// Parameter not defined for this resource type.
602    UnknownParameter,
603    /// Modifier not supported for this parameter.
604    UnsupportedModifier,
605    /// Prefix not supported for this parameter type.
606    UnsupportedPrefix,
607    /// Chaining not supported.
608    UnsupportedChaining,
609    /// Reverse chaining (_has) not supported.
610    UnsupportedReverseChaining,
611    /// Include/revinclude not supported.
612    UnsupportedInclude,
613    /// Composite parameter not supported.
614    UnsupportedComposite,
615    /// Special parameter not supported.
616    UnsupportedSpecialParameter,
617    /// Result mode not supported.
618    UnsupportedResultMode,
619    /// Pagination mode not supported.
620    UnsupportedPagination,
621}
622
623impl UnsupportedSearchFeature {
624    /// Creates a new unsupported feature error.
625    pub fn new(feature_type: UnsupportedFeatureType, description: impl Into<String>) -> Self {
626        Self {
627            feature_type,
628            description: description.into(),
629            parameter: None,
630            suggestion: None,
631        }
632    }
633
634    /// Sets the parameter that caused the error.
635    pub fn with_parameter(mut self, param: impl Into<String>) -> Self {
636        self.parameter = Some(param.into());
637        self
638    }
639
640    /// Sets a suggestion for the user.
641    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
642        self.suggestion = Some(suggestion.into());
643        self
644    }
645
646    /// Creates an unknown parameter error.
647    pub fn unknown_parameter(resource_type: &str, param: &str) -> Self {
648        Self::new(
649            UnsupportedFeatureType::UnknownParameter,
650            format!(
651                "Parameter '{}' is not defined for resource type '{}'",
652                param, resource_type
653            ),
654        )
655        .with_parameter(param)
656    }
657
658    /// Creates an unsupported modifier error.
659    pub fn unsupported_modifier(param: &str, modifier: &str) -> Self {
660        Self::new(
661            UnsupportedFeatureType::UnsupportedModifier,
662            format!(
663                "Modifier '{}' is not supported for parameter '{}'",
664                modifier, param
665            ),
666        )
667        .with_parameter(format!("{}:{}", param, modifier))
668    }
669
670    /// Creates an unsupported prefix error.
671    pub fn unsupported_prefix(param: &str, prefix: &str) -> Self {
672        Self::new(
673            UnsupportedFeatureType::UnsupportedPrefix,
674            format!(
675                "Prefix '{}' is not supported for parameter '{}'",
676                prefix, param
677            ),
678        )
679        .with_parameter(param)
680    }
681}
682
683impl std::fmt::Display for UnsupportedSearchFeature {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        write!(f, "{}", self.description)?;
686        if let Some(ref suggestion) = self.suggestion {
687            write!(f, " ({})", suggestion)?;
688        }
689        Ok(())
690    }
691}
692
693impl std::error::Error for UnsupportedSearchFeature {}
694
695/// Trait for providing detailed search capabilities.
696///
697/// This trait extends basic capability reporting with detailed search
698/// capability information and query validation.
699pub trait SearchCapabilityProvider: Send + Sync {
700    /// Returns detailed search capabilities for a resource type.
701    ///
702    /// Returns `None` if the resource type is not supported.
703    fn resource_search_capabilities(
704        &self,
705        resource_type: &str,
706    ) -> Option<ResourceSearchCapabilities>;
707
708    /// Returns global search capabilities that apply to all resource types.
709    fn global_search_capabilities(&self) -> GlobalSearchCapabilities;
710
711    /// Validates a search query against the backend's capabilities.
712    ///
713    /// Returns `Ok(())` if the query is fully supported, or an error
714    /// describing the first unsupported feature encountered.
715    fn validate_search_query(&self, query: &SearchQuery) -> Result<(), UnsupportedSearchFeature> {
716        let resource_type = &query.resource_type;
717        let caps = self
718            .resource_search_capabilities(resource_type)
719            .ok_or_else(|| {
720                UnsupportedSearchFeature::new(
721                    UnsupportedFeatureType::UnknownParameter,
722                    format!("Resource type '{}' is not supported", resource_type),
723                )
724            })?;
725
726        // Validate each parameter
727        for param in &query.parameters {
728            // Check if parameter is defined
729            let param_cap = caps.get_param(&param.name).ok_or_else(|| {
730                UnsupportedSearchFeature::unknown_parameter(resource_type, &param.name)
731            })?;
732
733            // Check modifier if present
734            if let Some(ref modifier) = param.modifier {
735                let modifier_str = modifier.to_string();
736                if !param_cap.supports_modifier(&modifier_str) {
737                    return Err(UnsupportedSearchFeature::unsupported_modifier(
738                        &param.name,
739                        &modifier_str,
740                    ));
741                }
742            }
743
744            // Check prefixes
745            for value in &param.values {
746                let prefix_str = value.prefix.to_string();
747                if !param_cap.supports_prefix(&prefix_str) {
748                    return Err(UnsupportedSearchFeature::unsupported_prefix(
749                        &param.name,
750                        &prefix_str,
751                    ));
752                }
753            }
754
755            // Check chaining
756            if !param.chain.is_empty() && !caps.supports_chaining() {
757                return Err(UnsupportedSearchFeature::new(
758                    UnsupportedFeatureType::UnsupportedChaining,
759                    "Chained search parameters are not supported",
760                ));
761            }
762        }
763
764        // Check reverse chaining
765        if !query.reverse_chains.is_empty() && !caps.supports_reverse_chaining() {
766            return Err(UnsupportedSearchFeature::new(
767                UnsupportedFeatureType::UnsupportedReverseChaining,
768                "_has (reverse chaining) is not supported",
769            ));
770        }
771
772        // Check includes
773        for include in &query.includes {
774            let include_cap = if include.include_type == crate::types::IncludeType::Include {
775                if include.iterate {
776                    IncludeCapability::IncludeIterate
777                } else {
778                    IncludeCapability::Include
779                }
780            } else if include.iterate {
781                IncludeCapability::RevincludeIterate
782            } else {
783                IncludeCapability::Revinclude
784            };
785
786            if !caps.supports_include(include_cap) {
787                return Err(UnsupportedSearchFeature::new(
788                    UnsupportedFeatureType::UnsupportedInclude,
789                    format!("{:?} is not supported", include_cap),
790                ));
791            }
792        }
793
794        Ok(())
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801
802    #[test]
803    fn test_interaction_display() {
804        assert_eq!(Interaction::Read.to_string(), "read");
805        assert_eq!(Interaction::HistoryInstance.to_string(), "history-instance");
806    }
807
808    #[test]
809    fn test_system_interaction_display() {
810        assert_eq!(SystemInteraction::Transaction.to_string(), "transaction");
811        assert_eq!(
812            SystemInteraction::HistorySystem.to_string(),
813            "history-system"
814        );
815    }
816
817    #[test]
818    fn test_search_param_capability() {
819        let cap = SearchParamCapability::new("name", SearchParamType::String)
820            .with_modifiers(vec!["exact", "contains"])
821            .with_documentation("Search by patient name");
822
823        assert_eq!(cap.name, "name");
824        assert_eq!(cap.modifiers.len(), 2);
825        assert!(cap.documentation.is_some());
826    }
827
828    #[test]
829    fn test_resource_capabilities() {
830        let caps = ResourceCapabilities::new("Patient")
831            .with_crud()
832            .with_versioning()
833            .with_conditional_ops();
834
835        assert!(caps.interactions.contains(&Interaction::Read));
836        assert!(caps.interactions.contains(&Interaction::Create));
837        assert!(caps.interactions.contains(&Interaction::Vread));
838        assert!(caps.conditional_create);
839    }
840
841    #[test]
842    fn test_storage_capabilities() {
843        let patient_caps = ResourceCapabilities::new("Patient")
844            .with_crud()
845            .with_search(vec![
846                SearchParamCapability::new("name", SearchParamType::String),
847                SearchParamCapability::new("identifier", SearchParamType::Token),
848            ]);
849
850        let caps = StorageCapabilities::new("sqlite")
851            .with_resource(patient_caps)
852            .with_transactions()
853            .with_pagination(20, Some(100));
854
855        assert!(caps.resources.contains_key("Patient"));
856        assert!(
857            caps.system_interactions
858                .contains(&SystemInteraction::Transaction)
859        );
860        assert_eq!(caps.default_page_size, 20);
861        assert_eq!(caps.max_page_size, Some(100));
862    }
863
864    #[test]
865    fn test_to_capability_rest() {
866        let caps = StorageCapabilities::new("test")
867            .with_resource(ResourceCapabilities::new("Patient").with_crud())
868            .with_transactions();
869
870        let rest = caps.to_capability_rest();
871        assert_eq!(rest["mode"], "server");
872        assert!(rest["resource"].is_array());
873        assert!(rest["interaction"].is_array());
874    }
875
876    // =========================================================================
877    // Enhanced Search Capabilities Tests
878    // =========================================================================
879
880    #[test]
881    fn test_resource_search_capabilities() {
882        let caps = ResourceSearchCapabilities::new("Patient")
883            .with_param(SearchParamFullCapability::new(
884                "name",
885                SearchParamType::String,
886            ))
887            .with_special_params(vec![
888                SpecialSearchParam::Id,
889                SpecialSearchParam::LastUpdated,
890            ])
891            .with_include_capabilities(vec![IncludeCapability::Include]);
892
893        assert_eq!(caps.resource_type, "Patient");
894        assert!(caps.get_param("name").is_some());
895        assert!(caps.supports_special(SpecialSearchParam::Id));
896        assert!(caps.supports_include(IncludeCapability::Include));
897    }
898
899    #[test]
900    fn test_global_search_capabilities() {
901        let global = GlobalSearchCapabilities::new()
902            .with_special_params(vec![SpecialSearchParam::Id])
903            .with_max_chain_depth(3)
904            .with_system_search();
905
906        assert!(
907            global
908                .common_special_params
909                .contains(&SpecialSearchParam::Id)
910        );
911        assert_eq!(global.max_chain_depth, Some(3));
912        assert!(global.supports_system_search);
913    }
914
915    #[test]
916    fn test_unsupported_search_feature() {
917        let err = UnsupportedSearchFeature::unknown_parameter("Patient", "unknown");
918        assert_eq!(err.feature_type, UnsupportedFeatureType::UnknownParameter);
919        assert!(err.parameter.is_some());
920        assert!(err.to_string().contains("unknown"));
921
922        let err2 = UnsupportedSearchFeature::unsupported_modifier("name", "phonetic");
923        assert_eq!(
924            err2.feature_type,
925            UnsupportedFeatureType::UnsupportedModifier
926        );
927    }
928
929    #[test]
930    fn test_search_capabilities_chaining() {
931        let caps = ResourceSearchCapabilities::new("Observation")
932            .with_chaining_capabilities(vec![ChainingCapability::ForwardChain]);
933
934        assert!(caps.supports_chaining());
935        assert!(!caps.supports_reverse_chaining());
936    }
937}