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}