Skip to main content

camel_core/
component_metadata_catalog.rs

1//! Runtime implementation of [`ComponentMetadataCatalog`].
2//!
3//! Thin wrapper around the component [`Registry`]'s `Arc<Mutex<Registry>>`
4//! that implements the query trait. Created on-demand via
5//! [`CamelContext::metadata_catalog`](crate::context::CamelContext::metadata_catalog).
6
7use std::sync::{Arc, Mutex};
8
9use camel_api::component_metadata::{ComponentMetadata, ComponentMetadataCatalog};
10
11use crate::shared::components::domain::Registry;
12
13/// Runtime catalog of component metadata backed by the live component
14/// [`Registry`].
15pub struct RuntimeComponentMetadataCatalog {
16    registry: Arc<Mutex<Registry>>,
17}
18
19impl RuntimeComponentMetadataCatalog {
20    /// Wrap an existing `Arc<Mutex<Registry>>` to expose it as a
21    /// [`ComponentMetadataCatalog`].
22    pub fn new(registry: Arc<Mutex<Registry>>) -> Self {
23        Self { registry }
24    }
25}
26
27impl ComponentMetadataCatalog for RuntimeComponentMetadataCatalog {
28    fn get_metadata(&self, scheme: &str) -> Option<ComponentMetadata> {
29        self.registry.lock().ok()?.get_metadata(scheme)
30    }
31
32    fn schemes(&self) -> Vec<String> {
33        self.registry
34            .lock()
35            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
36            .metadata_schemes()
37    }
38
39    fn all_metadata(&self) -> Vec<ComponentMetadata> {
40        self.registry
41            .lock()
42            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
43            .all_metadata()
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use camel_api::CamelError;
51    use camel_api::component_metadata::{
52        CapabilityQuery, ComponentMetadataCatalog, OptionKind, UriOption,
53    };
54    use camel_component_api::{Component, ComponentContext, Endpoint};
55    use camel_component_timer::TimerComponent;
56
57    /// Stand-in for the real `http`/`ws` components in catalog tests.
58    ///
59    /// `Registry::register` only harvests metadata at registration time, so a
60    /// stub carrying the real scheme name and a synthetic non-empty option list
61    /// satisfies the catalog assertions without pulling the cyclic
62    /// `camel-component-http`/`camel-component-ws` dev-deps into `camel-core`.
63    /// The real-option catalog tests for those schemes are relocated to
64    /// `crates/camel-test/tests/core_catalog_real_metadata_test.rs` (ADR-0055).
65    struct StubComponent {
66        scheme: String,
67        metadata: ComponentMetadata,
68    }
69
70    impl StubComponent {
71        fn new(scheme: &str, with_options: Vec<&str>) -> Self {
72            let uri_options: Vec<_> = with_options
73                .into_iter()
74                .map(|name| UriOption::new(name, "stub option", OptionKind::String))
75                .collect();
76            let metadata = ComponentMetadata::minimal(scheme)
77                .with_description("stub component for catalog tests")
78                .with_uri_options(uri_options);
79            Self {
80                scheme: scheme.to_string(),
81                metadata,
82            }
83        }
84    }
85
86    impl Component for StubComponent {
87        fn scheme(&self) -> &str {
88            &self.scheme
89        }
90
91        fn metadata(&self) -> ComponentMetadata {
92            self.metadata.clone()
93        }
94
95        fn create_endpoint(
96            &self,
97            _uri: &str,
98            _ctx: &dyn ComponentContext,
99        ) -> Result<Box<dyn Endpoint>, CamelError> {
100            Err(CamelError::EndpointCreationFailed(
101                "StubComponent: create_endpoint not implemented".to_string(),
102            ))
103        }
104    }
105
106    #[test]
107    fn catalog_exposes_registered_metadata() {
108        let registry = Arc::new(Mutex::new(Registry::new()));
109        registry
110            .lock()
111            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
112            .register(Arc::new(TimerComponent::new()));
113
114        let catalog = RuntimeComponentMetadataCatalog::new(Arc::clone(&registry));
115
116        let meta = catalog.get_metadata("timer");
117        assert!(meta.is_some());
118        assert_eq!(meta.unwrap().scheme, "timer"); // allow-unwrap
119        assert_eq!(catalog.schemes(), vec!["timer".to_string()]);
120        assert_eq!(catalog.all_metadata().len(), 1);
121    }
122
123    #[test]
124    fn catalog_query_capabilities_default_impl() {
125        let registry = Arc::new(Mutex::new(Registry::new()));
126        registry
127            .lock()
128            .expect("mutex poisoned: another thread panicked while holding this lock") // allow-unwrap
129            .register(Arc::new(TimerComponent::new()));
130
131        let catalog = RuntimeComponentMetadataCatalog::new(Arc::clone(&registry));
132
133        // No constraints => all metadata returned via the trait default impl.
134        let results = catalog.query_capabilities(&CapabilityQuery::default());
135        assert_eq!(results.len(), 1);
136    }
137
138    #[test]
139    fn all_components_in_catalog() {
140        use camel_component_container::ContainerComponent;
141        use camel_component_cron::CronComponent;
142        use camel_component_direct::DirectComponent;
143        use camel_component_file::FileComponent;
144        use camel_component_log::LogComponent;
145        use camel_component_mock::MockComponent;
146        use camel_component_opensearch::OpenSearchComponent;
147        use camel_component_seda::SedaComponent;
148        use camel_component_sql::SqlComponent;
149
150        let registry = Arc::new(Mutex::new(Registry::new()));
151        {
152            let mut reg = registry
153                .lock()
154                .expect("mutex poisoned: another thread panicked while holding this lock"); // allow-unwrap
155            reg.register(Arc::new(SqlComponent::new()));
156            reg.register(Arc::new(FileComponent::new()));
157            reg.register(Arc::new(CronComponent::new()));
158            reg.register(Arc::new(OpenSearchComponent::new()));
159            reg.register(Arc::new(StubComponent::new("ws", vec!["opt"])));
160            reg.register(Arc::new(ContainerComponent::new()));
161            reg.register(Arc::new(TimerComponent::new()));
162            reg.register(Arc::new(DirectComponent::new()));
163            reg.register(Arc::new(SedaComponent::new()));
164            reg.register(Arc::new(LogComponent::new()));
165            reg.register(Arc::new(MockComponent::new()));
166            reg.register(Arc::new(StubComponent::new("http", vec!["opt"])));
167        }
168
169        let catalog = RuntimeComponentMetadataCatalog::new(Arc::clone(&registry));
170
171        let expected_schemes: &[&str] = &[
172            "sql",
173            "file",
174            "cron",
175            "opensearch",
176            "ws",
177            "container",
178            "timer",
179            "direct",
180            "seda",
181            "log",
182            "mock",
183            "http",
184        ];
185
186        for scheme in expected_schemes {
187            let meta = catalog
188                .get_metadata(scheme)
189                .unwrap_or_else(|| panic!("missing metadata for scheme '{scheme}'"));
190            // mock may have empty options; others should be non-empty
191            if *scheme != "mock" {
192                assert!(
193                    !meta.uri_options.is_empty(),
194                    "uri_options must be non-empty for scheme '{scheme}'"
195                );
196            }
197            assert!(
198                !meta.scheme.is_empty(),
199                "scheme must be non-empty for '{scheme}'"
200            );
201            assert!(
202                !meta.description.is_empty(),
203                "description must be non-empty for scheme '{scheme}'"
204            );
205        }
206    }
207
208    #[test]
209    fn no_duplicate_option_names_all() {
210        use camel_component_container::ContainerComponent;
211        use camel_component_cron::CronComponent;
212        use camel_component_direct::DirectComponent;
213        use camel_component_file::FileComponent;
214        use camel_component_log::LogComponent;
215        use camel_component_mock::MockComponent;
216        use camel_component_opensearch::OpenSearchComponent;
217        use camel_component_seda::SedaComponent;
218        use camel_component_sql::SqlComponent;
219
220        let registry = Arc::new(Mutex::new(Registry::new()));
221        {
222            let mut reg = registry
223                .lock()
224                .expect("mutex poisoned: another thread panicked while holding this lock"); // allow-unwrap
225            reg.register(Arc::new(SqlComponent::new()));
226            reg.register(Arc::new(FileComponent::new()));
227            reg.register(Arc::new(CronComponent::new()));
228            reg.register(Arc::new(OpenSearchComponent::new()));
229            reg.register(Arc::new(StubComponent::new("ws", vec!["opt"])));
230            reg.register(Arc::new(ContainerComponent::new()));
231            reg.register(Arc::new(TimerComponent::new()));
232            reg.register(Arc::new(DirectComponent::new()));
233            reg.register(Arc::new(SedaComponent::new()));
234            reg.register(Arc::new(LogComponent::new()));
235            reg.register(Arc::new(MockComponent::new()));
236            reg.register(Arc::new(StubComponent::new("http", vec!["opt"])));
237        }
238
239        let catalog = RuntimeComponentMetadataCatalog::new(Arc::clone(&registry));
240
241        let schemes = &[
242            "sql",
243            "file",
244            "cron",
245            "opensearch",
246            "ws",
247            "container",
248            "timer",
249            "direct",
250            "seda",
251            "log",
252            "mock",
253            "http",
254        ];
255
256        for scheme in schemes {
257            let meta = catalog
258                .get_metadata(scheme)
259                .unwrap_or_else(|| panic!("missing metadata for scheme '{scheme}'"));
260            let mut names: Vec<&str> = meta.uri_options.iter().map(|o| o.name.as_str()).collect();
261            let original_len = names.len();
262            names.sort_unstable();
263            names.dedup();
264            assert_eq!(
265                names.len(),
266                original_len,
267                "duplicate option names found in scheme '{scheme}'"
268            );
269        }
270    }
271}