Skip to main content

camel_core/shared/components/domain/
registry.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use camel_api::CamelError;
5use camel_api::component_metadata::ComponentMetadata;
6use camel_component_api::Component;
7
8/// Registry that stores components by their URI scheme.
9///
10/// Also harvests and indexes [`ComponentMetadata`] for each registered
11/// component, so the metadata can be queried through a
12/// [`ComponentMetadataCatalog`](camel_api::component_metadata::ComponentMetadataCatalog)
13/// without re-invoking the component.
14pub struct Registry {
15    components: HashMap<String, Arc<dyn Component>>,
16    metadata: HashMap<String, ComponentMetadata>,
17}
18
19impl Registry {
20    /// Create an empty registry.
21    pub fn new() -> Self {
22        Self {
23            components: HashMap::new(),
24            metadata: HashMap::new(),
25        }
26    }
27
28    /// Register a component. Replaces any existing component with the same scheme.
29    ///
30    /// Harvests the component's [`ComponentMetadata`] and indexes it by scheme
31    /// in parallel with the component insertion. Validates that the metadata's
32    /// scheme matches the component's scheme, normalizing on mismatch with a
33    /// warning log.
34    pub fn register(&mut self, component: Arc<dyn Component>) {
35        let scheme = component.scheme().to_string();
36        let mut metadata = component.metadata();
37        if let Err(e) = metadata.validate_scheme(&scheme) {
38            tracing::warn!(scheme = %scheme, error = %e, "metadata scheme mismatch, normalizing");
39            metadata.scheme = scheme.clone();
40        }
41        self.metadata.insert(scheme.clone(), metadata);
42        self.components.insert(scheme, component);
43    }
44
45    /// Look up a component by scheme.
46    pub fn get(&self, scheme: &str) -> Option<Arc<dyn Component>> {
47        self.components.get(scheme).cloned()
48    }
49
50    /// Look up a component by scheme, returning an error if not found.
51    pub fn get_or_err(&self, scheme: &str) -> Result<Arc<dyn Component>, CamelError> {
52        self.get(scheme)
53            .ok_or_else(|| CamelError::ComponentNotFound(scheme.to_string()))
54    }
55
56    /// Look up harvested metadata for a component by scheme.
57    pub fn get_metadata(&self, scheme: &str) -> Option<ComponentMetadata> {
58        self.metadata.get(scheme).cloned()
59    }
60
61    /// Return metadata for every registered component.
62    pub fn all_metadata(&self) -> Vec<ComponentMetadata> {
63        self.metadata.values().cloned().collect()
64    }
65
66    /// Return the schemes of every registered component's metadata.
67    pub fn metadata_schemes(&self) -> Vec<String> {
68        self.metadata.keys().cloned().collect()
69    }
70
71    /// Returns the number of registered components.
72    pub fn len(&self) -> usize {
73        self.components.len()
74    }
75
76    /// Returns true if no components are registered.
77    pub fn is_empty(&self) -> bool {
78        self.components.is_empty()
79    }
80}
81
82impl Default for Registry {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88/// Adapter that lets `Registry` participate as a `ComponentContext`.
89///
90/// Wraps the shared `Arc<Mutex<Registry>>` and delegates `resolve_component`
91/// to `Registry::get`. The metrics collector is threaded in from the
92/// composition root (camel-cli) at construction; it is the ADR-0066
93/// late-bound handle, not a backend snapshot, so late registrations flow
94/// through [`camel_component_api::ComponentContext::metrics`] without
95/// re-snapshotting. The
96/// components-lever snapshot gates only the component-operations family —
97/// the error family is never lever-gated. When no collector is wired
98/// (e.g. compile-time security scan, standalone examples without a
99/// live context), construction resolves
100/// [`camel_api::NoOpMetrics`].
101pub struct RegistryComponentContext {
102    registry: Arc<std::sync::Mutex<Registry>>,
103    metrics: Arc<dyn camel_api::MetricsCollector>,
104    components_enabled: bool,
105}
106
107impl RegistryComponentContext {
108    /// Builds the context, resolving the collector once: `metrics` when
109    /// wired, `NoOpMetrics` otherwise.
110    pub fn new(
111        registry: Arc<std::sync::Mutex<Registry>>,
112        metrics: Option<Arc<dyn camel_api::MetricsCollector>>,
113        components_enabled: bool,
114    ) -> Self {
115        Self {
116            registry,
117            metrics: metrics.unwrap_or_else(|| Arc::new(camel_api::NoOpMetrics)),
118            components_enabled,
119        }
120    }
121}
122
123impl camel_component_api::ComponentContext for RegistryComponentContext {
124    fn resolve_component(&self, scheme: &str) -> Option<Arc<dyn camel_component_api::Component>> {
125        self.registry.lock().ok()?.get(scheme)
126    }
127
128    fn resolve_language(&self, _name: &str) -> Option<Arc<dyn camel_language_api::Language>> {
129        None
130    }
131
132    fn metrics(&self) -> Arc<dyn camel_api::MetricsCollector> {
133        self.metrics.clone()
134    }
135
136    fn component_metrics_enabled(&self) -> bool {
137        self.components_enabled
138    }
139
140    fn platform_service(&self) -> Arc<dyn camel_api::PlatformService> {
141        Arc::new(camel_api::NoopPlatformService::default())
142    }
143
144    fn register_route_health_check(
145        &self,
146        _route_id: &str,
147        _check: Arc<dyn camel_api::AsyncHealthCheck>,
148    ) {
149    }
150
151    fn unregister_route_health_check(&self, _route_id: &str) {}
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use std::time::Duration;
158
159    use camel_api::MetricsCollector;
160    use camel_api::component_metadata::ComponentMetadata;
161    use camel_component_api::{ComponentContext, RuntimeObservability};
162    use camel_component_log::LogComponent;
163    use camel_component_timer::TimerComponent;
164
165    /// Recording state owned by the [`RecordingMetrics`] double. Owned
166    /// `String`s throughout — the facade passes formatted labels.
167    struct RecordingState {
168        errors: Vec<(String, String)>,
169        component_ops: Vec<(String, String, String)>,
170        counters: Vec<(String, f64)>,
171    }
172
173    /// Local recording double capturing the families the registry context
174    /// can emit: error pairs, component-op triples, generic counters. All
175    /// other trait methods are empty.
176    struct RecordingMetrics {
177        state: Arc<std::sync::Mutex<RecordingState>>,
178    }
179
180    impl RecordingMetrics {
181        fn new() -> Self {
182            Self {
183                state: Arc::new(std::sync::Mutex::new(RecordingState {
184                    errors: Vec::new(),
185                    component_ops: Vec::new(),
186                    counters: Vec::new(),
187                })),
188            }
189        }
190
191        fn recorded_errors(&self) -> Vec<(String, String)> {
192            self.state
193                .lock()
194                .expect("recording state lock")
195                .errors
196                .clone()
197        }
198
199        fn recorded_component_operations(&self) -> Vec<(String, String, String)> {
200            self.state
201                .lock()
202                .expect("recording state lock")
203                .component_ops
204                .clone()
205        }
206
207        fn recorded_counters(&self) -> Vec<(String, f64)> {
208            self.state
209                .lock()
210                .expect("recording state lock")
211                .counters
212                .clone()
213        }
214    }
215
216    impl MetricsCollector for RecordingMetrics {
217        fn record_exchange_duration(&self, _route_id: &str, _duration: Duration) {}
218
219        fn increment_errors(&self, route_id: &str, error_type: &str) {
220            self.state
221                .lock()
222                .expect("recording state lock")
223                .errors
224                .push((route_id.to_string(), error_type.to_string()));
225        }
226
227        fn increment_exchanges(&self, _route_id: &str) {}
228
229        fn set_queue_depth(&self, _queue: &str, _depth: usize) {}
230
231        fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
232
233        fn record_counter(&self, name: &str, value: f64, _labels: &[(&str, &str)]) {
234            self.state
235                .lock()
236                .expect("recording state lock")
237                .counters
238                .push((name.to_string(), value));
239        }
240
241        fn record_component_operation(&self, component: &str, operation: &str, outcome: &str) {
242            self.state
243                .lock()
244                .expect("recording state lock")
245                .component_ops
246                .push((
247                    component.to_string(),
248                    operation.to_string(),
249                    outcome.to_string(),
250                ));
251        }
252    }
253
254    #[test]
255    fn registry_starts_empty() {
256        let registry = Registry::new();
257        assert!(registry.is_empty());
258        assert_eq!(registry.len(), 0);
259        assert!(registry.get("timer").is_none());
260    }
261
262    #[test]
263    fn registry_registers_and_gets_components() {
264        let mut registry = Registry::new();
265        registry.register(Arc::new(TimerComponent::new()));
266        registry.register(Arc::new(LogComponent::new()));
267
268        assert_eq!(registry.len(), 2);
269        assert!(registry.get("timer").is_some());
270        assert!(registry.get("log").is_some());
271        assert!(!registry.is_empty());
272    }
273
274    #[test]
275    fn registry_get_or_err_reports_missing_component() {
276        let mut registry = Registry::new();
277        registry.register(Arc::new(TimerComponent::new()));
278
279        let err = match registry.get_or_err("missing") {
280            Ok(_) => panic!("must fail"),
281            Err(err) => err,
282        };
283        assert!(matches!(err, CamelError::ComponentNotFound(_)));
284    }
285
286    #[test]
287    fn registry_replaces_component_with_same_scheme() {
288        let mut registry = Registry::new();
289        registry.register(Arc::new(TimerComponent::new()));
290        registry.register(Arc::new(TimerComponent::new()));
291
292        assert_eq!(registry.len(), 1);
293        assert!(registry.get("timer").is_some());
294        assert_eq!(registry.all_metadata().len(), 1);
295    }
296
297    #[test]
298    fn registry_harvests_metadata_on_register() {
299        let mut registry = Registry::new();
300        registry.register(Arc::new(TimerComponent::new()));
301
302        let meta = registry.get_metadata("timer");
303        assert!(meta.is_some());
304        let meta = meta.unwrap(); // allow-unwrap
305        assert_eq!(meta.scheme, "timer");
306        assert_eq!(meta.schema_version, ComponentMetadata::SCHEMA_VERSION);
307    }
308
309    #[test]
310    fn registry_all_metadata_returns_all_schemes() {
311        let mut registry = Registry::new();
312        registry.register(Arc::new(TimerComponent::new()));
313        registry.register(Arc::new(LogComponent::new()));
314
315        let all = registry.all_metadata();
316        assert_eq!(all.len(), 2);
317    }
318
319    #[test]
320    fn registry_metadata_schemes_lists_all_keys() {
321        let mut registry = Registry::new();
322        registry.register(Arc::new(TimerComponent::new()));
323        registry.register(Arc::new(LogComponent::new()));
324
325        let mut schemes = registry.metadata_schemes();
326        schemes.sort();
327        assert_eq!(schemes, vec!["log".to_string(), "timer".to_string()]);
328    }
329
330    #[test]
331    fn metrics_returns_wired_collector_and_is_stable() {
332        let registry = Arc::new(std::sync::Mutex::new(Registry::new()));
333        let collector = Arc::new(RecordingMetrics::new());
334        let wired_dyn: Arc<dyn MetricsCollector> = collector.clone();
335        let ctx = RegistryComponentContext::new(registry, Some(collector), false);
336
337        let first = ComponentContext::metrics(&ctx);
338        let second = ComponentContext::metrics(&ctx);
339        assert!(Arc::ptr_eq(&first, &wired_dyn));
340        assert!(Arc::ptr_eq(&second, &wired_dyn));
341    }
342
343    #[test]
344    fn component_metrics_enabled_reflects_constructor_lever() {
345        let on = RegistryComponentContext::new(
346            Arc::new(std::sync::Mutex::new(Registry::new())),
347            None,
348            true,
349        );
350        let off = RegistryComponentContext::new(
351            Arc::new(std::sync::Mutex::new(Registry::new())),
352            None,
353            false,
354        );
355
356        assert!(ComponentContext::component_metrics_enabled(&on));
357        assert!(!ComponentContext::component_metrics_enabled(&off));
358    }
359
360    #[test]
361    fn facade_error_family_reaches_wired_collector_with_lever_off() {
362        let registry = Arc::new(std::sync::Mutex::new(Registry::new()));
363        let collector = Arc::new(RecordingMetrics::new());
364        let ctx = RegistryComponentContext::new(registry, Some(collector.clone()), false);
365
366        let facade = RuntimeObservability::component_metrics(&ctx);
367        facade.observe("wasm", "invoke", true);
368
369        assert_eq!(
370            collector.recorded_errors(),
371            vec![("wasm".to_string(), "e:wasm:invoke".to_string())]
372        );
373    }
374
375    #[test]
376    fn facade_component_family_gated_by_lever() {
377        let registry = Arc::new(std::sync::Mutex::new(Registry::new()));
378        let collector = Arc::new(RecordingMetrics::new());
379        let ctx_on = RegistryComponentContext::new(registry.clone(), Some(collector.clone()), true);
380        let ctx_off = RegistryComponentContext::new(registry, Some(collector.clone()), false);
381
382        RuntimeObservability::component_metrics(&ctx_on).observe("wasm", "invoke", false);
383        RuntimeObservability::component_metrics(&ctx_off).observe("wasm", "invoke", false);
384
385        assert_eq!(
386            collector.recorded_component_operations(),
387            vec![(
388                "wasm".to_string(),
389                "invoke".to_string(),
390                "success".to_string()
391            )]
392        );
393        assert!(collector.recorded_errors().is_empty());
394        assert!(collector.recorded_counters().is_empty());
395    }
396
397    #[test]
398    fn late_registered_collector_reaches_registry_context() {
399        let registry = Arc::new(std::sync::Mutex::new(Registry::new()));
400        let handle = Arc::new(camel_api::MetricsHandle::new());
401        let handle_dyn: Arc<dyn MetricsCollector> = handle.clone();
402        let recording = Arc::new(RecordingMetrics::new());
403        let ctx = RegistryComponentContext::new(registry, Some(handle_dyn), false);
404
405        handle.register(recording.clone());
406        ComponentContext::metrics(&ctx).increment_errors("wasm", "e:wasm:invoke");
407
408        assert_eq!(
409            recording.recorded_errors(),
410            vec![("wasm".to_string(), "e:wasm:invoke".to_string())]
411        );
412    }
413
414    #[test]
415    fn none_falls_back_to_noop_semantics() {
416        let registry = Arc::new(std::sync::Mutex::new(Registry::new()));
417        let ctx = RegistryComponentContext::new(registry, None, false);
418
419        // None of these may panic: metrics() resolves the fallback
420        // collector, the facade builds over it, and the error flows into
421        // NoOp silently.
422        ComponentContext::metrics(&ctx);
423        RuntimeObservability::component_metrics(&ctx).observe("wasm", "invoke", true);
424
425        assert!(!ComponentContext::component_metrics_enabled(&ctx));
426    }
427
428    #[test]
429    fn resolve_component_unaffected_by_observability_params() {
430        let mut registry = Registry::new();
431        registry.register(Arc::new(TimerComponent::new()));
432        let registry = Arc::new(std::sync::Mutex::new(registry));
433        let ctx = RegistryComponentContext::new(registry, None, false);
434
435        assert!(ctx.resolve_component("timer").is_some());
436    }
437}