use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use super::SearchParamType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SpecialSearchParam {
Id,
LastUpdated,
Tag,
Profile,
Security,
Text,
Content,
List,
Has,
Type,
Query,
Filter,
Source,
}
impl SpecialSearchParam {
pub fn name(&self) -> &'static str {
match self {
SpecialSearchParam::Id => "_id",
SpecialSearchParam::LastUpdated => "_lastUpdated",
SpecialSearchParam::Tag => "_tag",
SpecialSearchParam::Profile => "_profile",
SpecialSearchParam::Security => "_security",
SpecialSearchParam::Text => "_text",
SpecialSearchParam::Content => "_content",
SpecialSearchParam::List => "_list",
SpecialSearchParam::Has => "_has",
SpecialSearchParam::Type => "_type",
SpecialSearchParam::Query => "_query",
SpecialSearchParam::Filter => "_filter",
SpecialSearchParam::Source => "_source",
}
}
pub fn param_type(&self) -> SearchParamType {
match self {
SpecialSearchParam::Id => SearchParamType::Token,
SpecialSearchParam::LastUpdated => SearchParamType::Date,
SpecialSearchParam::Tag => SearchParamType::Token,
SpecialSearchParam::Profile => SearchParamType::Uri,
SpecialSearchParam::Security => SearchParamType::Token,
SpecialSearchParam::Text => SearchParamType::Special,
SpecialSearchParam::Content => SearchParamType::Special,
SpecialSearchParam::List => SearchParamType::Reference,
SpecialSearchParam::Has => SearchParamType::Special,
SpecialSearchParam::Type => SearchParamType::Token,
SpecialSearchParam::Query => SearchParamType::Special,
SpecialSearchParam::Filter => SearchParamType::Special,
SpecialSearchParam::Source => SearchParamType::Uri,
}
}
pub fn all() -> &'static [SpecialSearchParam] {
&[
SpecialSearchParam::Id,
SpecialSearchParam::LastUpdated,
SpecialSearchParam::Tag,
SpecialSearchParam::Profile,
SpecialSearchParam::Security,
SpecialSearchParam::Text,
SpecialSearchParam::Content,
SpecialSearchParam::List,
SpecialSearchParam::Has,
SpecialSearchParam::Type,
SpecialSearchParam::Query,
SpecialSearchParam::Filter,
SpecialSearchParam::Source,
]
}
}
impl std::fmt::Display for SpecialSearchParam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IncludeCapability {
Include,
Revinclude,
IncludeIterate,
RevincludeIterate,
IncludeWildcard,
RevincludeWildcard,
}
impl IncludeCapability {
pub fn modifier(&self) -> Option<&'static str> {
match self {
IncludeCapability::Include => None,
IncludeCapability::Revinclude => None,
IncludeCapability::IncludeIterate => Some("iterate"),
IncludeCapability::RevincludeIterate => Some("iterate"),
IncludeCapability::IncludeWildcard => None,
IncludeCapability::RevincludeWildcard => None,
}
}
pub fn is_include(&self) -> bool {
matches!(
self,
IncludeCapability::Include
| IncludeCapability::IncludeIterate
| IncludeCapability::IncludeWildcard
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ChainingCapability {
ForwardChain,
ReverseChain,
MaxDepth(u8),
}
impl ChainingCapability {
pub fn max_depth(&self) -> Option<u8> {
match self {
ChainingCapability::MaxDepth(d) => Some(*d),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PaginationCapability {
Count,
Offset,
Cursor,
MaxPageSize(u32),
DefaultPageSize(u32),
}
impl PaginationCapability {
pub fn page_size(&self) -> Option<u32> {
match self {
PaginationCapability::MaxPageSize(s) | PaginationCapability::DefaultPageSize(s) => {
Some(*s)
}
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ResultModeCapability {
Summary,
SummaryTrue,
SummaryText,
SummaryData,
SummaryCount,
SummaryFalse,
Elements,
Total,
TotalNone,
TotalEstimate,
TotalAccurate,
Contained,
ContainedType,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CompositeComponent {
pub definition: String,
pub expression: String,
}
impl CompositeComponent {
pub fn new(definition: impl Into<String>, expression: impl Into<String>) -> Self {
Self {
definition: definition.into(),
expression: expression.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchParamFullCapability {
pub name: String,
pub param_type: SearchParamType,
pub definition: Option<String>,
pub modifiers: HashSet<String>,
pub prefixes: HashSet<String>,
pub chaining: Option<ChainingCapability>,
pub target_types: Vec<String>,
pub components: Vec<CompositeComponent>,
pub shall_support: bool,
}
impl SearchParamFullCapability {
pub fn new(name: impl Into<String>, param_type: SearchParamType) -> Self {
Self {
name: name.into(),
param_type,
definition: None,
modifiers: HashSet::new(),
prefixes: Self::default_prefixes(param_type),
chaining: None,
target_types: Vec::new(),
components: Vec::new(),
shall_support: false,
}
}
fn default_prefixes(param_type: SearchParamType) -> HashSet<String> {
let mut prefixes = HashSet::new();
prefixes.insert("eq".to_string());
match param_type {
SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity => {
prefixes.insert("ne".to_string());
prefixes.insert("gt".to_string());
prefixes.insert("lt".to_string());
prefixes.insert("ge".to_string());
prefixes.insert("le".to_string());
}
_ => {}
}
if param_type == SearchParamType::Date {
prefixes.insert("sa".to_string());
prefixes.insert("eb".to_string());
prefixes.insert("ap".to_string());
}
prefixes
}
pub fn with_definition(mut self, url: impl Into<String>) -> Self {
self.definition = Some(url.into());
self
}
pub fn with_modifiers<I, S>(mut self, modifiers: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.modifiers = modifiers.into_iter().map(Into::into).collect();
self
}
pub fn with_chaining(mut self, chaining: ChainingCapability) -> Self {
self.chaining = Some(chaining);
self
}
pub fn with_targets<I, S>(mut self, targets: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.target_types = targets.into_iter().map(Into::into).collect();
self
}
pub fn with_components(mut self, components: Vec<CompositeComponent>) -> Self {
self.components = components;
self
}
pub fn shall(mut self) -> Self {
self.shall_support = true;
self
}
pub fn supports_modifier(&self, modifier: &str) -> bool {
self.modifiers.contains(modifier)
}
pub fn supports_prefix(&self, prefix: &str) -> bool {
self.prefixes.contains(prefix)
}
pub fn is_composite(&self) -> bool {
self.param_type == SearchParamType::Composite && !self.components.is_empty()
}
pub fn supports_chaining(&self) -> bool {
self.chaining.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DatePrecision {
Year,
Month,
Day,
Hour,
Minute,
Second,
Millisecond,
}
impl DatePrecision {
pub fn from_date_string(s: &str) -> Self {
let base = s.split('+').next().unwrap_or(s);
let base = base.split('Z').next().unwrap_or(base);
match base.len() {
4 => DatePrecision::Year,
7 => DatePrecision::Month,
10 => DatePrecision::Day,
13 => DatePrecision::Hour,
16 => DatePrecision::Minute,
19 => DatePrecision::Second,
_ => DatePrecision::Millisecond,
}
}
pub fn sql_format(&self) -> &'static str {
match self {
DatePrecision::Year => "%Y",
DatePrecision::Month => "%Y-%m",
DatePrecision::Day => "%Y-%m-%d",
DatePrecision::Hour => "%Y-%m-%dT%H",
DatePrecision::Minute => "%Y-%m-%dT%H:%M",
DatePrecision::Second => "%Y-%m-%dT%H:%M:%S",
DatePrecision::Millisecond => "%Y-%m-%dT%H:%M:%S%.f",
}
}
}
impl std::fmt::Display for DatePrecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DatePrecision::Year => write!(f, "year"),
DatePrecision::Month => write!(f, "month"),
DatePrecision::Day => write!(f, "day"),
DatePrecision::Hour => write!(f, "hour"),
DatePrecision::Minute => write!(f, "minute"),
DatePrecision::Second => write!(f, "second"),
DatePrecision::Millisecond => write!(f, "millisecond"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum SearchStrategy {
#[default]
PrecomputedIndex,
QueryTimeEvaluation,
Hybrid {
indexed_params: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum IndexingMode {
#[default]
Inline,
Async,
HybridAsync {
inline_params: Vec<String>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct JsonbCapabilities {
pub path_extraction: bool,
pub array_iteration: bool,
pub containment_operator: bool,
pub gin_index: bool,
}
impl JsonbCapabilities {
pub fn sqlite() -> Self {
Self {
path_extraction: true,
array_iteration: true,
containment_operator: false,
gin_index: false,
}
}
pub fn postgresql() -> Self {
Self {
path_extraction: true,
array_iteration: true,
containment_operator: true,
gin_index: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_special_search_param() {
assert_eq!(SpecialSearchParam::Id.name(), "_id");
assert_eq!(
SpecialSearchParam::LastUpdated.param_type(),
SearchParamType::Date
);
assert_eq!(SpecialSearchParam::all().len(), 13);
}
#[test]
fn test_include_capability() {
assert!(IncludeCapability::Include.is_include());
assert!(!IncludeCapability::Revinclude.is_include());
assert_eq!(
IncludeCapability::IncludeIterate.modifier(),
Some("iterate")
);
}
#[test]
fn test_chaining_capability() {
assert_eq!(ChainingCapability::MaxDepth(3).max_depth(), Some(3));
assert_eq!(ChainingCapability::ForwardChain.max_depth(), None);
}
#[test]
fn test_pagination_capability() {
assert_eq!(
PaginationCapability::MaxPageSize(100).page_size(),
Some(100)
);
assert_eq!(PaginationCapability::Cursor.page_size(), None);
}
#[test]
fn test_search_param_full_capability() {
let cap = SearchParamFullCapability::new("name", SearchParamType::String)
.with_modifiers(vec!["exact", "contains"])
.with_definition("http://hl7.org/fhir/SearchParameter/Patient-name");
assert_eq!(cap.name, "name");
assert!(cap.supports_modifier("exact"));
assert!(!cap.supports_modifier("above"));
assert!(cap.supports_prefix("eq"));
}
#[test]
fn test_date_precision() {
assert_eq!(DatePrecision::from_date_string("2024"), DatePrecision::Year);
assert_eq!(
DatePrecision::from_date_string("2024-01"),
DatePrecision::Month
);
assert_eq!(
DatePrecision::from_date_string("2024-01-15"),
DatePrecision::Day
);
assert_eq!(
DatePrecision::from_date_string("2024-01-15T10:30:00"),
DatePrecision::Second
);
}
#[test]
fn test_composite_component() {
let comp = CompositeComponent::new(
"http://hl7.org/fhir/SearchParameter/Observation-code",
"Observation.code",
);
assert!(!comp.definition.is_empty());
assert!(!comp.expression.is_empty());
}
#[test]
fn test_search_strategy_default() {
assert_eq!(SearchStrategy::default(), SearchStrategy::PrecomputedIndex);
}
#[test]
fn test_indexing_mode_default() {
assert_eq!(IndexingMode::default(), IndexingMode::Inline);
}
#[test]
fn test_jsonb_capabilities() {
let sqlite = JsonbCapabilities::sqlite();
assert!(sqlite.path_extraction);
assert!(!sqlite.containment_operator);
let pg = JsonbCapabilities::postgresql();
assert!(pg.containment_operator);
assert!(pg.gin_index);
}
}