use std::collections::HashMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
use crate::types::SearchParamType;
use super::errors::RegistryError;
use super::loader::SearchParameterLoader;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SearchParameterStatus {
#[default]
Active,
Draft,
Retired,
}
impl SearchParameterStatus {
pub fn from_fhir_status(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"active" => Some(SearchParameterStatus::Active),
"draft" => Some(SearchParameterStatus::Draft),
"retired" => Some(SearchParameterStatus::Retired),
_ => None,
}
}
pub fn to_fhir_status(&self) -> &'static str {
match self {
SearchParameterStatus::Active => "active",
SearchParameterStatus::Draft => "draft",
SearchParameterStatus::Retired => "retired",
}
}
pub fn is_usable(&self) -> bool {
*self == SearchParameterStatus::Active
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SearchParameterSource {
#[default]
Embedded,
Stored,
Config,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompositeComponentDef {
pub definition: String,
pub expression: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchParameterDefinition {
pub url: String,
pub code: String,
pub name: Option<String>,
pub description: Option<String>,
pub param_type: SearchParamType,
pub expression: String,
pub base: Vec<String>,
pub target: Option<Vec<String>>,
pub component: Option<Vec<CompositeComponentDef>>,
pub status: SearchParameterStatus,
pub source: SearchParameterSource,
pub modifier: Option<Vec<String>>,
pub multiple_or: Option<bool>,
pub multiple_and: Option<bool>,
pub comparator: Option<Vec<String>>,
pub xpath: Option<String>,
}
impl SearchParameterDefinition {
pub fn new(
url: impl Into<String>,
code: impl Into<String>,
param_type: SearchParamType,
expression: impl Into<String>,
) -> Self {
Self {
url: url.into(),
code: code.into(),
name: None,
description: None,
param_type,
expression: expression.into(),
base: Vec::new(),
target: None,
component: None,
status: SearchParameterStatus::Active,
source: SearchParameterSource::Embedded,
modifier: None,
multiple_or: None,
multiple_and: None,
comparator: None,
xpath: None,
}
}
pub fn with_base<I, S>(mut self, base: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.base = base.into_iter().map(Into::into).collect();
self
}
pub fn with_targets<I, S>(mut self, targets: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.target = Some(targets.into_iter().map(Into::into).collect());
self
}
pub fn with_source(mut self, source: SearchParameterSource) -> Self {
self.source = source;
self
}
pub fn with_status(mut self, status: SearchParameterStatus) -> Self {
self.status = status;
self
}
pub fn is_composite(&self) -> bool {
self.param_type == SearchParamType::Composite
&& self
.component
.as_ref()
.map(|c| !c.is_empty())
.unwrap_or(false)
}
pub fn applies_to(&self, resource_type: &str) -> bool {
self.base
.iter()
.any(|b| b == resource_type || b == "Resource" || b == "DomainResource")
}
}
#[derive(Debug, Clone)]
pub enum RegistryUpdate {
Added(String),
Removed(String),
StatusChanged(String, SearchParameterStatus),
Reloaded,
}
pub struct SearchParameterRegistry {
params_by_type: HashMap<String, HashMap<String, Arc<SearchParameterDefinition>>>,
params_by_url: HashMap<String, Arc<SearchParameterDefinition>>,
update_tx: broadcast::Sender<RegistryUpdate>,
}
impl SearchParameterRegistry {
pub fn new() -> Self {
let (update_tx, _) = broadcast::channel(64);
Self {
params_by_type: HashMap::new(),
params_by_url: HashMap::new(),
update_tx,
}
}
pub fn len(&self) -> usize {
self.params_by_url.len()
}
pub fn is_empty(&self) -> bool {
self.params_by_url.is_empty()
}
pub async fn load_all(
&mut self,
loader: &SearchParameterLoader,
) -> Result<usize, super::errors::LoaderError> {
let params = loader.load_embedded()?;
let count = params.len();
for param in params {
if !self.params_by_url.contains_key(¶m.url) {
self.register_internal(param);
}
}
let _ = self.update_tx.send(RegistryUpdate::Reloaded);
Ok(count)
}
pub fn get_active_params(&self, resource_type: &str) -> Vec<Arc<SearchParameterDefinition>> {
self.params_by_type
.get(resource_type)
.map(|params| {
params
.values()
.filter(|p| p.status.is_usable())
.cloned()
.collect()
})
.unwrap_or_default()
}
pub fn get_all_params(&self, resource_type: &str) -> Vec<Arc<SearchParameterDefinition>> {
self.params_by_type
.get(resource_type)
.map(|params| params.values().cloned().collect())
.unwrap_or_default()
}
pub fn get_param(
&self,
resource_type: &str,
code: &str,
) -> Option<Arc<SearchParameterDefinition>> {
self.params_by_type
.get(resource_type)
.and_then(|params| params.get(code))
.cloned()
}
pub fn get_by_url(&self, url: &str) -> Option<Arc<SearchParameterDefinition>> {
self.params_by_url.get(url).cloned()
}
pub fn register(&mut self, param: SearchParameterDefinition) -> Result<(), RegistryError> {
if self.params_by_url.contains_key(¶m.url) {
return Err(RegistryError::DuplicateUrl { url: param.url });
}
let url = param.url.clone();
self.register_internal(param);
let _ = self.update_tx.send(RegistryUpdate::Added(url));
Ok(())
}
fn register_internal(&mut self, param: SearchParameterDefinition) {
let param = Arc::new(param);
self.params_by_url
.insert(param.url.clone(), Arc::clone(¶m));
for base in ¶m.base {
self.params_by_type
.entry(base.clone())
.or_default()
.insert(param.code.clone(), Arc::clone(¶m));
}
}
pub fn update_status(
&mut self,
url: &str,
status: SearchParameterStatus,
) -> Result<(), RegistryError> {
let old_param = self
.params_by_url
.get(url)
.ok_or_else(|| RegistryError::NotFound {
identifier: url.to_string(),
})?;
let mut new_def = (**old_param).clone();
new_def.status = status;
let new_param = Arc::new(new_def);
self.params_by_url
.insert(url.to_string(), Arc::clone(&new_param));
for base in &new_param.base {
if let Some(type_params) = self.params_by_type.get_mut(base) {
type_params.insert(new_param.code.clone(), Arc::clone(&new_param));
}
}
let _ = self
.update_tx
.send(RegistryUpdate::StatusChanged(url.to_string(), status));
Ok(())
}
pub fn unregister(&mut self, url: &str) -> Result<(), RegistryError> {
let param = self
.params_by_url
.remove(url)
.ok_or_else(|| RegistryError::NotFound {
identifier: url.to_string(),
})?;
for base in ¶m.base {
if let Some(type_params) = self.params_by_type.get_mut(base) {
type_params.remove(¶m.code);
if type_params.is_empty() {
self.params_by_type.remove(base);
}
}
}
let _ = self
.update_tx
.send(RegistryUpdate::Removed(url.to_string()));
Ok(())
}
pub fn subscribe(&self) -> broadcast::Receiver<RegistryUpdate> {
self.update_tx.subscribe()
}
pub fn resource_types(&self) -> Vec<String> {
self.params_by_type.keys().cloned().collect()
}
pub fn all_urls(&self) -> Vec<String> {
self.params_by_url.keys().cloned().collect()
}
}
impl Default for SearchParameterRegistry {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for SearchParameterRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SearchParameterRegistry")
.field("params_count", &self.params_by_url.len())
.field(
"resource_types",
&self.params_by_type.keys().collect::<Vec<_>>(),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_parameter_status() {
assert!(SearchParameterStatus::Active.is_usable());
assert!(!SearchParameterStatus::Draft.is_usable());
assert!(!SearchParameterStatus::Retired.is_usable());
assert_eq!(
SearchParameterStatus::from_fhir_status("active"),
Some(SearchParameterStatus::Active)
);
assert_eq!(SearchParameterStatus::Active.to_fhir_status(), "active");
}
#[test]
fn test_search_parameter_definition() {
let def = SearchParameterDefinition::new(
"http://hl7.org/fhir/SearchParameter/Patient-name",
"name",
SearchParamType::String,
"Patient.name",
)
.with_base(vec!["Patient"]);
assert_eq!(def.code, "name");
assert!(def.applies_to("Patient"));
assert!(!def.applies_to("Observation"));
}
#[test]
fn test_registry_operations() {
let mut registry = SearchParameterRegistry::new();
let def = SearchParameterDefinition::new(
"http://example.org/sp/test",
"test",
SearchParamType::String,
"Patient.test",
)
.with_base(vec!["Patient"]);
registry.register(def.clone()).unwrap();
assert_eq!(registry.len(), 1);
let found = registry.get_by_url("http://example.org/sp/test");
assert!(found.is_some());
let found = registry.get_param("Patient", "test");
assert!(found.is_some());
assert_eq!(found.unwrap().code, "test");
let active = registry.get_active_params("Patient");
assert_eq!(active.len(), 1);
registry
.update_status("http://example.org/sp/test", SearchParameterStatus::Retired)
.unwrap();
let active = registry.get_active_params("Patient");
assert_eq!(active.len(), 0);
registry.unregister("http://example.org/sp/test").unwrap();
assert_eq!(registry.len(), 0);
}
#[test]
fn test_duplicate_url_error() {
let mut registry = SearchParameterRegistry::new();
let def = SearchParameterDefinition::new(
"http://example.org/sp/test",
"test",
SearchParamType::String,
"Patient.test",
)
.with_base(vec!["Patient"]);
registry.register(def.clone()).unwrap();
let result = registry.register(def);
assert!(matches!(result, Err(RegistryError::DuplicateUrl { .. })));
}
}