Skip to main content

camel_api/
processor.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::{Arc, Mutex};
4use std::task::{Context, Poll};
5
6use tower::Service;
7
8use crate::error::CamelError;
9use crate::exchange::Exchange;
10
11/// A Processor is a Tower Service that transforms an Exchange.
12///
13/// Any type implementing `Service<Exchange, Response = Exchange, Error = CamelError>`
14/// that is also `Clone + Send + Sync + 'static` automatically implements `Processor`.
15pub trait Processor:
16    Service<Exchange, Response = Exchange, Error = CamelError> + Clone + Send + Sync + 'static
17{
18}
19
20// Blanket implementation: anything satisfying the bounds is a Processor.
21impl<P> Processor for P where
22    P: Service<Exchange, Response = Exchange, Error = CamelError> + Clone + Send + Sync + 'static
23{
24}
25
26/// An identity processor that passes the exchange through unchanged.
27#[derive(Debug, Clone)]
28pub struct IdentityProcessor;
29
30impl Service<Exchange> for IdentityProcessor {
31    type Response = Exchange;
32    type Error = CamelError;
33    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
34
35    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
36        Poll::Ready(Ok(()))
37    }
38
39    fn call(&mut self, exchange: Exchange) -> Self::Future {
40        Box::pin(async move { Ok(exchange) })
41    }
42}
43
44/// A type-erased, cloneable processor. This is the main runtime representation
45/// of a processor pipeline — a composed chain of Tower Services erased to a
46/// single boxed type.
47pub type BoxProcessor = tower::util::BoxCloneService<Exchange, Exchange, CamelError>;
48
49/// Opaque newtype around [`BoxProcessor`] for `Debug` redaction.
50///
51/// `BoxProcessor` is a type alias for Tower's `BoxCloneService`, which doesn't
52/// have a useful `Debug` impl. This newtype lets the rest of the codebase use
53/// `#[derive(Debug)]` on data structures that hold processors while keeping
54/// the `Debug` output bounded. Use `op.0` to get the inner `BoxProcessor`
55/// for invocation or further wrapping.
56///
57/// Pre-v1.0: introduced to enable `#[derive(Debug)]` on `BuilderStep` (H2).
58/// Once the data model is more stable, the inner type may grow a structured
59/// `Debug` impl and this wrapper can be removed.
60#[derive(Clone)]
61pub struct OpaqueProcessor(pub BoxProcessor);
62
63impl std::fmt::Debug for OpaqueProcessor {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.write_str("BoxProcessor(...)")
66    }
67}
68
69/// Thread-safe wrapper for [`BoxProcessor`].
70///
71/// `BoxProcessor` (`BoxCloneService`) is `Send` but not `Sync` because the
72/// inner `Box<dyn CloneServiceInner>` lacks a `Sync` bound. This wrapper
73/// stores the processor behind `Arc<Mutex<...>>`, providing safe `Send+Sync`
74/// access. The Mutex is only held briefly during `clone()` — each caller
75/// gets an independent `BoxProcessor` copy.
76pub struct SyncBoxProcessor(Arc<Mutex<BoxProcessor>>);
77
78impl SyncBoxProcessor {
79    pub fn new(processor: BoxProcessor) -> Self {
80        SyncBoxProcessor(Arc::new(Mutex::new(processor)))
81    }
82
83    pub fn clone_inner(&self) -> BoxProcessor {
84        self.0.lock().unwrap_or_else(|e| e.into_inner()).clone()
85    }
86}
87
88impl Clone for SyncBoxProcessor {
89    fn clone(&self) -> Self {
90        SyncBoxProcessor(self.0.clone())
91    }
92}
93
94/// Extension trait for [`BoxProcessor`] providing ergonomic constructors.
95///
96/// Since `BoxProcessor` is a type alias for Tower's `BoxCloneService`, we cannot
97/// add inherent methods to it. This trait fills that gap.
98///
99/// # Example
100///
101/// ```ignore
102/// use camel_api::{BoxProcessor, BoxProcessorExt};
103///
104/// let processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
105/// ```
106pub trait BoxProcessorExt {
107    /// Create a [`BoxProcessor`] from an async closure.
108    ///
109    /// This is a convenience shorthand for `BoxProcessor::new(ProcessorFn::new(f))`.
110    fn from_fn<F, Fut>(f: F) -> BoxProcessor
111    where
112        F: Fn(Exchange) -> Fut + Send + Sync + 'static,
113        Fut: Future<Output = Result<Exchange, CamelError>> + Send + 'static;
114}
115
116impl BoxProcessorExt for BoxProcessor {
117    fn from_fn<F, Fut>(f: F) -> BoxProcessor
118    where
119        F: Fn(Exchange) -> Fut + Send + Sync + 'static,
120        Fut: Future<Output = Result<Exchange, CamelError>> + Send + 'static,
121    {
122        BoxProcessor::new(ProcessorFn::new(f))
123    }
124}
125
126/// Adapts an `Fn(Exchange) -> Future<Result<Exchange>>` closure into a Tower Service.
127/// This allows user-provided async closures (via `.process()`) to participate
128/// in the Tower pipeline.
129pub struct ProcessorFn<F> {
130    f: Arc<F>,
131}
132
133// Manual Clone impl: Arc<F> is always Clone, regardless of F.
134impl<F> Clone for ProcessorFn<F> {
135    fn clone(&self) -> Self {
136        Self {
137            f: Arc::clone(&self.f),
138        }
139    }
140}
141
142impl<F> ProcessorFn<F> {
143    pub fn new(f: F) -> Self {
144        Self { f: Arc::new(f) }
145    }
146}
147
148impl<F, Fut> Service<Exchange> for ProcessorFn<F>
149where
150    F: Fn(Exchange) -> Fut + Send + Sync + 'static,
151    Fut: Future<Output = Result<Exchange, CamelError>> + Send + 'static,
152{
153    type Response = Exchange;
154    type Error = CamelError;
155    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
156
157    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
158        Poll::Ready(Ok(()))
159    }
160
161    fn call(&mut self, exchange: Exchange) -> Self::Future {
162        let f = Arc::clone(&self.f);
163        Box::pin(async move { f(exchange).await })
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::message::Message;
171    use tower::ServiceExt;
172
173    #[tokio::test]
174    async fn test_identity_processor_passes_through() {
175        let exchange = Exchange::new(Message::new("hello"));
176        let processor = IdentityProcessor;
177
178        let result = processor.oneshot(exchange).await.unwrap();
179        assert_eq!(result.input.body.as_text(), Some("hello"));
180    }
181
182    #[tokio::test]
183    async fn test_identity_processor_preserves_headers() {
184        let mut exchange = Exchange::new(Message::default());
185        exchange
186            .input
187            .set_header("key", serde_json::Value::String("value".into()));
188
189        let processor = IdentityProcessor;
190        let result = processor.oneshot(exchange).await.unwrap();
191        assert_eq!(
192            result.input.header("key"),
193            Some(&serde_json::Value::String("value".into()))
194        );
195    }
196
197    #[tokio::test]
198    async fn test_identity_processor_preserves_properties() {
199        let mut exchange = Exchange::new(Message::default());
200        exchange.set_property("prop", serde_json::Value::Bool(true));
201
202        let processor = IdentityProcessor;
203        let result = processor.oneshot(exchange).await.unwrap();
204        assert_eq!(
205            result.property("prop"),
206            Some(&serde_json::Value::Bool(true))
207        );
208    }
209
210    #[tokio::test]
211    async fn test_processor_fn_transforms_exchange() {
212        let processor = ProcessorFn::new(|mut ex: Exchange| async move {
213            ex.input.body = crate::body::Body::Text("transformed".into());
214            Ok(ex)
215        });
216
217        let exchange = Exchange::new(Message::new("original"));
218        let result = processor.oneshot(exchange).await.unwrap();
219        assert_eq!(result.input.body.as_text(), Some("transformed"));
220    }
221
222    #[tokio::test]
223    async fn test_processor_fn_can_return_error() {
224        let processor = ProcessorFn::new(|_ex: Exchange| async move {
225            Err(CamelError::ProcessorError("intentional error".into()))
226        });
227
228        let exchange = Exchange::new(Message::default());
229        let result: Result<Exchange, CamelError> = processor.oneshot(exchange).await;
230        assert!(result.is_err());
231    }
232
233    #[tokio::test]
234    async fn test_processor_fn_is_cloneable() {
235        let processor = ProcessorFn::new(|ex: Exchange| async move { Ok(ex) });
236        let cloned = processor.clone();
237
238        let exchange = Exchange::new(Message::new("test"));
239        let result = cloned.oneshot(exchange).await.unwrap();
240        assert_eq!(result.input.body.as_text(), Some("test"));
241    }
242
243    #[tokio::test]
244    async fn test_box_processor_from_identity() {
245        let processor: BoxProcessor = tower::util::BoxCloneService::new(IdentityProcessor);
246
247        let exchange = Exchange::new(Message::new("boxed"));
248        let result = processor.oneshot(exchange).await.unwrap();
249        assert_eq!(result.input.body.as_text(), Some("boxed"));
250    }
251
252    #[tokio::test]
253    async fn test_box_processor_from_processor_fn() {
254        let processor: BoxProcessor =
255            tower::util::BoxCloneService::new(ProcessorFn::new(|mut ex: Exchange| async move {
256                ex.input.body = crate::body::Body::Text("via_box".into());
257                Ok(ex)
258            }));
259
260        let exchange = Exchange::new(Message::new("original"));
261        let result = processor.oneshot(exchange).await.unwrap();
262        assert_eq!(result.input.body.as_text(), Some("via_box"));
263    }
264
265    #[tokio::test]
266    async fn test_box_processor_ext_from_fn() {
267        let processor = BoxProcessor::from_fn(|mut ex: Exchange| async move {
268            ex.input.body = crate::body::Body::Text("via_from_fn".into());
269            Ok(ex)
270        });
271
272        let exchange = Exchange::new(Message::new("original"));
273        let result = processor.oneshot(exchange).await.unwrap();
274        assert_eq!(result.input.body.as_text(), Some("via_from_fn"));
275    }
276}