use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::types::{
ChainingCapability, IncludeCapability, PaginationCapability, ResultModeCapability,
SearchParamFullCapability, SearchParamType, SearchQuery, SpecialSearchParam,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Interaction {
Read,
Vread,
Update,
Patch,
Delete,
HistoryInstance,
HistoryType,
Create,
SearchType,
}
impl std::fmt::Display for Interaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Interaction::Read => write!(f, "read"),
Interaction::Vread => write!(f, "vread"),
Interaction::Update => write!(f, "update"),
Interaction::Patch => write!(f, "patch"),
Interaction::Delete => write!(f, "delete"),
Interaction::HistoryInstance => write!(f, "history-instance"),
Interaction::HistoryType => write!(f, "history-type"),
Interaction::Create => write!(f, "create"),
Interaction::SearchType => write!(f, "search-type"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SystemInteraction {
Transaction,
Batch,
HistorySystem,
SearchSystem,
}
impl std::fmt::Display for SystemInteraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SystemInteraction::Transaction => write!(f, "transaction"),
SystemInteraction::Batch => write!(f, "batch"),
SystemInteraction::HistorySystem => write!(f, "history-system"),
SystemInteraction::SearchSystem => write!(f, "search-system"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchParamCapability {
pub name: String,
pub param_type: SearchParamType,
pub modifiers: Vec<String>,
pub supports_chaining: bool,
pub documentation: Option<String>,
}
impl SearchParamCapability {
pub fn new(name: impl Into<String>, param_type: SearchParamType) -> Self {
Self {
name: name.into(),
param_type,
modifiers: Vec::new(),
supports_chaining: false,
documentation: None,
}
}
pub fn with_modifiers(mut self, modifiers: Vec<&str>) -> Self {
self.modifiers = modifiers.into_iter().map(String::from).collect();
self
}
pub fn with_chaining(mut self) -> Self {
self.supports_chaining = true;
self
}
pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
self.documentation = Some(doc.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceCapabilities {
pub resource_type: String,
pub interactions: HashSet<Interaction>,
pub search_params: Vec<SearchParamCapability>,
pub supports_include: bool,
pub supports_revinclude: bool,
pub include_targets: Vec<String>,
pub revinclude_targets: Vec<String>,
pub conditional_create: bool,
pub conditional_update: bool,
pub conditional_delete: bool,
pub documentation: Option<String>,
}
impl ResourceCapabilities {
pub fn new(resource_type: impl Into<String>) -> Self {
Self {
resource_type: resource_type.into(),
..Default::default()
}
}
pub fn with_interactions(
mut self,
interactions: impl IntoIterator<Item = Interaction>,
) -> Self {
self.interactions.extend(interactions);
self
}
pub fn with_crud(mut self) -> Self {
self.interactions.insert(Interaction::Read);
self.interactions.insert(Interaction::Create);
self.interactions.insert(Interaction::Update);
self.interactions.insert(Interaction::Delete);
self
}
pub fn with_versioning(mut self) -> Self {
self.interactions.insert(Interaction::Vread);
self.interactions.insert(Interaction::HistoryInstance);
self
}
pub fn with_search(mut self, params: Vec<SearchParamCapability>) -> Self {
self.interactions.insert(Interaction::SearchType);
self.search_params = params;
self
}
pub fn with_include(mut self, targets: Vec<&str>) -> Self {
self.supports_include = true;
self.include_targets = targets.into_iter().map(String::from).collect();
self
}
pub fn with_revinclude(mut self, targets: Vec<&str>) -> Self {
self.supports_revinclude = true;
self.revinclude_targets = targets.into_iter().map(String::from).collect();
self
}
pub fn with_conditional_ops(mut self) -> Self {
self.conditional_create = true;
self.conditional_update = true;
self.conditional_delete = true;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StorageCapabilities {
pub resources: HashMap<String, ResourceCapabilities>,
pub system_interactions: HashSet<SystemInteraction>,
pub supports_system_history: bool,
pub supports_system_search: bool,
pub supported_sorts: Vec<String>,
pub supports_total: bool,
pub max_page_size: Option<u32>,
pub default_page_size: u32,
pub backend_name: String,
pub backend_version: Option<String>,
}
impl StorageCapabilities {
pub fn new(backend_name: impl Into<String>) -> Self {
Self {
backend_name: backend_name.into(),
default_page_size: 20,
..Default::default()
}
}
pub fn with_resource(mut self, caps: ResourceCapabilities) -> Self {
self.resources.insert(caps.resource_type.clone(), caps);
self
}
pub fn with_system_interactions(
mut self,
interactions: impl IntoIterator<Item = SystemInteraction>,
) -> Self {
self.system_interactions.extend(interactions);
self
}
pub fn with_system_history(mut self) -> Self {
self.supports_system_history = true;
self.system_interactions
.insert(SystemInteraction::HistorySystem);
self
}
pub fn with_system_search(mut self) -> Self {
self.supports_system_search = true;
self.system_interactions
.insert(SystemInteraction::SearchSystem);
self
}
pub fn with_transactions(mut self) -> Self {
self.system_interactions
.insert(SystemInteraction::Transaction);
self.system_interactions.insert(SystemInteraction::Batch);
self
}
pub fn with_pagination(mut self, default: u32, max: Option<u32>) -> Self {
self.default_page_size = default;
self.max_page_size = max;
self
}
pub fn with_sorts(mut self, sorts: Vec<&str>) -> Self {
self.supported_sorts = sorts.into_iter().map(String::from).collect();
self
}
pub fn with_total_support(mut self) -> Self {
self.supports_total = true;
self
}
pub fn to_capability_rest(&self) -> Value {
let mut resources = Vec::new();
for caps in self.resources.values() {
let mut resource = serde_json::json!({
"type": caps.resource_type,
"interaction": caps.interactions.iter().map(|i| {
serde_json::json!({"code": i.to_string()})
}).collect::<Vec<_>>(),
});
if !caps.search_params.is_empty() {
resource["searchParam"] = serde_json::json!(
caps.search_params
.iter()
.map(|sp| {
serde_json::json!({
"name": sp.name,
"type": sp.param_type.to_string(),
})
})
.collect::<Vec<_>>()
);
}
if caps.conditional_create {
resource["conditionalCreate"] = serde_json::json!(true);
}
if caps.conditional_update {
resource["conditionalUpdate"] = serde_json::json!(true);
}
if caps.conditional_delete {
resource["conditionalDelete"] = serde_json::json!("single");
}
resources.push(resource);
}
let mut rest = serde_json::json!({
"mode": "server",
"resource": resources,
});
if !self.system_interactions.is_empty() {
rest["interaction"] = serde_json::json!(
self.system_interactions
.iter()
.map(|i| { serde_json::json!({"code": i.to_string()}) })
.collect::<Vec<_>>()
);
}
rest
}
}
pub trait CapabilityProvider {
fn capabilities(&self) -> StorageCapabilities;
fn supports_interaction(&self, resource_type: &str, interaction: Interaction) -> bool {
self.capabilities()
.resources
.get(resource_type)
.map(|r| r.interactions.contains(&interaction))
.unwrap_or(false)
}
fn supports_system_interaction(&self, interaction: SystemInteraction) -> bool {
self.capabilities()
.system_interactions
.contains(&interaction)
}
fn resource_capabilities(&self, resource_type: &str) -> Option<ResourceCapabilities> {
self.capabilities().resources.get(resource_type).cloned()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceSearchCapabilities {
pub resource_type: String,
pub search_params: Vec<SearchParamFullCapability>,
pub special_params: HashSet<SpecialSearchParam>,
pub include_capabilities: HashSet<IncludeCapability>,
pub chaining_capabilities: HashSet<ChainingCapability>,
pub pagination_capabilities: HashSet<PaginationCapability>,
pub result_mode_capabilities: HashSet<ResultModeCapability>,
}
impl ResourceSearchCapabilities {
pub fn new(resource_type: impl Into<String>) -> Self {
Self {
resource_type: resource_type.into(),
..Default::default()
}
}
pub fn with_param(mut self, param: SearchParamFullCapability) -> Self {
self.search_params.push(param);
self
}
pub fn with_param_list(mut self, params: Vec<SearchParamFullCapability>) -> Self {
self.search_params.extend(params);
self
}
pub fn with_special_params<I>(mut self, params: I) -> Self
where
I: IntoIterator<Item = SpecialSearchParam>,
{
self.special_params.extend(params);
self
}
pub fn with_include_capabilities<I>(mut self, caps: I) -> Self
where
I: IntoIterator<Item = IncludeCapability>,
{
self.include_capabilities.extend(caps);
self
}
pub fn with_chaining_capabilities<I>(mut self, caps: I) -> Self
where
I: IntoIterator<Item = ChainingCapability>,
{
self.chaining_capabilities.extend(caps);
self
}
pub fn with_pagination_capabilities<I>(mut self, caps: I) -> Self
where
I: IntoIterator<Item = PaginationCapability>,
{
self.pagination_capabilities.extend(caps);
self
}
pub fn with_result_mode_capabilities<I>(mut self, caps: I) -> Self
where
I: IntoIterator<Item = ResultModeCapability>,
{
self.result_mode_capabilities.extend(caps);
self
}
pub fn get_param(&self, name: &str) -> Option<&SearchParamFullCapability> {
self.search_params.iter().find(|p| p.name == name)
}
pub fn supports_special(&self, param: SpecialSearchParam) -> bool {
self.special_params.contains(¶m)
}
pub fn supports_include(&self, cap: IncludeCapability) -> bool {
self.include_capabilities.contains(&cap)
}
pub fn supports_chaining(&self) -> bool {
self.chaining_capabilities
.contains(&ChainingCapability::ForwardChain)
}
pub fn supports_reverse_chaining(&self) -> bool {
self.chaining_capabilities
.contains(&ChainingCapability::ReverseChain)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GlobalSearchCapabilities {
pub common_special_params: HashSet<SpecialSearchParam>,
pub common_include_capabilities: HashSet<IncludeCapability>,
pub common_pagination_capabilities: HashSet<PaginationCapability>,
pub common_result_mode_capabilities: HashSet<ResultModeCapability>,
pub max_chain_depth: Option<u8>,
pub supports_system_search: bool,
pub common_sort_params: Vec<String>,
}
impl GlobalSearchCapabilities {
pub fn new() -> Self {
Self::default()
}
pub fn with_special_params<I>(mut self, params: I) -> Self
where
I: IntoIterator<Item = SpecialSearchParam>,
{
self.common_special_params.extend(params);
self
}
pub fn with_pagination<I>(mut self, caps: I) -> Self
where
I: IntoIterator<Item = PaginationCapability>,
{
self.common_pagination_capabilities.extend(caps);
self
}
pub fn with_max_chain_depth(mut self, depth: u8) -> Self {
self.max_chain_depth = Some(depth);
self
}
pub fn with_system_search(mut self) -> Self {
self.supports_system_search = true;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnsupportedSearchFeature {
pub feature_type: UnsupportedFeatureType,
pub description: String,
pub parameter: Option<String>,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UnsupportedFeatureType {
UnknownParameter,
UnsupportedModifier,
UnsupportedPrefix,
UnsupportedChaining,
UnsupportedReverseChaining,
UnsupportedInclude,
UnsupportedComposite,
UnsupportedSpecialParameter,
UnsupportedResultMode,
UnsupportedPagination,
}
impl UnsupportedSearchFeature {
pub fn new(feature_type: UnsupportedFeatureType, description: impl Into<String>) -> Self {
Self {
feature_type,
description: description.into(),
parameter: None,
suggestion: None,
}
}
pub fn with_parameter(mut self, param: impl Into<String>) -> Self {
self.parameter = Some(param.into());
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn unknown_parameter(resource_type: &str, param: &str) -> Self {
Self::new(
UnsupportedFeatureType::UnknownParameter,
format!(
"Parameter '{}' is not defined for resource type '{}'",
param, resource_type
),
)
.with_parameter(param)
}
pub fn unsupported_modifier(param: &str, modifier: &str) -> Self {
Self::new(
UnsupportedFeatureType::UnsupportedModifier,
format!(
"Modifier '{}' is not supported for parameter '{}'",
modifier, param
),
)
.with_parameter(format!("{}:{}", param, modifier))
}
pub fn unsupported_prefix(param: &str, prefix: &str) -> Self {
Self::new(
UnsupportedFeatureType::UnsupportedPrefix,
format!(
"Prefix '{}' is not supported for parameter '{}'",
prefix, param
),
)
.with_parameter(param)
}
}
impl std::fmt::Display for UnsupportedSearchFeature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)?;
if let Some(ref suggestion) = self.suggestion {
write!(f, " ({})", suggestion)?;
}
Ok(())
}
}
impl std::error::Error for UnsupportedSearchFeature {}
pub trait SearchCapabilityProvider: Send + Sync {
fn resource_search_capabilities(
&self,
resource_type: &str,
) -> Option<ResourceSearchCapabilities>;
fn global_search_capabilities(&self) -> GlobalSearchCapabilities;
fn validate_search_query(&self, query: &SearchQuery) -> Result<(), UnsupportedSearchFeature> {
let resource_type = &query.resource_type;
let caps = self
.resource_search_capabilities(resource_type)
.ok_or_else(|| {
UnsupportedSearchFeature::new(
UnsupportedFeatureType::UnknownParameter,
format!("Resource type '{}' is not supported", resource_type),
)
})?;
for param in &query.parameters {
let param_cap = caps.get_param(¶m.name).ok_or_else(|| {
UnsupportedSearchFeature::unknown_parameter(resource_type, ¶m.name)
})?;
if let Some(ref modifier) = param.modifier {
let modifier_str = modifier.to_string();
if !param_cap.supports_modifier(&modifier_str) {
return Err(UnsupportedSearchFeature::unsupported_modifier(
¶m.name,
&modifier_str,
));
}
}
for value in ¶m.values {
let prefix_str = value.prefix.to_string();
if !param_cap.supports_prefix(&prefix_str) {
return Err(UnsupportedSearchFeature::unsupported_prefix(
¶m.name,
&prefix_str,
));
}
}
if !param.chain.is_empty() && !caps.supports_chaining() {
return Err(UnsupportedSearchFeature::new(
UnsupportedFeatureType::UnsupportedChaining,
"Chained search parameters are not supported",
));
}
}
if !query.reverse_chains.is_empty() && !caps.supports_reverse_chaining() {
return Err(UnsupportedSearchFeature::new(
UnsupportedFeatureType::UnsupportedReverseChaining,
"_has (reverse chaining) is not supported",
));
}
for include in &query.includes {
let include_cap = if include.include_type == crate::types::IncludeType::Include {
if include.iterate {
IncludeCapability::IncludeIterate
} else {
IncludeCapability::Include
}
} else if include.iterate {
IncludeCapability::RevincludeIterate
} else {
IncludeCapability::Revinclude
};
if !caps.supports_include(include_cap) {
return Err(UnsupportedSearchFeature::new(
UnsupportedFeatureType::UnsupportedInclude,
format!("{:?} is not supported", include_cap),
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interaction_display() {
assert_eq!(Interaction::Read.to_string(), "read");
assert_eq!(Interaction::HistoryInstance.to_string(), "history-instance");
}
#[test]
fn test_system_interaction_display() {
assert_eq!(SystemInteraction::Transaction.to_string(), "transaction");
assert_eq!(
SystemInteraction::HistorySystem.to_string(),
"history-system"
);
}
#[test]
fn test_search_param_capability() {
let cap = SearchParamCapability::new("name", SearchParamType::String)
.with_modifiers(vec!["exact", "contains"])
.with_documentation("Search by patient name");
assert_eq!(cap.name, "name");
assert_eq!(cap.modifiers.len(), 2);
assert!(cap.documentation.is_some());
}
#[test]
fn test_resource_capabilities() {
let caps = ResourceCapabilities::new("Patient")
.with_crud()
.with_versioning()
.with_conditional_ops();
assert!(caps.interactions.contains(&Interaction::Read));
assert!(caps.interactions.contains(&Interaction::Create));
assert!(caps.interactions.contains(&Interaction::Vread));
assert!(caps.conditional_create);
}
#[test]
fn test_storage_capabilities() {
let patient_caps = ResourceCapabilities::new("Patient")
.with_crud()
.with_search(vec![
SearchParamCapability::new("name", SearchParamType::String),
SearchParamCapability::new("identifier", SearchParamType::Token),
]);
let caps = StorageCapabilities::new("sqlite")
.with_resource(patient_caps)
.with_transactions()
.with_pagination(20, Some(100));
assert!(caps.resources.contains_key("Patient"));
assert!(
caps.system_interactions
.contains(&SystemInteraction::Transaction)
);
assert_eq!(caps.default_page_size, 20);
assert_eq!(caps.max_page_size, Some(100));
}
#[test]
fn test_to_capability_rest() {
let caps = StorageCapabilities::new("test")
.with_resource(ResourceCapabilities::new("Patient").with_crud())
.with_transactions();
let rest = caps.to_capability_rest();
assert_eq!(rest["mode"], "server");
assert!(rest["resource"].is_array());
assert!(rest["interaction"].is_array());
}
#[test]
fn test_resource_search_capabilities() {
let caps = ResourceSearchCapabilities::new("Patient")
.with_param(SearchParamFullCapability::new(
"name",
SearchParamType::String,
))
.with_special_params(vec![
SpecialSearchParam::Id,
SpecialSearchParam::LastUpdated,
])
.with_include_capabilities(vec![IncludeCapability::Include]);
assert_eq!(caps.resource_type, "Patient");
assert!(caps.get_param("name").is_some());
assert!(caps.supports_special(SpecialSearchParam::Id));
assert!(caps.supports_include(IncludeCapability::Include));
}
#[test]
fn test_global_search_capabilities() {
let global = GlobalSearchCapabilities::new()
.with_special_params(vec![SpecialSearchParam::Id])
.with_max_chain_depth(3)
.with_system_search();
assert!(
global
.common_special_params
.contains(&SpecialSearchParam::Id)
);
assert_eq!(global.max_chain_depth, Some(3));
assert!(global.supports_system_search);
}
#[test]
fn test_unsupported_search_feature() {
let err = UnsupportedSearchFeature::unknown_parameter("Patient", "unknown");
assert_eq!(err.feature_type, UnsupportedFeatureType::UnknownParameter);
assert!(err.parameter.is_some());
assert!(err.to_string().contains("unknown"));
let err2 = UnsupportedSearchFeature::unsupported_modifier("name", "phonetic");
assert_eq!(
err2.feature_type,
UnsupportedFeatureType::UnsupportedModifier
);
}
#[test]
fn test_search_capabilities_chaining() {
let caps = ResourceSearchCapabilities::new("Observation")
.with_chaining_capabilities(vec![ChainingCapability::ForwardChain]);
assert!(caps.supports_chaining());
assert!(!caps.supports_reverse_chaining());
}
}