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}