Skip to main content

helios_persistence/search/
registry.rs

1//! SearchParameter Registry.
2//!
3//! The registry maintains an in-memory cache of all active SearchParameters,
4//! indexed by both (resource_type, param_code) and canonical URL.
5
6use 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/// Status of a SearchParameter.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
19#[serde(rename_all = "lowercase")]
20pub enum SearchParameterStatus {
21    /// Active - can be used in searches.
22    #[default]
23    Active,
24    /// Draft - informational, not yet active.
25    Draft,
26    /// Retired - disabled, not usable.
27    Retired,
28}
29
30impl SearchParameterStatus {
31    /// Parse from FHIR status string.
32    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    /// Convert to FHIR status string.
42    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    /// Returns true if this status allows the parameter to be used in searches.
51    pub fn is_usable(&self) -> bool {
52        *self == SearchParameterStatus::Active
53    }
54}
55
56/// Source of a SearchParameter definition.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
58#[serde(rename_all = "lowercase")]
59pub enum SearchParameterSource {
60    /// Built-in standard parameters (bundled at compile time).
61    #[default]
62    Embedded,
63    /// POSTed SearchParameter resources (persisted in database).
64    Stored,
65    /// Runtime configuration file.
66    Config,
67}
68
69/// Component of a composite search parameter.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct CompositeComponentDef {
72    /// Definition URL of the component parameter.
73    pub definition: String,
74    /// FHIRPath expression for extracting this component.
75    pub expression: String,
76}
77
78/// Complete definition of a SearchParameter.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SearchParameterDefinition {
81    /// Canonical URL (unique identifier).
82    pub url: String,
83
84    /// Parameter code (the URL param name, e.g., "name", "identifier").
85    pub code: String,
86
87    /// Human-readable name.
88    pub name: Option<String>,
89
90    /// Description of the parameter.
91    pub description: Option<String>,
92
93    /// The parameter type.
94    pub param_type: SearchParamType,
95
96    /// FHIRPath expression for extracting values.
97    pub expression: String,
98
99    /// Resource types this parameter applies to.
100    pub base: Vec<String>,
101
102    /// Target resource types (for reference parameters).
103    pub target: Option<Vec<String>>,
104
105    /// Components (for composite parameters).
106    pub component: Option<Vec<CompositeComponentDef>>,
107
108    /// Current status.
109    pub status: SearchParameterStatus,
110
111    /// Source of this definition.
112    pub source: SearchParameterSource,
113
114    /// Supported modifiers.
115    pub modifier: Option<Vec<String>>,
116
117    /// Whether multiple values should use AND or OR logic.
118    pub multiple_or: Option<bool>,
119    /// Whether multiple parameters should use AND or OR logic.
120    pub multiple_and: Option<bool>,
121
122    /// Comparators supported (for number/date/quantity).
123    pub comparator: Option<Vec<String>>,
124
125    /// XPath expression (legacy, for reference).
126    pub xpath: Option<String>,
127}
128
129impl SearchParameterDefinition {
130    /// Creates a new SearchParameter definition.
131    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    /// Sets the base resource types.
158    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    /// Sets target types for reference parameters.
168    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    /// Sets the source.
178    pub fn with_source(mut self, source: SearchParameterSource) -> Self {
179        self.source = source;
180        self
181    }
182
183    /// Sets the status.
184    pub fn with_status(mut self, status: SearchParameterStatus) -> Self {
185        self.status = status;
186        self
187    }
188
189    /// Returns whether this is a composite parameter.
190    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    /// Returns whether this parameter applies to the given resource type.
200    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/// Update notification for registry changes.
208#[derive(Debug, Clone)]
209pub enum RegistryUpdate {
210    /// A parameter was added.
211    Added(String),
212    /// A parameter was removed.
213    Removed(String),
214    /// A parameter's status changed.
215    StatusChanged(String, SearchParameterStatus),
216    /// Registry was bulk-reloaded.
217    Reloaded,
218}
219
220/// In-memory registry of SearchParameter definitions.
221///
222/// Provides fast lookup by (resource_type, param_code) and by URL.
223/// Notifies subscribers when parameters are added, removed, or changed.
224pub struct SearchParameterRegistry {
225    /// Parameters indexed by (resource_type, param_code).
226    params_by_type: HashMap<String, HashMap<String, Arc<SearchParameterDefinition>>>,
227
228    /// Parameters indexed by canonical URL.
229    params_by_url: HashMap<String, Arc<SearchParameterDefinition>>,
230
231    /// Notification channel for registry updates.
232    update_tx: broadcast::Sender<RegistryUpdate>,
233}
234
235impl SearchParameterRegistry {
236    /// Creates a new empty registry.
237    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    /// Returns the number of registered parameters.
247    pub fn len(&self) -> usize {
248        self.params_by_url.len()
249    }
250
251    /// Returns true if the registry is empty.
252    pub fn is_empty(&self) -> bool {
253        self.params_by_url.is_empty()
254    }
255
256    /// Loads all parameters from a loader.
257    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            // Skip duplicates silently during bulk load
266            if !self.params_by_url.contains_key(&param.url) {
267                self.register_internal(param);
268            }
269        }
270
271        let _ = self.update_tx.send(RegistryUpdate::Reloaded);
272        Ok(count)
273    }
274
275    /// Gets all active parameters for a resource type.
276    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    /// Gets all parameters for a resource type (including inactive).
290    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    /// Gets a specific parameter by resource type and code.
298    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    /// Gets a parameter by its canonical URL.
310    pub fn get_by_url(&self, url: &str) -> Option<Arc<SearchParameterDefinition>> {
311        self.params_by_url.get(url).cloned()
312    }
313
314    /// Registers a new parameter.
315    pub fn register(&mut self, param: SearchParameterDefinition) -> Result<(), RegistryError> {
316        if self.params_by_url.contains_key(&param.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    /// Internal registration without duplicate checking.
328    fn register_internal(&mut self, param: SearchParameterDefinition) {
329        let param = Arc::new(param);
330
331        // Index by URL
332        self.params_by_url
333            .insert(param.url.clone(), Arc::clone(&param));
334
335        // Index by (resource_type, code) for each base type
336        for base in &param.base {
337            self.params_by_type
338                .entry(base.clone())
339                .or_default()
340                .insert(param.code.clone(), Arc::clone(&param));
341        }
342    }
343
344    /// Updates a parameter's status.
345    pub fn update_status(
346        &mut self,
347        url: &str,
348        status: SearchParameterStatus,
349    ) -> Result<(), RegistryError> {
350        // We need to create a new Arc with the updated status
351        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        // Create updated definition
359        let mut new_def = (**old_param).clone();
360        new_def.status = status;
361        let new_param = Arc::new(new_def);
362
363        // Update URL index
364        self.params_by_url
365            .insert(url.to_string(), Arc::clone(&new_param));
366
367        // Update type indexes
368        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    /// Removes a parameter from the registry.
382    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        // Remove from type indexes
391        for base in &param.base {
392            if let Some(type_params) = self.params_by_type.get_mut(base) {
393                type_params.remove(&param.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    /// Subscribes to registry updates.
408    pub fn subscribe(&self) -> broadcast::Receiver<RegistryUpdate> {
409        self.update_tx.subscribe()
410    }
411
412    /// Returns all resource types that have registered parameters.
413    pub fn resource_types(&self) -> Vec<String> {
414        self.params_by_type.keys().cloned().collect()
415    }
416
417    /// Returns all registered parameter URLs.
418    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        // Register
486        registry.register(def.clone()).unwrap();
487        assert_eq!(registry.len(), 1);
488
489        // Get by URL
490        let found = registry.get_by_url("http://example.org/sp/test");
491        assert!(found.is_some());
492
493        // Get by type and code
494        let found = registry.get_param("Patient", "test");
495        assert!(found.is_some());
496        assert_eq!(found.unwrap().code, "test");
497
498        // Get active params
499        let active = registry.get_active_params("Patient");
500        assert_eq!(active.len(), 1);
501
502        // Update status
503        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        // Unregister
510        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}