Skip to main content

camel_component_api/
endpoint.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use camel_api::{BodyType, BoxProcessor, CamelError, Exchange, StepLifecycle};
6
7use crate::ProducerContext;
8use crate::consumer::Consumer;
9use crate::runtime_observability::RuntimeObservability;
10
11/// A polling consumer receives messages on demand (pull model) rather than
12/// being event-driven (push model).
13///
14/// Implement this trait on endpoints that support synchronous pull-based
15/// consumption (e.g., file, FTP, JMS). Components that are purely
16/// event-driven (e.g., HTTP server, Kafka) can leave the default
17/// [`Endpoint::polling_consumer`] returning `None`.
18#[async_trait]
19pub trait PollingConsumer: Send + Sync {
20    /// Receive the next available exchange within `timeout`, or `None` if none
21    /// arrives. `timeout = Duration::ZERO` means non-blocking (return
22    /// immediately if nothing pending).
23    async fn receive(&mut self, timeout: Duration) -> Result<Option<Exchange>, CamelError>;
24}
25
26/// An Endpoint represents a source or destination in a route URI.
27pub trait Endpoint: Send + Sync {
28    /// The URI that identifies this endpoint.
29    fn uri(&self) -> &str;
30
31    /// Create a consumer that reads from this endpoint.
32    ///
33    /// `rt` provides narrow observability access (`metrics()` + `health()`)
34    /// per ADR-0012 Phase A. Consumers store the Arc for later
35    /// `rt.metrics().increment_errors(...)` / `rt.health().force_unhealthy_for_route(...)`
36    /// calls (Phase B).
37    fn create_consumer(
38        &self,
39        rt: Arc<dyn RuntimeObservability>,
40    ) -> Result<Box<dyn Consumer>, CamelError>;
41
42    /// Create a producer that writes to this endpoint.
43    ///
44    /// `rt` provides narrow observability access (`metrics()` + `health()`)
45    /// per ADR-0012 Phase A. Producers store the Arc for later
46    /// `rt.health().force_unhealthy_for_route(...)` calls on creation failure
47    /// (Phase B category (g) sites).
48    fn create_producer(
49        &self,
50        rt: Arc<dyn RuntimeObservability>,
51        ctx: &ProducerContext,
52    ) -> Result<BoxProcessor, CamelError>;
53
54    /// Optional body type contract for the producer.
55    ///
56    /// When `Some(t)`, the pipeline will coerce the body to `t` before calling
57    /// the producer. Default: `None` (accept any body variant, zero overhead).
58    fn body_contract(&self) -> Option<BodyType> {
59        None
60    }
61
62    /// Return a polling consumer for this endpoint, if supported.
63    ///
64    /// Polling consumers use a pull model — callers invoke
65    /// [`PollingConsumer::receive`] to retrieve the next message.
66    /// Endpoints that only support push-based consumption should leave
67    /// this default (returns `None`).
68    fn polling_consumer(&self) -> Option<Box<dyn PollingConsumer>> {
69        None
70    }
71
72    /// Return this endpoint's lifecycle handle, if it is stateful.
73    ///
74    /// Endpoints that own background work beyond a single `process()` call
75    /// (timers, buckets, gap-detectors, queues) should override this to
76    /// expose a `StepLifecycle` for the runtime to start and shut down in
77    /// route order. Default: `None` (stateless; the common case).
78    ///
79    /// Mirrors the contract on the `Endpoint` trait only — see
80    /// `CompiledStep::Process.lifecycle` in camel-core for the consumer
81    /// side. The returned handle is an `Arc<dyn StepLifecycle>` so it can be
82    /// cloned into the compiled pipeline snapshot and shared across the
83    /// route controller without extra ownership plumbing.
84    fn lifecycle(&self) -> Option<Arc<dyn StepLifecycle>> {
85        None
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::ComponentContext;
93    use crate::test_support::PanicRuntimeObservability;
94
95    /// A minimal mock endpoint for testing default trait methods.
96    struct MockEndpoint {
97        uri: String,
98    }
99
100    impl MockEndpoint {
101        fn new(uri: &str) -> Self {
102            Self {
103                uri: uri.to_string(),
104            }
105        }
106    }
107
108    impl Endpoint for MockEndpoint {
109        fn uri(&self) -> &str {
110            &self.uri
111        }
112
113        fn create_consumer(
114            &self,
115            _rt: Arc<dyn crate::RuntimeObservability>,
116        ) -> Result<Box<dyn Consumer>, CamelError> {
117            Err(CamelError::EndpointCreationFailed("mock".into()))
118        }
119
120        fn create_producer(
121            &self,
122            _rt: Arc<dyn crate::RuntimeObservability>,
123            _ctx: &ProducerContext,
124        ) -> Result<BoxProcessor, CamelError> {
125            Err(CamelError::ProcessorError("mock".into()))
126        }
127    }
128
129    #[test]
130    fn mock_endpoint_polling_consumer_returns_none() {
131        let ep = MockEndpoint::new("mock://test");
132        assert!(ep.polling_consumer().is_none());
133    }
134
135    #[test]
136    fn mock_endpoint_body_contract_default_is_none() {
137        let ep = MockEndpoint::new("mock://test");
138        assert!(ep.body_contract().is_none());
139    }
140
141    #[test]
142    fn mock_endpoint_uri() {
143        let ep = MockEndpoint::new("mock://test");
144        assert_eq!(ep.uri(), "mock://test");
145    }
146
147    #[test]
148    fn mock_endpoint_create_consumer_errors() {
149        let ep = MockEndpoint::new("mock://test");
150        let rt: Arc<dyn crate::RuntimeObservability> = Arc::new(PanicRuntimeObservability);
151        let result = ep.create_consumer(rt);
152        assert!(result.is_err());
153    }
154
155    #[test]
156    fn mock_endpoint_create_producer_errors() {
157        let ep = MockEndpoint::new("mock://test");
158        let ctx = ProducerContext::new();
159        let rt: Arc<dyn crate::RuntimeObservability> = Arc::new(PanicRuntimeObservability);
160        let result = ep.create_producer(rt, &ctx);
161        assert!(result.is_err());
162    }
163
164    /// Default `lifecycle()` on the `Endpoint` trait returns `None` —
165    /// stateless endpoints (the common case) need not override the hook.
166    #[test]
167    fn endpoint_lifecycle_default_none() {
168        let ep = MockEndpoint::new("mock://test");
169        assert!(
170            ep.lifecycle().is_none(),
171            "default Endpoint::lifecycle() must return None"
172        );
173    }
174
175    /// Verify ComponentContext can be constructed (via NoOpComponentContext).
176    #[test]
177    fn component_context_noop_can_be_constructed() {
178        let _ctx = crate::NoOpComponentContext;
179    }
180
181    #[test]
182    fn component_context_noop_resolve_returns_none() {
183        let ctx = crate::NoOpComponentContext;
184        assert!(ctx.resolve_component("anything").is_none());
185        assert!(ctx.resolve_language("anything").is_none());
186    }
187}