Skip to main content

camel_api/
processor.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
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.
47///
48/// The erased type is `Send + Sync`, so pipeline snapshots can be shared
49/// between threads directly. Cloning is a lock-free virtual `clone_box()`
50/// call on the inner trait object.
51pub type BoxProcessor = tower::util::BoxCloneSyncService<Exchange, Exchange, CamelError>;
52
53/// Opaque newtype around [`BoxProcessor`] for `Debug` redaction.
54///
55/// `BoxProcessor` is a type alias for Tower's `BoxCloneSyncService`, which
56/// doesn't have a useful `Debug` impl. This newtype lets the rest of the codebase use
57/// `#[derive(Debug)]` on data structures that hold processors while keeping
58/// the `Debug` output bounded. Use `op.0` to get the inner `BoxProcessor`
59/// for invocation or further wrapping.
60///
61/// Pre-v1.0: introduced to enable `#[derive(Debug)]` on `BuilderStep` (H2).
62/// Once the data model is more stable, the inner type may grow a structured
63/// `Debug` impl and this wrapper can be removed.
64#[derive(Clone)]
65pub struct OpaqueProcessor(pub BoxProcessor);
66
67impl std::fmt::Debug for OpaqueProcessor {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.write_str("BoxProcessor(...)")
70    }
71}
72
73/// Shareable wrapper for [`BoxProcessor`].
74///
75/// `BoxProcessor` (`BoxCloneSyncService`) is `Send + Sync`, so this wrapper
76/// needs no mutex: it holds the processor directly and `clone_inner()` is a
77/// lock-free virtual `clone_box()` call, giving each caller an independent
78/// `BoxProcessor` copy.
79///
80/// Vestigial wrapper: with the `Send + Sync` erased type the newtype adds no
81/// safety anymore. Collapsing it into a plain `BoxProcessor` alias is a
82/// separate follow-up, not this change.
83pub struct SyncBoxProcessor(BoxProcessor);
84
85impl SyncBoxProcessor {
86    pub fn new(processor: BoxProcessor) -> Self {
87        SyncBoxProcessor(processor)
88    }
89
90    pub fn clone_inner(&self) -> BoxProcessor {
91        self.0.clone()
92    }
93}
94
95impl Clone for SyncBoxProcessor {
96    fn clone(&self) -> Self {
97        SyncBoxProcessor(self.0.clone())
98    }
99}
100
101// Regression lock: the erased pipeline type must stay `Sync` so no wrapper
102// mutex is ever needed to share pipeline snapshots between threads.
103#[allow(dead_code)]
104const _: () = {
105    fn is_sync<T: Sync>() {}
106    fn _check() {
107        is_sync::<BoxProcessor>();
108    }
109};
110
111/// Extension trait for [`BoxProcessor`] providing ergonomic constructors.
112///
113/// Since `BoxProcessor` is a type alias for Tower's `BoxCloneSyncService`, we
114/// cannot add inherent methods to it. This trait fills that gap.
115///
116/// # Example
117///
118/// ```ignore
119/// use camel_api::{BoxProcessor, BoxProcessorExt};
120///
121/// let processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
122/// ```
123pub trait BoxProcessorExt {
124    /// Create a [`BoxProcessor`] from an async closure.
125    ///
126    /// This is a convenience shorthand for `BoxProcessor::new(ProcessorFn::new(f))`.
127    fn from_fn<F, Fut>(f: F) -> BoxProcessor
128    where
129        F: Fn(Exchange) -> Fut + Send + Sync + 'static,
130        Fut: Future<Output = Result<Exchange, CamelError>> + Send + 'static;
131}
132
133impl BoxProcessorExt for BoxProcessor {
134    fn from_fn<F, Fut>(f: F) -> BoxProcessor
135    where
136        F: Fn(Exchange) -> Fut + Send + Sync + 'static,
137        Fut: Future<Output = Result<Exchange, CamelError>> + Send + 'static,
138    {
139        BoxProcessor::new(ProcessorFn::new(f))
140    }
141}
142
143/// Adapts an `Fn(Exchange) -> Future<Result<Exchange>>` closure into a Tower Service.
144/// This allows user-provided async closures (via `.process()`) to participate
145/// in the Tower pipeline.
146pub struct ProcessorFn<F> {
147    f: Arc<F>,
148}
149
150// Manual Clone impl: Arc<F> is always Clone, regardless of F.
151impl<F> Clone for ProcessorFn<F> {
152    fn clone(&self) -> Self {
153        Self {
154            f: Arc::clone(&self.f),
155        }
156    }
157}
158
159impl<F> ProcessorFn<F> {
160    pub fn new(f: F) -> Self {
161        Self { f: Arc::new(f) }
162    }
163}
164
165impl<F, Fut> Service<Exchange> for ProcessorFn<F>
166where
167    F: Fn(Exchange) -> Fut + Send + Sync + 'static,
168    Fut: Future<Output = Result<Exchange, CamelError>> + Send + 'static,
169{
170    type Response = Exchange;
171    type Error = CamelError;
172    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
173
174    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
175        Poll::Ready(Ok(()))
176    }
177
178    fn call(&mut self, exchange: Exchange) -> Self::Future {
179        let f = Arc::clone(&self.f);
180        Box::pin(async move { f(exchange).await })
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::message::Message;
188    use tower::ServiceExt;
189
190    #[tokio::test]
191    async fn test_identity_processor_passes_through() {
192        let exchange = Exchange::new(Message::new("hello"));
193        let processor = IdentityProcessor;
194
195        let result = processor.oneshot(exchange).await.unwrap();
196        assert_eq!(result.input.body.as_text(), Some("hello"));
197    }
198
199    #[tokio::test]
200    async fn test_identity_processor_preserves_headers() {
201        let mut exchange = Exchange::new(Message::default());
202        exchange
203            .input
204            .set_header("key", serde_json::Value::String("value".into()));
205
206        let processor = IdentityProcessor;
207        let result = processor.oneshot(exchange).await.unwrap();
208        assert_eq!(
209            result.input.header("key"),
210            Some(&serde_json::Value::String("value".into()))
211        );
212    }
213
214    #[tokio::test]
215    async fn test_identity_processor_preserves_properties() {
216        let mut exchange = Exchange::new(Message::default());
217        exchange.set_property("prop", serde_json::Value::Bool(true));
218
219        let processor = IdentityProcessor;
220        let result = processor.oneshot(exchange).await.unwrap();
221        assert_eq!(
222            result.property("prop"),
223            Some(&serde_json::Value::Bool(true))
224        );
225    }
226
227    #[tokio::test]
228    async fn test_processor_fn_transforms_exchange() {
229        let processor = ProcessorFn::new(|mut ex: Exchange| async move {
230            ex.input.body = crate::body::Body::Text("transformed".into());
231            Ok(ex)
232        });
233
234        let exchange = Exchange::new(Message::new("original"));
235        let result = processor.oneshot(exchange).await.unwrap();
236        assert_eq!(result.input.body.as_text(), Some("transformed"));
237    }
238
239    #[tokio::test]
240    async fn test_processor_fn_can_return_error() {
241        let processor = ProcessorFn::new(|_ex: Exchange| async move {
242            Err(CamelError::ProcessorError("intentional error".into()))
243        });
244
245        let exchange = Exchange::new(Message::default());
246        let result: Result<Exchange, CamelError> = processor.oneshot(exchange).await;
247        assert!(result.is_err());
248    }
249
250    #[tokio::test]
251    async fn test_processor_fn_is_cloneable() {
252        let processor = ProcessorFn::new(|ex: Exchange| async move { Ok(ex) });
253        let cloned = processor.clone();
254
255        let exchange = Exchange::new(Message::new("test"));
256        let result = cloned.oneshot(exchange).await.unwrap();
257        assert_eq!(result.input.body.as_text(), Some("test"));
258    }
259
260    #[tokio::test]
261    async fn test_box_processor_from_identity() {
262        let processor: BoxProcessor = BoxProcessor::new(IdentityProcessor);
263
264        let exchange = Exchange::new(Message::new("boxed"));
265        let result = processor.oneshot(exchange).await.unwrap();
266        assert_eq!(result.input.body.as_text(), Some("boxed"));
267    }
268
269    #[tokio::test]
270    async fn test_box_processor_from_processor_fn() {
271        let processor: BoxProcessor =
272            BoxProcessor::new(ProcessorFn::new(|mut ex: Exchange| async move {
273                ex.input.body = crate::body::Body::Text("via_box".into());
274                Ok(ex)
275            }));
276
277        let exchange = Exchange::new(Message::new("original"));
278        let result = processor.oneshot(exchange).await.unwrap();
279        assert_eq!(result.input.body.as_text(), Some("via_box"));
280    }
281
282    #[tokio::test]
283    async fn test_box_processor_ext_from_fn() {
284        let processor = BoxProcessor::from_fn(|mut ex: Exchange| async move {
285            ex.input.body = crate::body::Body::Text("via_from_fn".into());
286            Ok(ex)
287        });
288
289        let exchange = Exchange::new(Message::new("original"));
290        let result = processor.oneshot(exchange).await.unwrap();
291        assert_eq!(result.input.body.as_text(), Some("via_from_fn"));
292    }
293}