Skip to main content

camel_api/
layer.rs

1//! Tower `Layer` bridge for [`BoxProcessor`] pipelines.
2//!
3//! [`ProcessorLayer`] is an object-safe factory that wraps a [`BoxProcessor`]
4//! in a Tower layer and re-erases the result behind [`CamelError`].
5//! [`CamelErrorAdapter`] bridges a service whose error is `Into<BoxError>`
6//! back to `CamelError`, and [`LayerFactory`] is the cloneable, redacted
7//! handle that builder steps store. [`SegmentProcessor`] runs an
8//! [`OutcomeSegment`] as a `Service<Exchange>`, so segment-mode pipelines can
9//! be wrapped by the same layers.
10
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::task::{Context, Poll};
15
16use tower::{Layer, Service};
17
18use crate::error::{BoxError, CamelError};
19use crate::exchange::Exchange;
20use crate::outcome_segment::OutcomeSegment;
21use crate::pipeline_outcome::PipelineOutcome;
22use crate::processor::BoxProcessor;
23
24/// Object-safe factory that wraps a [`BoxProcessor`] in a Tower layer.
25///
26/// Implementations are provided by the blanket impl below, which requires the
27/// layered service to be `Clone + Send + Sync + 'static` with a `Send` future
28/// and an error convertible into [`BoxError`].
29///
30/// That blanket impl also forecloses hand-written `ProcessorLayer` impls for
31/// concrete tower layers in this crate — such an impl overlaps the blanket one
32/// (`E0119`), so a manual impl needs a newtype wrapper around the tower layer.
33///
34/// # Compile-time rejection
35///
36/// A layer whose service does not satisfy those bounds fails to compile at the
37/// `wrap` call site; there is no runtime fallback. A direct
38/// `tower::limit::RateLimit` service is the canonical example: it is not
39/// `Clone`, so wrapping it fails the `Clone` bound (`E0277`, compile-probe
40/// evidence in `openspec/changes/builder-layer-middleware/probe/PROBE.md`,
41/// finding 3). Stateful layers such as `RateLimit` must be shared through
42/// `tower::buffer::Buffer` before they can be used here.
43///
44/// # Example
45///
46/// ```
47/// use std::time::Duration;
48///
49/// use camel_api::{BoxProcessor, Exchange, LayerFactory, Message, ProcessorFn};
50/// use tower::timeout::TimeoutLayer;
51///
52/// # #[tokio::main(flavor = "current_thread")]
53/// # async fn main() {
54/// let inner: BoxProcessor =
55///     BoxProcessor::new(ProcessorFn::new(|ex: Exchange| async move { Ok(ex) }));
56///
57/// let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
58/// let wrapped = factory.wrap(inner).expect("timeout layer is Clone + Send");
59///
60/// let exchange = Exchange::new(Message::new("payload"));
61/// let response = tower::ServiceExt::oneshot(wrapped, exchange)
62///     .await
63///     .expect("a fast call passes through the timeout layer");
64/// assert_eq!(response.input.body.as_text(), Some("payload"));
65/// # }
66/// ```
67pub trait ProcessorLayer: Send + Sync + 'static {
68    /// Wrap `inner` and re-erase the layered service as a [`BoxProcessor`].
69    fn wrap(&self, inner: BoxProcessor) -> Result<BoxProcessor, CamelError>;
70}
71
72/// Adapts a Tower service whose error is convertible into [`BoxError`] to the
73/// pipeline's [`CamelError`] error type.
74///
75/// Readiness and invocation both operate on `self.inner`: the adapter never
76/// clones inside `call`. Tower's readiness contract reserves capacity on the
77/// handle that called `poll_ready`, so a clone-per-call adapter deadlocks
78/// permit-style layers (compile-probe finding P4 in
79/// `openspec/changes/builder-layer-middleware/probe/PROBE.md`).
80#[derive(Clone)]
81pub struct CamelErrorAdapter<S> {
82    inner: S,
83}
84
85impl<S> CamelErrorAdapter<S> {
86    /// Wrap `inner` so its errors surface as [`CamelError`].
87    ///
88    /// # Example
89    ///
90    /// A native [`CamelError`] passes through unchanged:
91    ///
92    /// ```
93    /// use camel_api::{BoxProcessor, CamelError, CamelErrorAdapter, Exchange, Message, ProcessorFn};
94    /// use tower::{Layer, ServiceExt};
95    ///
96    /// /// A pass-through layer: wraps nothing, changes no behavior.
97    /// #[derive(Clone)]
98    /// struct Passthrough;
99    ///
100    /// impl<S> Layer<S> for Passthrough {
101    ///     type Service = S;
102    ///
103    ///     fn layer(&self, inner: S) -> S {
104    ///         inner
105    ///     }
106    /// }
107    ///
108    /// # #[tokio::main(flavor = "current_thread")]
109    /// # async fn main() {
110    /// let failing: BoxProcessor = BoxProcessor::new(ProcessorFn::new(|_ex: Exchange| async {
111    ///     Err(CamelError::ProcessorError("x".to_string()))
112    /// }));
113    ///
114    /// let adapter = CamelErrorAdapter::new(Passthrough.layer(failing));
115    /// let err = ServiceExt::oneshot(adapter, Exchange::new(Message::new("x")))
116    ///     .await
117    ///     .expect_err("the inner service always fails");
118    ///
119    /// assert!(
120    ///     matches!(err, CamelError::ProcessorError(ref msg) if msg == "x"),
121    ///     "CamelError must pass through unchanged, got: {err:?}"
122    /// );
123    /// # }
124    /// ```
125    pub fn new(inner: S) -> Self {
126        Self { inner }
127    }
128}
129
130impl<S> Service<Exchange> for CamelErrorAdapter<S>
131where
132    S: Service<Exchange, Response = Exchange> + Clone + Send + Sync + 'static,
133    S::Error: Into<BoxError>,
134    S::Future: Send,
135{
136    type Response = Exchange;
137    type Error = CamelError;
138    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
139
140    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
141        self.inner
142            .poll_ready(cx)
143            .map(|r| r.map_err(|e| CamelError::from(e.into())))
144    }
145
146    fn call(&mut self, exchange: Exchange) -> Self::Future {
147        // tower readiness contract: poll_ready reserved capacity on THIS handle;
148        // call must consume it on the same handle. Cloning here (permit-style
149        // layers) deadlocks: the original handle keeps its unconsumed permit
150        // while the clone waits for another one.
151        let fut = self.inner.call(exchange);
152        Box::pin(async move { fut.await.map_err(|e| CamelError::from(e.into())) })
153    }
154}
155
156impl<L> ProcessorLayer for L
157where
158    L: Layer<BoxProcessor> + Send + Sync + 'static,
159    L::Service: Service<Exchange, Response = Exchange> + Clone + Send + Sync + 'static,
160    <L::Service as Service<Exchange>>::Error: Into<BoxError>,
161    <L::Service as Service<Exchange>>::Future: Send,
162{
163    fn wrap(&self, inner: BoxProcessor) -> Result<BoxProcessor, CamelError> {
164        Ok(BoxProcessor::new(CamelErrorAdapter::new(self.layer(inner))))
165    }
166}
167
168/// Cloneable handle to a [`ProcessorLayer`].
169///
170/// `Debug` is redacted to `LayerFactory(...)` so pipeline snapshots stay
171/// printable regardless of the wrapped layer's `Debug` (same style as
172/// [`OpaqueProcessor`](crate::processor::OpaqueProcessor)).
173///
174/// Equality is `Arc` pointer identity, not structural: two factories built
175/// from equal-but-distinct layer values compare unequal. Builder tests rely on
176/// this to assert factory ORDER in nesting assertions while the layer itself
177/// stays private.
178#[derive(Clone)]
179pub struct LayerFactory(Arc<dyn ProcessorLayer>);
180
181impl LayerFactory {
182    /// Build a factory from any [`ProcessorLayer`] implementation.
183    ///
184    /// # Example
185    ///
186    /// A `TimeoutLayer` turns a slow producer into a timeout-classified error:
187    ///
188    /// ```
189    /// use std::time::Duration;
190    ///
191    /// use camel_api::{BoxProcessor, CamelError, Exchange, LayerFactory, Message, ProcessorFn};
192    /// use tower::timeout::TimeoutLayer;
193    ///
194    /// # #[tokio::main(flavor = "current_thread")]
195    /// # async fn main() {
196    /// let producer: BoxProcessor = BoxProcessor::new(ProcessorFn::new(|_ex: Exchange| async {
197    ///     tokio::time::sleep(Duration::from_secs(5)).await;
198    ///     Ok(Exchange::new(Message::new("late")))
199    /// }));
200    ///
201    /// let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
202    /// let wrapped = factory.wrap(producer).expect("timeout layer is Clone + Send");
203    ///
204    /// // The 50ms timeout must fire well inside this 1s guard.
205    /// let result = tokio::time::timeout(
206    ///     Duration::from_secs(1),
207    ///     tower::ServiceExt::oneshot(wrapped, Exchange::new(Message::new("x"))),
208    /// )
209    /// .await
210    /// .expect("the timeout must fire before the 1s guard");
211    /// let err = result.expect_err("the 50ms timeout must fire before the 5s producer finishes");
212    /// assert!(
213    ///     err.to_string().contains("middleware timeout elapsed"),
214    ///     "expected a timeout-classified CamelError, got: {err}"
215    /// );
216    ///
217    /// // Walk the public `source()` chain: the opaque payload returns the
218    /// // original error directly from `source()`, so the concrete `Elapsed` is
219    /// // the first chain node (reading the payload directly would bypass the
220    /// // consumer path).
221    /// let mut current = std::error::Error::source(&err);
222    /// let mut reached_elapsed = false;
223    /// for _ in 0..3 {
224    ///     let Some(node) = current else { break };
225    ///     if node.downcast_ref::<tower::timeout::error::Elapsed>().is_some() {
226    ///         reached_elapsed = true;
227    ///         break;
228    ///     }
229    ///     current = std::error::Error::source(node);
230    /// }
231    /// assert!(
232    ///     reached_elapsed,
233    ///     "source chain must reach tower Elapsed: {err:?}"
234    /// );
235    /// # }
236    /// ```
237    pub fn new(layer: impl ProcessorLayer + 'static) -> Self {
238        Self(Arc::new(layer))
239    }
240
241    /// Wrap `inner` with the layer held by this factory.
242    pub fn wrap(&self, inner: BoxProcessor) -> Result<BoxProcessor, CamelError> {
243        self.0.wrap(inner)
244    }
245}
246
247impl std::fmt::Debug for LayerFactory {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        f.write_str("LayerFactory(...)")
250    }
251}
252
253impl PartialEq for LayerFactory {
254    fn eq(&self, other: &Self) -> bool {
255        Arc::ptr_eq(&self.0, &other.0)
256    }
257}
258
259/// Runs an [`OutcomeSegment`] as a Tower `Service<Exchange>`.
260///
261/// Bridges segment-mode pipelines (which produce [`PipelineOutcome`]) into the
262/// Tower-result world so they can be wrapped by [`ProcessorLayer`]
263/// implementations. `Completed` and `Failed` map straight through;
264/// `Stopped(ex)` maps to `Ok(ex)` after UNCONDITIONALLY setting the
265/// `CamelStop` property to `true`, so `run_steps` re-derives the stop at the
266/// next `Process` step. The unconditional overwrite is deliberate: an exchange
267/// carrying `CamelStop = false` from an earlier step must not defeat the
268/// re-derivation.
269#[derive(Clone)]
270pub struct SegmentProcessor {
271    inner: OutcomeSegment,
272}
273
274impl SegmentProcessor {
275    /// Wrap `inner` as a Tower service.
276    ///
277    /// # Example
278    ///
279    /// A `Stopped` outcome leaves the wrapper as `Ok(ex)` with `CamelStop`
280    /// forced to `true`, even though the incoming exchange carried
281    /// `CamelStop = false`:
282    ///
283    /// ```
284    /// use std::future::Future;
285    /// use std::pin::Pin;
286    ///
287    /// use camel_api::{
288    ///     Exchange, Message, OutcomePipeline, OutcomeSegment, PipelineOutcome, SegmentProcessor,
289    ///     Value, is_camel_stop,
290    /// };
291    /// use tower::ServiceExt;
292    ///
293    /// /// Always reports `Stopped`, leaving the exchange untouched.
294    /// #[derive(Clone)]
295    /// struct StoppedPipeline;
296    ///
297    /// impl OutcomePipeline for StoppedPipeline {
298    ///     fn clone_box(&self) -> Box<dyn OutcomePipeline> {
299    ///         Box::new(self.clone())
300    ///     }
301    ///
302    ///     fn run<'a>(
303    ///         &'a mut self,
304    ///         exchange: Exchange,
305    ///     ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
306    ///         Box::pin(async move { PipelineOutcome::Stopped(exchange) })
307    ///     }
308    /// }
309    ///
310    /// # #[tokio::main(flavor = "current_thread")]
311    /// # async fn main() {
312    /// let mut exchange = Exchange::new(Message::new("x"));
313    /// exchange.set_property(camel_api::CAMEL_STOP, Value::Bool(false));
314    ///
315    /// let processor = SegmentProcessor::new(OutcomeSegment::new(Box::new(StoppedPipeline)));
316    /// let response = ServiceExt::oneshot(processor, exchange)
317    ///     .await
318    ///     .expect("a stopped outcome is not an error");
319    ///
320    /// assert!(
321    ///     is_camel_stop(&response),
322    ///     "the wrapper must force CamelStop=true"
323    /// );
324    /// # }
325    /// ```
326    pub fn new(inner: OutcomeSegment) -> Self {
327        Self { inner }
328    }
329}
330
331impl Service<Exchange> for SegmentProcessor {
332    type Response = Exchange;
333    type Error = CamelError;
334    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
335
336    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
337        // Segments are stateless w.r.t. capacity: `run_steps` also treats the
338        // Segment arm as always ready.
339        Poll::Ready(Ok(()))
340    }
341
342    fn call(&mut self, exchange: Exchange) -> Self::Future {
343        // Segments run through `&mut self`; clone the handle so the future owns
344        // an independent segment. The clone walks the pipeline tree via
345        // `clone_box`, so its cost is bounded by the compile-time tree — the
346        // same cost `run_steps` already pays on every Segment step.
347        let mut segment = self.inner.clone();
348        Box::pin(async move {
349            match segment.run(exchange).await {
350                PipelineOutcome::Stopped(mut ex) => {
351                    // Unconditional overwrite: an earlier `CamelStop=false`
352                    // must not defeat re-derivation by `run_steps`.
353                    ex.set_property(crate::exchange::CAMEL_STOP, serde_json::Value::Bool(true));
354                    Ok(ex)
355                }
356                // `Completed`/`Failed` delegate to the single canonical
357                // PipelineOutcome -> Result translation (ADR-0024 §3.5).
358                outcome => outcome.into_tower_result(),
359            }
360        })
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use std::time::{Duration, Instant};
368
369    use tokio::time::sleep;
370    use tower::ServiceExt;
371    use tower::timeout::TimeoutLayer;
372
373    use crate::message::Message;
374    use crate::outcome_pipeline::OutcomePipeline;
375    use crate::outcome_segment::OutcomeSegment;
376    use crate::pipeline_outcome::PipelineOutcome;
377    use crate::processor::ProcessorFn;
378
379    /// Outcome a [`StubPipeline`] reports, so `SegmentProcessor` mapping can be
380    /// exercised without a real EIP.
381    #[derive(Clone, Copy)]
382    enum StubOutcome {
383        Completed,
384        Stopped,
385        Failed,
386    }
387
388    /// Minimal [`OutcomePipeline`] stub: returns the configured outcome and
389    /// leaves the exchange exactly as received.
390    #[derive(Clone)]
391    struct StubPipeline {
392        outcome: StubOutcome,
393    }
394
395    impl StubPipeline {
396        fn new(outcome: StubOutcome) -> Self {
397            Self { outcome }
398        }
399    }
400
401    impl OutcomePipeline for StubPipeline {
402        fn clone_box(&self) -> Box<dyn OutcomePipeline> {
403            Box::new(self.clone())
404        }
405
406        fn run<'a>(
407            &'a mut self,
408            exchange: Exchange,
409        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
410            let outcome = self.outcome;
411            Box::pin(async move {
412                match outcome {
413                    StubOutcome::Completed => PipelineOutcome::Completed(exchange),
414                    StubOutcome::Stopped => PipelineOutcome::Stopped(exchange),
415                    StubOutcome::Failed => {
416                        PipelineOutcome::Failed(CamelError::ProcessorError("x".to_string()))
417                    }
418                }
419            })
420        }
421    }
422
423    fn sleeping_processor(delay: Duration) -> BoxProcessor {
424        BoxProcessor::new(ProcessorFn::new(move |ex: Exchange| async move {
425            sleep(delay).await;
426            Ok(ex)
427        }))
428    }
429
430    fn identity_processor() -> BoxProcessor {
431        BoxProcessor::new(ProcessorFn::new(|ex: Exchange| async move { Ok(ex) }))
432    }
433
434    /// Walks the public error chain looking for the Tower timeout marker.
435    ///
436    /// The [`CamelError::ProcessorErrorWithSource`] payload is opaque, so the
437    /// chain is traversed through [`std::error::Error::source`] exactly as a
438    /// consumer would: the original error is the first node `source()` returns.
439    fn source_chain_reaches_elapsed(err: &CamelError) -> bool {
440        let mut current = std::error::Error::source(err);
441        for _ in 0..3 {
442            let Some(node) = current else { return false };
443            if node
444                .downcast_ref::<tower::timeout::error::Elapsed>()
445                .is_some()
446            {
447                return true;
448            }
449            current = std::error::Error::source(node);
450        }
451        false
452    }
453
454    #[tokio::test]
455    async fn layer_factory_wraps_timeout_layer() {
456        let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
457        let wrapped = factory
458            .wrap(sleeping_processor(Duration::from_secs(5)))
459            .expect("timeout layer must satisfy the ProcessorLayer bounds");
460
461        let started = Instant::now();
462        let result = tokio::time::timeout(
463            Duration::from_secs(1),
464            wrapped.oneshot(Exchange::new(Message::new("x"))),
465        )
466        .await
467        .expect("the 50ms timeout must return well inside the 1s guard");
468
469        let err = result.expect_err("the sleeping producer must be timed out");
470        assert!(
471            source_chain_reaches_elapsed(&err),
472            "source chain must reach tower::timeout::error::Elapsed: {err:?}"
473        );
474        assert!(
475            err.to_string().contains("middleware timeout elapsed"),
476            "unexpected error classification: {err}"
477        );
478        assert!(
479            started.elapsed() < Duration::from_secs(1),
480            "timeout fired too late: {:?}",
481            started.elapsed()
482        );
483    }
484
485    #[tokio::test]
486    async fn layer_factory_is_clone_and_debug_redacted() {
487        let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
488        assert_eq!(format!("{factory:?}"), "LayerFactory(...)");
489
490        let clone = factory.clone();
491        let wrapped = clone
492            .wrap(identity_processor())
493            .expect("the clone must wrap the same layer");
494        wrapped
495            .oneshot(Exchange::new(Message::new("clone")))
496            .await
497            .expect("a fast call passes through the cloned factory");
498    }
499
500    #[test]
501    fn layer_factory_partial_eq_ptr_identity() {
502        let original = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
503        let cloned = original.clone();
504        let different = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
505
506        assert_eq!(original, cloned, "a clone shares the same Arc");
507        assert_ne!(
508            original, different,
509            "distinct layer values are distinct factories"
510        );
511    }
512
513    #[tokio::test]
514    async fn segment_processor_remarks_stopped_as_camel_stop_true() {
515        // An earlier step explicitly cleared the stop marker; a Stopped outcome
516        // must still leave the wrapper marked, unconditionally.
517        let mut exchange = Exchange::new(Message::new("stopped"));
518        exchange.set_property(crate::CAMEL_STOP, serde_json::Value::Bool(false));
519
520        let processor = SegmentProcessor::new(OutcomeSegment::new(Box::new(StubPipeline::new(
521            StubOutcome::Stopped,
522        ))));
523
524        let response = processor
525            .oneshot(exchange)
526            .await
527            .expect("a stopped segment outcome is Ok at the Tower boundary");
528
529        assert!(
530            crate::is_camel_stop(&response),
531            "SegmentProcessor must force CamelStop=true on the Stopped arm"
532        );
533    }
534
535    #[tokio::test]
536    async fn segment_processor_passes_completed_and_failed() {
537        let completed = SegmentProcessor::new(OutcomeSegment::new(Box::new(StubPipeline::new(
538            StubOutcome::Completed,
539        ))));
540        let response = completed
541            .oneshot(Exchange::new(Message::new("done")))
542            .await
543            .expect("Completed maps to Ok");
544        assert_eq!(response.input.body.as_text(), Some("done"));
545
546        let failed = SegmentProcessor::new(OutcomeSegment::new(Box::new(StubPipeline::new(
547            StubOutcome::Failed,
548        ))));
549        let err = failed
550            .oneshot(Exchange::new(Message::new("boom")))
551            .await
552            .expect_err("Failed maps to Err");
553        assert!(
554            matches!(err, CamelError::ProcessorError(ref msg) if msg == "x"),
555            "unexpected error: {err:?}"
556        );
557    }
558}