helios_persistence/search/
registry.rs1use std::collections::HashMap;
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10use tokio::sync::broadcast;
11
12use crate::types::SearchParamType;
13
14use super::errors::RegistryError;
15use super::loader::SearchParameterLoader;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
19#[serde(rename_all = "lowercase")]
20pub enum SearchParameterStatus {
21 #[default]
23 Active,
24 Draft,
26 Retired,
28}
29
30impl SearchParameterStatus {
31 pub fn from_fhir_status(s: &str) -> Option<Self> {
33 match s.to_lowercase().as_str() {
34 "active" => Some(SearchParameterStatus::Active),
35 "draft" => Some(SearchParameterStatus::Draft),
36 "retired" => Some(SearchParameterStatus::Retired),
37 _ => None,
38 }
39 }
40
41 pub fn to_fhir_status(&self) -> &'static str {
43 match self {
44 SearchParameterStatus::Active => "active",
45 SearchParameterStatus::Draft => "draft",
46 SearchParameterStatus::Retired => "retired",
47 }
48 }
49
50 pub fn is_usable(&self) -> bool {
52 *self == SearchParameterStatus::Active
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
58#[serde(rename_all = "lowercase")]
59pub enum SearchParameterSource {
60 #[default]
62 Embedded,
63 Stored,
65 Config,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct CompositeComponentDef {
72 pub definition: String,
74 pub expression: String,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SearchParameterDefinition {
81 pub url: String,
83
84 pub code: String,
86
87 pub name: Option<String>,
89
90 pub description: Option<String>,
92
93 pub param_type: SearchParamType,
95
96 pub expression: String,
98
99 pub base: Vec<String>,
101
102 pub target: Option<Vec<String>>,
104
105 pub component: Option<Vec<CompositeComponentDef>>,
107
108 pub status: SearchParameterStatus,
110
111 pub source: SearchParameterSource,
113
114 pub modifier: Option<Vec<String>>,
116
117 pub multiple_or: Option<bool>,
119 pub multiple_and: Option<bool>,
121
122 pub comparator: Option<Vec<String>>,
124
125 pub xpath: Option<String>,
127}
128
129impl SearchParameterDefinition {
130 pub fn new(
132 url: impl Into<String>,
133 code: impl Into<String>,
134 param_type: SearchParamType,
135 expression: impl Into<String>,
136 ) -> Self {
137 Self {
138 url: url.into(),
139 code: code.into(),
140 name: None,
141 description: None,
142 param_type,
143 expression: expression.into(),
144 base: Vec::new(),
145 target: None,
146 component: None,
147 status: SearchParameterStatus::Active,
148 source: SearchParameterSource::Embedded,
149 modifier: None,
150 multiple_or: None,
151 multiple_and: None,
152 comparator: None,
153 xpath: None,
154 }
155 }
156
157 pub fn with_base<I, S>(mut self, base: I) -> Self
159 where
160 I: IntoIterator<Item = S>,
161 S: Into<String>,
162 {
163 self.base = base.into_iter().map(Into::into).collect();
164 self
165 }
166
167 pub fn with_targets<I, S>(mut self, targets: I) -> Self
169 where
170 I: IntoIterator<Item = S>,
171 S: Into<String>,
172 {
173 self.target = Some(targets.into_iter().map(Into::into).collect());
174 self
175 }
176
177 pub fn with_source(mut self, source: SearchParameterSource) -> Self {
179 self.source = source;
180 self
181 }
182
183 pub fn with_status(mut self, status: SearchParameterStatus) -> Self {
185 self.status = status;
186 self
187 }
188
189 pub fn is_composite(&self) -> bool {
191 self.param_type == SearchParamType::Composite
192 && self
193 .component
194 .as_ref()
195 .map(|c| !c.is_empty())
196 .unwrap_or(false)
197 }
198
199 pub fn applies_to(&self, resource_type: &str) -> bool {
201 self.base
202 .iter()
203 .any(|b| b == resource_type || b == "Resource" || b == "DomainResource")
204 }
205}
206
207#[derive(Debug, Clone)]
209pub enum RegistryUpdate {
210 Added(String),
212 Removed(String),
214 StatusChanged(String, SearchParameterStatus),
216 Reloaded,
218}
219
220pub struct SearchParameterRegistry {
225 params_by_type: HashMap<String, HashMap<String, Arc<SearchParameterDefinition>>>,
227
228 params_by_url: HashMap<String, Arc<SearchParameterDefinition>>,
230
231 update_tx: broadcast::Sender<RegistryUpdate>,
233}
234
235impl SearchParameterRegistry {
236 pub fn new() -> Self {
238 let (update_tx, _) = broadcast::channel(64);
239 Self {
240 params_by_type: HashMap::new(),
241 params_by_url: HashMap::new(),
242 update_tx,
243 }
244 }
245
246 pub fn len(&self) -> usize {
248 self.params_by_url.len()
249 }
250
251 pub fn is_empty(&self) -> bool {
253 self.params_by_url.is_empty()
254 }
255
256 pub async fn load_all(
258 &mut self,
259 loader: &SearchParameterLoader,
260 ) -> Result<usize, super::errors::LoaderError> {
261 let params = loader.load_embedded()?;
262 let count = params.len();
263
264 for param in params {
265 if !self.params_by_url.contains_key(¶m.url) {
267 self.register_internal(param);
268 }
269 }
270
271 let _ = self.update_tx.send(RegistryUpdate::Reloaded);
272 Ok(count)
273 }
274
275 pub fn get_active_params(&self, resource_type: &str) -> Vec<Arc<SearchParameterDefinition>> {
277 self.params_by_type
278 .get(resource_type)
279 .map(|params| {
280 params
281 .values()
282 .filter(|p| p.status.is_usable())
283 .cloned()
284 .collect()
285 })
286 .unwrap_or_default()
287 }
288
289 pub fn get_all_params(&self, resource_type: &str) -> Vec<Arc<SearchParameterDefinition>> {
291 self.params_by_type
292 .get(resource_type)
293 .map(|params| params.values().cloned().collect())
294 .unwrap_or_default()
295 }
296
297 pub fn get_param(
299 &self,
300 resource_type: &str,
301 code: &str,
302 ) -> Option<Arc<SearchParameterDefinition>> {
303 self.params_by_type
304 .get(resource_type)
305 .and_then(|params| params.get(code))
306 .cloned()
307 }
308
309 pub fn get_by_url(&self, url: &str) -> Option<Arc<SearchParameterDefinition>> {
311 self.params_by_url.get(url).cloned()
312 }
313
314 pub fn register(&mut self, param: SearchParameterDefinition) -> Result<(), RegistryError> {
316 if self.params_by_url.contains_key(¶m.url) {
317 return Err(RegistryError::DuplicateUrl { url: param.url });
318 }
319
320 let url = param.url.clone();
321 self.register_internal(param);
322 let _ = self.update_tx.send(RegistryUpdate::Added(url));
323
324 Ok(())
325 }
326
327 fn register_internal(&mut self, param: SearchParameterDefinition) {
329 let param = Arc::new(param);
330
331 self.params_by_url
333 .insert(param.url.clone(), Arc::clone(¶m));
334
335 for base in ¶m.base {
337 self.params_by_type
338 .entry(base.clone())
339 .or_default()
340 .insert(param.code.clone(), Arc::clone(¶m));
341 }
342 }
343
344 pub fn update_status(
346 &mut self,
347 url: &str,
348 status: SearchParameterStatus,
349 ) -> Result<(), RegistryError> {
350 let old_param = self
352 .params_by_url
353 .get(url)
354 .ok_or_else(|| RegistryError::NotFound {
355 identifier: url.to_string(),
356 })?;
357
358 let mut new_def = (**old_param).clone();
360 new_def.status = status;
361 let new_param = Arc::new(new_def);
362
363 self.params_by_url
365 .insert(url.to_string(), Arc::clone(&new_param));
366
367 for base in &new_param.base {
369 if let Some(type_params) = self.params_by_type.get_mut(base) {
370 type_params.insert(new_param.code.clone(), Arc::clone(&new_param));
371 }
372 }
373
374 let _ = self
375 .update_tx
376 .send(RegistryUpdate::StatusChanged(url.to_string(), status));
377
378 Ok(())
379 }
380
381 pub fn unregister(&mut self, url: &str) -> Result<(), RegistryError> {
383 let param = self
384 .params_by_url
385 .remove(url)
386 .ok_or_else(|| RegistryError::NotFound {
387 identifier: url.to_string(),
388 })?;
389
390 for base in ¶m.base {
392 if let Some(type_params) = self.params_by_type.get_mut(base) {
393 type_params.remove(¶m.code);
394 if type_params.is_empty() {
395 self.params_by_type.remove(base);
396 }
397 }
398 }
399
400 let _ = self
401 .update_tx
402 .send(RegistryUpdate::Removed(url.to_string()));
403
404 Ok(())
405 }
406
407 pub fn subscribe(&self) -> broadcast::Receiver<RegistryUpdate> {
409 self.update_tx.subscribe()
410 }
411
412 pub fn resource_types(&self) -> Vec<String> {
414 self.params_by_type.keys().cloned().collect()
415 }
416
417 pub fn all_urls(&self) -> Vec<String> {
419 self.params_by_url.keys().cloned().collect()
420 }
421}
422
423impl Default for SearchParameterRegistry {
424 fn default() -> Self {
425 Self::new()
426 }
427}
428
429impl std::fmt::Debug for SearchParameterRegistry {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 f.debug_struct("SearchParameterRegistry")
432 .field("params_count", &self.params_by_url.len())
433 .field(
434 "resource_types",
435 &self.params_by_type.keys().collect::<Vec<_>>(),
436 )
437 .finish()
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_search_parameter_status() {
447 assert!(SearchParameterStatus::Active.is_usable());
448 assert!(!SearchParameterStatus::Draft.is_usable());
449 assert!(!SearchParameterStatus::Retired.is_usable());
450
451 assert_eq!(
452 SearchParameterStatus::from_fhir_status("active"),
453 Some(SearchParameterStatus::Active)
454 );
455 assert_eq!(SearchParameterStatus::Active.to_fhir_status(), "active");
456 }
457
458 #[test]
459 fn test_search_parameter_definition() {
460 let def = SearchParameterDefinition::new(
461 "http://hl7.org/fhir/SearchParameter/Patient-name",
462 "name",
463 SearchParamType::String,
464 "Patient.name",
465 )
466 .with_base(vec!["Patient"]);
467
468 assert_eq!(def.code, "name");
469 assert!(def.applies_to("Patient"));
470 assert!(!def.applies_to("Observation"));
471 }
472
473 #[test]
474 fn test_registry_operations() {
475 let mut registry = SearchParameterRegistry::new();
476
477 let def = SearchParameterDefinition::new(
478 "http://example.org/sp/test",
479 "test",
480 SearchParamType::String,
481 "Patient.test",
482 )
483 .with_base(vec!["Patient"]);
484
485 registry.register(def.clone()).unwrap();
487 assert_eq!(registry.len(), 1);
488
489 let found = registry.get_by_url("http://example.org/sp/test");
491 assert!(found.is_some());
492
493 let found = registry.get_param("Patient", "test");
495 assert!(found.is_some());
496 assert_eq!(found.unwrap().code, "test");
497
498 let active = registry.get_active_params("Patient");
500 assert_eq!(active.len(), 1);
501
502 registry
504 .update_status("http://example.org/sp/test", SearchParameterStatus::Retired)
505 .unwrap();
506 let active = registry.get_active_params("Patient");
507 assert_eq!(active.len(), 0);
508
509 registry.unregister("http://example.org/sp/test").unwrap();
511 assert_eq!(registry.len(), 0);
512 }
513
514 #[test]
515 fn test_duplicate_url_error() {
516 let mut registry = SearchParameterRegistry::new();
517
518 let def = SearchParameterDefinition::new(
519 "http://example.org/sp/test",
520 "test",
521 SearchParamType::String,
522 "Patient.test",
523 )
524 .with_base(vec!["Patient"]);
525
526 registry.register(def.clone()).unwrap();
527
528 let result = registry.register(def);
529 assert!(matches!(result, Err(RegistryError::DuplicateUrl { .. })));
530 }
531}