1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Interaction {
20 Read,
22 Vread,
24 Update,
26 Patch,
28 Delete,
30 HistoryInstance,
32 HistoryType,
34 Create,
36 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum SystemInteraction {
60 Transaction,
62 Batch,
64 HistorySystem,
66 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#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SearchParamCapability {
84 pub name: String,
86 pub param_type: SearchParamType,
88 pub modifiers: Vec<String>,
90 pub supports_chaining: bool,
92 pub documentation: Option<String>,
94}
95
96impl SearchParamCapability {
97 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 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 pub fn with_chaining(mut self) -> Self {
116 self.supports_chaining = true;
117 self
118 }
119
120 pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
122 self.documentation = Some(doc.into());
123 self
124 }
125}
126
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129pub struct ResourceCapabilities {
130 pub resource_type: String,
132 pub interactions: HashSet<Interaction>,
134 pub search_params: Vec<SearchParamCapability>,
136 pub supports_include: bool,
138 pub supports_revinclude: bool,
140 pub include_targets: Vec<String>,
142 pub revinclude_targets: Vec<String>,
144 pub conditional_create: bool,
146 pub conditional_update: bool,
148 pub conditional_delete: bool,
150 pub documentation: Option<String>,
152}
153
154impl ResourceCapabilities {
155 pub fn new(resource_type: impl Into<String>) -> Self {
157 Self {
158 resource_type: resource_type.into(),
159 ..Default::default()
160 }
161 }
162
163 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 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 pub fn with_versioning(mut self) -> Self {
183 self.interactions.insert(Interaction::Vread);
184 self.interactions.insert(Interaction::HistoryInstance);
185 self
186 }
187
188 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 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 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
220pub struct StorageCapabilities {
221 pub resources: HashMap<String, ResourceCapabilities>,
223 pub system_interactions: HashSet<SystemInteraction>,
225 pub supports_system_history: bool,
227 pub supports_system_search: bool,
229 pub supported_sorts: Vec<String>,
231 pub supports_total: bool,
233 pub max_page_size: Option<u32>,
235 pub default_page_size: u32,
237 pub backend_name: String,
239 pub backend_version: Option<String>,
241}
242
243impl StorageCapabilities {
244 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 pub fn with_resource(mut self, caps: ResourceCapabilities) -> Self {
255 self.resources.insert(caps.resource_type.clone(), caps);
256 self
257 }
258
259 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 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 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 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 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 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 pub fn with_total_support(mut self) -> Self {
307 self.supports_total = true;
308 self
309 }
310
311 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
368pub trait CapabilityProvider {
370 fn capabilities(&self) -> StorageCapabilities;
372
373 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 fn supports_system_interaction(&self, interaction: SystemInteraction) -> bool {
384 self.capabilities()
385 .system_interactions
386 .contains(&interaction)
387 }
388
389 fn resource_capabilities(&self, resource_type: &str) -> Option<ResourceCapabilities> {
391 self.capabilities().resources.get(resource_type).cloned()
392 }
393}
394
395#[derive(Debug, Clone, Default, Serialize, Deserialize)]
405pub struct ResourceSearchCapabilities {
406 pub resource_type: String,
408
409 pub search_params: Vec<SearchParamFullCapability>,
411
412 pub special_params: HashSet<SpecialSearchParam>,
414
415 pub include_capabilities: HashSet<IncludeCapability>,
417
418 pub chaining_capabilities: HashSet<ChainingCapability>,
420
421 pub pagination_capabilities: HashSet<PaginationCapability>,
423
424 pub result_mode_capabilities: HashSet<ResultModeCapability>,
426}
427
428impl ResourceSearchCapabilities {
429 pub fn new(resource_type: impl Into<String>) -> Self {
431 Self {
432 resource_type: resource_type.into(),
433 ..Default::default()
434 }
435 }
436
437 pub fn with_param(mut self, param: SearchParamFullCapability) -> Self {
439 self.search_params.push(param);
440 self
441 }
442
443 pub fn with_param_list(mut self, params: Vec<SearchParamFullCapability>) -> Self {
445 self.search_params.extend(params);
446 self
447 }
448
449 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 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 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 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 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 pub fn get_param(&self, name: &str) -> Option<&SearchParamFullCapability> {
496 self.search_params.iter().find(|p| p.name == name)
497 }
498
499 pub fn supports_special(&self, param: SpecialSearchParam) -> bool {
501 self.special_params.contains(¶m)
502 }
503
504 pub fn supports_include(&self, cap: IncludeCapability) -> bool {
506 self.include_capabilities.contains(&cap)
507 }
508
509 pub fn supports_chaining(&self) -> bool {
511 self.chaining_capabilities
512 .contains(&ChainingCapability::ForwardChain)
513 }
514
515 pub fn supports_reverse_chaining(&self) -> bool {
517 self.chaining_capabilities
518 .contains(&ChainingCapability::ReverseChain)
519 }
520}
521
522#[derive(Debug, Clone, Default, Serialize, Deserialize)]
524pub struct GlobalSearchCapabilities {
525 pub common_special_params: HashSet<SpecialSearchParam>,
527
528 pub common_include_capabilities: HashSet<IncludeCapability>,
530
531 pub common_pagination_capabilities: HashSet<PaginationCapability>,
533
534 pub common_result_mode_capabilities: HashSet<ResultModeCapability>,
536
537 pub max_chain_depth: Option<u8>,
539
540 pub supports_system_search: bool,
542
543 pub common_sort_params: Vec<String>,
545}
546
547impl GlobalSearchCapabilities {
548 pub fn new() -> Self {
550 Self::default()
551 }
552
553 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 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 pub fn with_max_chain_depth(mut self, depth: u8) -> Self {
573 self.max_chain_depth = Some(depth);
574 self
575 }
576
577 pub fn with_system_search(mut self) -> Self {
579 self.supports_system_search = true;
580 self
581 }
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct UnsupportedSearchFeature {
587 pub feature_type: UnsupportedFeatureType,
589 pub description: String,
591 pub parameter: Option<String>,
593 pub suggestion: Option<String>,
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
599#[serde(rename_all = "camelCase")]
600pub enum UnsupportedFeatureType {
601 UnknownParameter,
603 UnsupportedModifier,
605 UnsupportedPrefix,
607 UnsupportedChaining,
609 UnsupportedReverseChaining,
611 UnsupportedInclude,
613 UnsupportedComposite,
615 UnsupportedSpecialParameter,
617 UnsupportedResultMode,
619 UnsupportedPagination,
621}
622
623impl UnsupportedSearchFeature {
624 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 pub fn with_parameter(mut self, param: impl Into<String>) -> Self {
636 self.parameter = Some(param.into());
637 self
638 }
639
640 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
642 self.suggestion = Some(suggestion.into());
643 self
644 }
645
646 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 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 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
695pub trait SearchCapabilityProvider: Send + Sync {
700 fn resource_search_capabilities(
704 &self,
705 resource_type: &str,
706 ) -> Option<ResourceSearchCapabilities>;
707
708 fn global_search_capabilities(&self) -> GlobalSearchCapabilities;
710
711 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 for param in &query.parameters {
728 let param_cap = caps.get_param(¶m.name).ok_or_else(|| {
730 UnsupportedSearchFeature::unknown_parameter(resource_type, ¶m.name)
731 })?;
732
733 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 ¶m.name,
739 &modifier_str,
740 ));
741 }
742 }
743
744 for value in ¶m.values {
746 let prefix_str = value.prefix.to_string();
747 if !param_cap.supports_prefix(&prefix_str) {
748 return Err(UnsupportedSearchFeature::unsupported_prefix(
749 ¶m.name,
750 &prefix_str,
751 ));
752 }
753 }
754
755 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 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 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 #[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}