use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SearchParamType {
#[default]
String,
Uri,
Number,
Date,
Quantity,
Token,
Reference,
Composite,
Special,
}
impl fmt::Display for SearchParamType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SearchParamType::String => write!(f, "string"),
SearchParamType::Uri => write!(f, "uri"),
SearchParamType::Number => write!(f, "number"),
SearchParamType::Date => write!(f, "date"),
SearchParamType::Quantity => write!(f, "quantity"),
SearchParamType::Token => write!(f, "token"),
SearchParamType::Reference => write!(f, "reference"),
SearchParamType::Composite => write!(f, "composite"),
SearchParamType::Special => write!(f, "special"),
}
}
}
impl FromStr for SearchParamType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"string" => Ok(SearchParamType::String),
"uri" => Ok(SearchParamType::Uri),
"number" => Ok(SearchParamType::Number),
"date" => Ok(SearchParamType::Date),
"quantity" => Ok(SearchParamType::Quantity),
"token" => Ok(SearchParamType::Token),
"reference" => Ok(SearchParamType::Reference),
"composite" => Ok(SearchParamType::Composite),
"special" => Ok(SearchParamType::Special),
_ => Err(format!("unknown search parameter type: {}", s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SearchModifier {
Exact,
Contains,
Text,
Not,
Missing,
Above,
Below,
In,
NotIn,
Identifier,
Type(String),
OfType,
CodeOnly,
Iterate,
TextAdvanced,
CodeText,
}
impl fmt::Display for SearchModifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SearchModifier::Exact => write!(f, "exact"),
SearchModifier::Contains => write!(f, "contains"),
SearchModifier::Text => write!(f, "text"),
SearchModifier::Not => write!(f, "not"),
SearchModifier::Missing => write!(f, "missing"),
SearchModifier::Above => write!(f, "above"),
SearchModifier::Below => write!(f, "below"),
SearchModifier::In => write!(f, "in"),
SearchModifier::NotIn => write!(f, "not-in"),
SearchModifier::Identifier => write!(f, "identifier"),
SearchModifier::Type(t) => write!(f, "{}", t),
SearchModifier::OfType => write!(f, "ofType"),
SearchModifier::CodeOnly => write!(f, "code"),
SearchModifier::Iterate => write!(f, "iterate"),
SearchModifier::TextAdvanced => write!(f, "text-advanced"),
SearchModifier::CodeText => write!(f, "code-text"),
}
}
}
impl SearchModifier {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"exact" => Some(SearchModifier::Exact),
"contains" => Some(SearchModifier::Contains),
"text" => Some(SearchModifier::Text),
"not" => Some(SearchModifier::Not),
"missing" => Some(SearchModifier::Missing),
"above" => Some(SearchModifier::Above),
"below" => Some(SearchModifier::Below),
"in" => Some(SearchModifier::In),
"not-in" => Some(SearchModifier::NotIn),
"identifier" => Some(SearchModifier::Identifier),
"oftype" => Some(SearchModifier::OfType),
"code" => Some(SearchModifier::CodeOnly),
"iterate" => Some(SearchModifier::Iterate),
"text-advanced" => Some(SearchModifier::TextAdvanced),
"code-text" => Some(SearchModifier::CodeText),
_ => {
if s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
Some(SearchModifier::Type(s.to_string()))
} else {
None
}
}
}
}
pub fn is_valid_for(&self, param_type: SearchParamType) -> bool {
match self {
SearchModifier::Exact | SearchModifier::Contains => {
param_type == SearchParamType::String
}
SearchModifier::Text => param_type == SearchParamType::Token,
SearchModifier::Not => true, SearchModifier::Missing => true, SearchModifier::Above
| SearchModifier::Below
| SearchModifier::In
| SearchModifier::NotIn => {
param_type == SearchParamType::Token || param_type == SearchParamType::Uri
}
SearchModifier::Identifier | SearchModifier::Type(_) => {
param_type == SearchParamType::Reference
}
SearchModifier::OfType => param_type == SearchParamType::Token,
SearchModifier::CodeOnly => param_type == SearchParamType::Token,
SearchModifier::Iterate => false, SearchModifier::TextAdvanced => {
param_type == SearchParamType::String || param_type == SearchParamType::Token
}
SearchModifier::CodeText => param_type == SearchParamType::Token,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SearchPrefix {
#[default]
Eq,
Ne,
Gt,
Lt,
Ge,
Le,
Sa,
Eb,
Ap,
}
impl fmt::Display for SearchPrefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SearchPrefix::Eq => write!(f, "eq"),
SearchPrefix::Ne => write!(f, "ne"),
SearchPrefix::Gt => write!(f, "gt"),
SearchPrefix::Lt => write!(f, "lt"),
SearchPrefix::Ge => write!(f, "ge"),
SearchPrefix::Le => write!(f, "le"),
SearchPrefix::Sa => write!(f, "sa"),
SearchPrefix::Eb => write!(f, "eb"),
SearchPrefix::Ap => write!(f, "ap"),
}
}
}
impl FromStr for SearchPrefix {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"eq" => Ok(SearchPrefix::Eq),
"ne" => Ok(SearchPrefix::Ne),
"gt" => Ok(SearchPrefix::Gt),
"lt" => Ok(SearchPrefix::Lt),
"ge" => Ok(SearchPrefix::Ge),
"le" => Ok(SearchPrefix::Le),
"sa" => Ok(SearchPrefix::Sa),
"eb" => Ok(SearchPrefix::Eb),
"ap" => Ok(SearchPrefix::Ap),
_ => Err(format!("unknown search prefix: {}", s)),
}
}
}
impl SearchPrefix {
pub fn extract(value: &str) -> (Self, &str) {
if value.len() >= 2 {
let prefix = &value[..2];
if let Ok(p) = prefix.parse() {
return (p, &value[2..]);
}
}
(SearchPrefix::Eq, value)
}
pub fn is_valid_for(&self, param_type: SearchParamType) -> bool {
match self {
SearchPrefix::Eq | SearchPrefix::Ne => true,
SearchPrefix::Gt | SearchPrefix::Lt | SearchPrefix::Ge | SearchPrefix::Le => {
matches!(
param_type,
SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity
)
}
SearchPrefix::Sa | SearchPrefix::Eb => param_type == SearchParamType::Date,
SearchPrefix::Ap => {
matches!(
param_type,
SearchParamType::Number | SearchParamType::Date | SearchParamType::Quantity
)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SearchParameter {
#[serde(default)]
pub name: String,
#[serde(default)]
pub param_type: SearchParamType,
#[serde(default)]
pub modifier: Option<SearchModifier>,
#[serde(default)]
pub values: Vec<SearchValue>,
#[serde(default)]
pub chain: Vec<ChainedParameter>,
#[serde(default)]
pub components: Vec<CompositeSearchComponent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositeSearchComponent {
pub param_type: SearchParamType,
pub param_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchValue {
pub prefix: SearchPrefix,
pub value: String,
}
impl SearchValue {
pub fn new(prefix: SearchPrefix, value: impl Into<String>) -> Self {
Self {
prefix,
value: value.into(),
}
}
pub fn eq(value: impl Into<String>) -> Self {
Self::new(SearchPrefix::Eq, value)
}
pub fn parse(s: &str) -> Self {
let (prefix, value) = SearchPrefix::extract(s);
Self::new(prefix, value)
}
pub fn token(system: Option<&str>, code: impl Into<String>) -> Self {
let code = code.into();
match system {
Some(sys) => Self::eq(format!("{}|{}", sys, code)),
None => Self::eq(code),
}
}
pub fn token_system_only(system: impl Into<String>) -> Self {
Self::eq(format!("{}|", system.into()))
}
pub fn boolean(value: bool) -> Self {
Self::eq(value.to_string())
}
pub fn string(value: impl Into<String>) -> Self {
Self::eq(value)
}
pub fn of_type(
type_system: impl Into<String>,
type_code: impl Into<String>,
value: impl Into<String>,
) -> Self {
Self::eq(format!(
"{}|{}|{}",
type_system.into(),
type_code.into(),
value.into()
))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainedParameter {
pub reference_param: String,
pub target_type: Option<String>,
pub target_param: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReverseChainedParameter {
pub source_type: String,
pub reference_param: String,
pub search_param: String,
pub value: Option<SearchValue>,
pub nested: Option<Box<ReverseChainedParameter>>,
}
impl ReverseChainedParameter {
pub fn terminal(
source_type: impl Into<String>,
reference_param: impl Into<String>,
search_param: impl Into<String>,
value: SearchValue,
) -> Self {
Self {
source_type: source_type.into(),
reference_param: reference_param.into(),
search_param: search_param.into(),
value: Some(value),
nested: None,
}
}
pub fn nested(
source_type: impl Into<String>,
reference_param: impl Into<String>,
inner: ReverseChainedParameter,
) -> Self {
Self {
source_type: source_type.into(),
reference_param: reference_param.into(),
search_param: String::new(),
value: None,
nested: Some(Box::new(inner)),
}
}
pub fn depth(&self) -> usize {
match &self.nested {
Some(inner) => 1 + inner.depth(),
None => 1,
}
}
pub fn is_terminal(&self) -> bool {
self.nested.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainConfig {
pub max_forward_depth: usize,
pub max_reverse_depth: usize,
}
impl Default for ChainConfig {
fn default() -> Self {
Self {
max_forward_depth: 4,
max_reverse_depth: 4,
}
}
}
impl ChainConfig {
pub fn new(max_forward_depth: usize, max_reverse_depth: usize) -> Self {
Self {
max_forward_depth: max_forward_depth.min(8),
max_reverse_depth: max_reverse_depth.min(8),
}
}
pub fn validate_forward_depth(&self, depth: usize) -> bool {
depth <= self.max_forward_depth
}
pub fn validate_reverse_depth(&self, depth: usize) -> bool {
depth <= self.max_reverse_depth
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IncludeDirective {
pub include_type: IncludeType,
pub source_type: String,
pub search_param: String,
pub target_type: Option<String>,
pub iterate: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IncludeType {
Include,
Revinclude,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum SortDirection {
#[default]
Ascending,
Descending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SortDirective {
pub parameter: String,
pub direction: SortDirection,
}
impl SortDirective {
pub fn parse(s: &str) -> Self {
if let Some(stripped) = s.strip_prefix('-') {
Self {
parameter: stripped.to_string(),
direction: SortDirection::Descending,
}
} else {
Self {
parameter: s.to_string(),
direction: SortDirection::Ascending,
}
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SearchQuery {
pub resource_type: String,
pub parameters: Vec<SearchParameter>,
pub reverse_chains: Vec<ReverseChainedParameter>,
pub includes: Vec<IncludeDirective>,
pub sort: Vec<SortDirective>,
pub count: Option<u32>,
pub offset: Option<u32>,
pub cursor: Option<String>,
pub total: Option<TotalMode>,
pub summary: Option<SummaryMode>,
pub elements: Vec<String>,
pub raw_params: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TotalMode {
None,
Estimate,
Accurate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SummaryMode {
True,
False,
Text,
Data,
Count,
}
impl SearchQuery {
pub fn new(resource_type: impl Into<String>) -> Self {
Self {
resource_type: resource_type.into(),
..Default::default()
}
}
pub fn with_parameter(mut self, param: SearchParameter) -> Self {
self.parameters.push(param);
self
}
pub fn with_include(mut self, include: IncludeDirective) -> Self {
self.includes.push(include);
self
}
pub fn with_sort(mut self, sort: SortDirective) -> Self {
self.sort.push(sort);
self
}
pub fn with_count(mut self, count: u32) -> Self {
self.count = Some(count);
self
}
pub fn with_cursor(mut self, cursor: String) -> Self {
self.cursor = Some(cursor);
self
}
pub fn requires_advanced_features(&self) -> bool {
if self.parameters.iter().any(|p| !p.chain.is_empty()) {
return true;
}
if !self.reverse_chains.is_empty() {
return true;
}
if !self.includes.is_empty() {
return true;
}
if self.parameters.iter().any(|p| {
matches!(
p.modifier,
Some(SearchModifier::Above)
| Some(SearchModifier::Below)
| Some(SearchModifier::In)
| Some(SearchModifier::NotIn)
)
}) {
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_param_type_display() {
assert_eq!(SearchParamType::String.to_string(), "string");
assert_eq!(SearchParamType::Token.to_string(), "token");
assert_eq!(SearchParamType::Reference.to_string(), "reference");
}
#[test]
fn test_search_param_type_parse() {
assert_eq!(
"string".parse::<SearchParamType>().unwrap(),
SearchParamType::String
);
assert_eq!(
"TOKEN".parse::<SearchParamType>().unwrap(),
SearchParamType::Token
);
}
#[test]
fn test_search_modifier_parse() {
assert_eq!(SearchModifier::parse("exact"), Some(SearchModifier::Exact));
assert_eq!(
SearchModifier::parse("contains"),
Some(SearchModifier::Contains)
);
assert_eq!(
SearchModifier::parse("Patient"),
Some(SearchModifier::Type("Patient".to_string()))
);
assert_eq!(SearchModifier::parse("unknown"), None);
}
#[test]
fn test_search_modifier_validity() {
assert!(SearchModifier::Exact.is_valid_for(SearchParamType::String));
assert!(!SearchModifier::Exact.is_valid_for(SearchParamType::Token));
assert!(SearchModifier::Text.is_valid_for(SearchParamType::Token));
assert!(SearchModifier::Not.is_valid_for(SearchParamType::String));
assert!(SearchModifier::Not.is_valid_for(SearchParamType::Token));
}
#[test]
fn test_search_prefix_extract() {
assert_eq!(
SearchPrefix::extract("gt2020-01-01"),
(SearchPrefix::Gt, "2020-01-01")
);
assert_eq!(
SearchPrefix::extract("2020-01-01"),
(SearchPrefix::Eq, "2020-01-01")
);
assert_eq!(SearchPrefix::extract("le100"), (SearchPrefix::Le, "100"));
}
#[test]
fn test_search_prefix_validity() {
assert!(SearchPrefix::Gt.is_valid_for(SearchParamType::Number));
assert!(SearchPrefix::Gt.is_valid_for(SearchParamType::Date));
assert!(!SearchPrefix::Gt.is_valid_for(SearchParamType::String));
assert!(SearchPrefix::Sa.is_valid_for(SearchParamType::Date));
assert!(!SearchPrefix::Sa.is_valid_for(SearchParamType::Number));
}
#[test]
fn test_search_value_parse() {
let value = SearchValue::parse("gt100");
assert_eq!(value.prefix, SearchPrefix::Gt);
assert_eq!(value.value, "100");
let value2 = SearchValue::parse("Smith");
assert_eq!(value2.prefix, SearchPrefix::Eq);
assert_eq!(value2.value, "Smith");
}
#[test]
fn test_sort_directive_parse() {
let asc = SortDirective::parse("date");
assert_eq!(asc.parameter, "date");
assert_eq!(asc.direction, SortDirection::Ascending);
let desc = SortDirective::parse("-date");
assert_eq!(desc.parameter, "date");
assert_eq!(desc.direction, SortDirection::Descending);
}
#[test]
fn test_search_query_builder() {
let query = SearchQuery::new("Patient")
.with_count(10)
.with_sort(SortDirective::parse("-_lastUpdated"));
assert_eq!(query.resource_type, "Patient");
assert_eq!(query.count, Some(10));
assert_eq!(query.sort.len(), 1);
}
#[test]
fn test_requires_advanced_features() {
let simple = SearchQuery::new("Patient");
assert!(!simple.requires_advanced_features());
let with_include = SearchQuery::new("Patient").with_include(IncludeDirective {
include_type: IncludeType::Include,
source_type: "Patient".to_string(),
search_param: "organization".to_string(),
target_type: None,
iterate: false,
});
assert!(with_include.requires_advanced_features());
}
}