camel-api 0.67.0

Core traits and interfaces for rust-camel
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
//! Tower `Layer` bridge for [`BoxProcessor`] pipelines.
//!
//! [`ProcessorLayer`] is an object-safe factory that wraps a [`BoxProcessor`]
//! in a Tower layer and re-erases the result behind [`CamelError`].
//! [`CamelErrorAdapter`] bridges a service whose error is `Into<BoxError>`
//! back to `CamelError`, and [`LayerFactory`] is the cloneable, redacted
//! handle that builder steps store. [`SegmentProcessor`] runs an
//! [`OutcomeSegment`] as a `Service<Exchange>`, so segment-mode pipelines can
//! be wrapped by the same layers.

use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};

use tower::{Layer, Service};

use crate::error::{BoxError, CamelError};
use crate::exchange::Exchange;
use crate::outcome_segment::OutcomeSegment;
use crate::pipeline_outcome::PipelineOutcome;
use crate::processor::BoxProcessor;

/// Object-safe factory that wraps a [`BoxProcessor`] in a Tower layer.
///
/// Implementations are provided by the blanket impl below, which requires the
/// layered service to be `Clone + Send + Sync + 'static` with a `Send` future
/// and an error convertible into [`BoxError`].
///
/// That blanket impl also forecloses hand-written `ProcessorLayer` impls for
/// concrete tower layers in this crate — such an impl overlaps the blanket one
/// (`E0119`), so a manual impl needs a newtype wrapper around the tower layer.
///
/// # Compile-time rejection
///
/// A layer whose service does not satisfy those bounds fails to compile at the
/// `wrap` call site; there is no runtime fallback. A direct
/// `tower::limit::RateLimit` service is the canonical example: it is not
/// `Clone`, so wrapping it fails the `Clone` bound (`E0277`, compile-probe
/// evidence in `openspec/changes/builder-layer-middleware/probe/PROBE.md`,
/// finding 3). Stateful layers such as `RateLimit` must be shared through
/// `tower::buffer::Buffer` before they can be used here.
///
/// # Example
///
/// ```
/// use std::time::Duration;
///
/// use camel_api::{BoxProcessor, Exchange, LayerFactory, Message, ProcessorFn};
/// use tower::timeout::TimeoutLayer;
///
/// # #[tokio::main(flavor = "current_thread")]
/// # async fn main() {
/// let inner: BoxProcessor =
///     BoxProcessor::new(ProcessorFn::new(|ex: Exchange| async move { Ok(ex) }));
///
/// let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
/// let wrapped = factory.wrap(inner).expect("timeout layer is Clone + Send");
///
/// let exchange = Exchange::new(Message::new("payload"));
/// let response = tower::ServiceExt::oneshot(wrapped, exchange)
///     .await
///     .expect("a fast call passes through the timeout layer");
/// assert_eq!(response.input.body.as_text(), Some("payload"));
/// # }
/// ```
pub trait ProcessorLayer: Send + Sync + 'static {
    /// Wrap `inner` and re-erase the layered service as a [`BoxProcessor`].
    fn wrap(&self, inner: BoxProcessor) -> Result<BoxProcessor, CamelError>;
}

/// Adapts a Tower service whose error is convertible into [`BoxError`] to the
/// pipeline's [`CamelError`] error type.
///
/// Readiness and invocation both operate on `self.inner`: the adapter never
/// clones inside `call`. Tower's readiness contract reserves capacity on the
/// handle that called `poll_ready`, so a clone-per-call adapter deadlocks
/// permit-style layers (compile-probe finding P4 in
/// `openspec/changes/builder-layer-middleware/probe/PROBE.md`).
#[derive(Clone)]
pub struct CamelErrorAdapter<S> {
    inner: S,
}

impl<S> CamelErrorAdapter<S> {
    /// Wrap `inner` so its errors surface as [`CamelError`].
    ///
    /// # Example
    ///
    /// A native [`CamelError`] passes through unchanged:
    ///
    /// ```
    /// use camel_api::{BoxProcessor, CamelError, CamelErrorAdapter, Exchange, Message, ProcessorFn};
    /// use tower::{Layer, ServiceExt};
    ///
    /// /// A pass-through layer: wraps nothing, changes no behavior.
    /// #[derive(Clone)]
    /// struct Passthrough;
    ///
    /// impl<S> Layer<S> for Passthrough {
    ///     type Service = S;
    ///
    ///     fn layer(&self, inner: S) -> S {
    ///         inner
    ///     }
    /// }
    ///
    /// # #[tokio::main(flavor = "current_thread")]
    /// # async fn main() {
    /// let failing: BoxProcessor = BoxProcessor::new(ProcessorFn::new(|_ex: Exchange| async {
    ///     Err(CamelError::ProcessorError("x".to_string()))
    /// }));
    ///
    /// let adapter = CamelErrorAdapter::new(Passthrough.layer(failing));
    /// let err = ServiceExt::oneshot(adapter, Exchange::new(Message::new("x")))
    ///     .await
    ///     .expect_err("the inner service always fails");
    ///
    /// assert!(
    ///     matches!(err, CamelError::ProcessorError(ref msg) if msg == "x"),
    ///     "CamelError must pass through unchanged, got: {err:?}"
    /// );
    /// # }
    /// ```
    pub fn new(inner: S) -> Self {
        Self { inner }
    }
}

impl<S> Service<Exchange> for CamelErrorAdapter<S>
where
    S: Service<Exchange, Response = Exchange> + Clone + Send + Sync + 'static,
    S::Error: Into<BoxError>,
    S::Future: Send,
{
    type Response = Exchange;
    type Error = CamelError;
    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner
            .poll_ready(cx)
            .map(|r| r.map_err(|e| CamelError::from(e.into())))
    }

    fn call(&mut self, exchange: Exchange) -> Self::Future {
        // tower readiness contract: poll_ready reserved capacity on THIS handle;
        // call must consume it on the same handle. Cloning here (permit-style
        // layers) deadlocks: the original handle keeps its unconsumed permit
        // while the clone waits for another one.
        let fut = self.inner.call(exchange);
        Box::pin(async move { fut.await.map_err(|e| CamelError::from(e.into())) })
    }
}

impl<L> ProcessorLayer for L
where
    L: Layer<BoxProcessor> + Send + Sync + 'static,
    L::Service: Service<Exchange, Response = Exchange> + Clone + Send + Sync + 'static,
    <L::Service as Service<Exchange>>::Error: Into<BoxError>,
    <L::Service as Service<Exchange>>::Future: Send,
{
    fn wrap(&self, inner: BoxProcessor) -> Result<BoxProcessor, CamelError> {
        Ok(BoxProcessor::new(CamelErrorAdapter::new(self.layer(inner))))
    }
}

/// Cloneable handle to a [`ProcessorLayer`].
///
/// `Debug` is redacted to `LayerFactory(...)` so pipeline snapshots stay
/// printable regardless of the wrapped layer's `Debug` (same style as
/// [`OpaqueProcessor`](crate::processor::OpaqueProcessor)).
///
/// Equality is `Arc` pointer identity, not structural: two factories built
/// from equal-but-distinct layer values compare unequal. Builder tests rely on
/// this to assert factory ORDER in nesting assertions while the layer itself
/// stays private.
#[derive(Clone)]
pub struct LayerFactory(Arc<dyn ProcessorLayer>);

impl LayerFactory {
    /// Build a factory from any [`ProcessorLayer`] implementation.
    ///
    /// # Example
    ///
    /// A `TimeoutLayer` turns a slow producer into a timeout-classified error:
    ///
    /// ```
    /// use std::time::Duration;
    ///
    /// use camel_api::{BoxProcessor, CamelError, Exchange, LayerFactory, Message, ProcessorFn};
    /// use tower::timeout::TimeoutLayer;
    ///
    /// # #[tokio::main(flavor = "current_thread")]
    /// # async fn main() {
    /// let producer: BoxProcessor = BoxProcessor::new(ProcessorFn::new(|_ex: Exchange| async {
    ///     tokio::time::sleep(Duration::from_secs(5)).await;
    ///     Ok(Exchange::new(Message::new("late")))
    /// }));
    ///
    /// let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
    /// let wrapped = factory.wrap(producer).expect("timeout layer is Clone + Send");
    ///
    /// // The 50ms timeout must fire well inside this 1s guard.
    /// let result = tokio::time::timeout(
    ///     Duration::from_secs(1),
    ///     tower::ServiceExt::oneshot(wrapped, Exchange::new(Message::new("x"))),
    /// )
    /// .await
    /// .expect("the timeout must fire before the 1s guard");
    /// let err = result.expect_err("the 50ms timeout must fire before the 5s producer finishes");
    /// assert!(
    ///     err.to_string().contains("middleware timeout elapsed"),
    ///     "expected a timeout-classified CamelError, got: {err}"
    /// );
    ///
    /// // Walk the public `source()` chain: the opaque payload returns the
    /// // original error directly from `source()`, so the concrete `Elapsed` is
    /// // the first chain node (reading the payload directly would bypass the
    /// // consumer path).
    /// let mut current = std::error::Error::source(&err);
    /// let mut reached_elapsed = false;
    /// for _ in 0..3 {
    ///     let Some(node) = current else { break };
    ///     if node.downcast_ref::<tower::timeout::error::Elapsed>().is_some() {
    ///         reached_elapsed = true;
    ///         break;
    ///     }
    ///     current = std::error::Error::source(node);
    /// }
    /// assert!(
    ///     reached_elapsed,
    ///     "source chain must reach tower Elapsed: {err:?}"
    /// );
    /// # }
    /// ```
    pub fn new(layer: impl ProcessorLayer + 'static) -> Self {
        Self(Arc::new(layer))
    }

    /// Wrap `inner` with the layer held by this factory.
    pub fn wrap(&self, inner: BoxProcessor) -> Result<BoxProcessor, CamelError> {
        self.0.wrap(inner)
    }
}

impl std::fmt::Debug for LayerFactory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("LayerFactory(...)")
    }
}

impl PartialEq for LayerFactory {
    fn eq(&self, other: &Self) -> bool {
        Arc::ptr_eq(&self.0, &other.0)
    }
}

/// Runs an [`OutcomeSegment`] as a Tower `Service<Exchange>`.
///
/// Bridges segment-mode pipelines (which produce [`PipelineOutcome`]) into the
/// Tower-result world so they can be wrapped by [`ProcessorLayer`]
/// implementations. `Completed` and `Failed` map straight through;
/// `Stopped(ex)` maps to `Ok(ex)` after UNCONDITIONALLY setting the
/// `CamelStop` property to `true`, so `run_steps` re-derives the stop at the
/// next `Process` step. The unconditional overwrite is deliberate: an exchange
/// carrying `CamelStop = false` from an earlier step must not defeat the
/// re-derivation.
#[derive(Clone)]
pub struct SegmentProcessor {
    inner: OutcomeSegment,
}

impl SegmentProcessor {
    /// Wrap `inner` as a Tower service.
    ///
    /// # Example
    ///
    /// A `Stopped` outcome leaves the wrapper as `Ok(ex)` with `CamelStop`
    /// forced to `true`, even though the incoming exchange carried
    /// `CamelStop = false`:
    ///
    /// ```
    /// use std::future::Future;
    /// use std::pin::Pin;
    ///
    /// use camel_api::{
    ///     Exchange, Message, OutcomePipeline, OutcomeSegment, PipelineOutcome, SegmentProcessor,
    ///     Value, is_camel_stop,
    /// };
    /// use tower::ServiceExt;
    ///
    /// /// Always reports `Stopped`, leaving the exchange untouched.
    /// #[derive(Clone)]
    /// struct StoppedPipeline;
    ///
    /// impl OutcomePipeline for StoppedPipeline {
    ///     fn clone_box(&self) -> Box<dyn OutcomePipeline> {
    ///         Box::new(self.clone())
    ///     }
    ///
    ///     fn run<'a>(
    ///         &'a mut self,
    ///         exchange: Exchange,
    ///     ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
    ///         Box::pin(async move { PipelineOutcome::Stopped(exchange) })
    ///     }
    /// }
    ///
    /// # #[tokio::main(flavor = "current_thread")]
    /// # async fn main() {
    /// let mut exchange = Exchange::new(Message::new("x"));
    /// exchange.set_property(camel_api::CAMEL_STOP, Value::Bool(false));
    ///
    /// let processor = SegmentProcessor::new(OutcomeSegment::new(Box::new(StoppedPipeline)));
    /// let response = ServiceExt::oneshot(processor, exchange)
    ///     .await
    ///     .expect("a stopped outcome is not an error");
    ///
    /// assert!(
    ///     is_camel_stop(&response),
    ///     "the wrapper must force CamelStop=true"
    /// );
    /// # }
    /// ```
    pub fn new(inner: OutcomeSegment) -> Self {
        Self { inner }
    }
}

impl Service<Exchange> for SegmentProcessor {
    type Response = Exchange;
    type Error = CamelError;
    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;

    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // Segments are stateless w.r.t. capacity: `run_steps` also treats the
        // Segment arm as always ready.
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, exchange: Exchange) -> Self::Future {
        // Segments run through `&mut self`; clone the handle so the future owns
        // an independent segment. The clone walks the pipeline tree via
        // `clone_box`, so its cost is bounded by the compile-time tree — the
        // same cost `run_steps` already pays on every Segment step.
        let mut segment = self.inner.clone();
        Box::pin(async move {
            match segment.run(exchange).await {
                PipelineOutcome::Stopped(mut ex) => {
                    // Unconditional overwrite: an earlier `CamelStop=false`
                    // must not defeat re-derivation by `run_steps`.
                    ex.set_property(crate::exchange::CAMEL_STOP, serde_json::Value::Bool(true));
                    Ok(ex)
                }
                // `Completed`/`Failed` delegate to the single canonical
                // PipelineOutcome -> Result translation (ADR-0024 §3.5).
                outcome => outcome.into_tower_result(),
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{Duration, Instant};

    use tokio::time::sleep;
    use tower::ServiceExt;
    use tower::timeout::TimeoutLayer;

    use crate::message::Message;
    use crate::outcome_pipeline::OutcomePipeline;
    use crate::outcome_segment::OutcomeSegment;
    use crate::pipeline_outcome::PipelineOutcome;
    use crate::processor::ProcessorFn;

    /// Outcome a [`StubPipeline`] reports, so `SegmentProcessor` mapping can be
    /// exercised without a real EIP.
    #[derive(Clone, Copy)]
    enum StubOutcome {
        Completed,
        Stopped,
        Failed,
    }

    /// Minimal [`OutcomePipeline`] stub: returns the configured outcome and
    /// leaves the exchange exactly as received.
    #[derive(Clone)]
    struct StubPipeline {
        outcome: StubOutcome,
    }

    impl StubPipeline {
        fn new(outcome: StubOutcome) -> Self {
            Self { outcome }
        }
    }

    impl OutcomePipeline for StubPipeline {
        fn clone_box(&self) -> Box<dyn OutcomePipeline> {
            Box::new(self.clone())
        }

        fn run<'a>(
            &'a mut self,
            exchange: Exchange,
        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
            let outcome = self.outcome;
            Box::pin(async move {
                match outcome {
                    StubOutcome::Completed => PipelineOutcome::Completed(exchange),
                    StubOutcome::Stopped => PipelineOutcome::Stopped(exchange),
                    StubOutcome::Failed => {
                        PipelineOutcome::Failed(CamelError::ProcessorError("x".to_string()))
                    }
                }
            })
        }
    }

    fn sleeping_processor(delay: Duration) -> BoxProcessor {
        BoxProcessor::new(ProcessorFn::new(move |ex: Exchange| async move {
            sleep(delay).await;
            Ok(ex)
        }))
    }

    fn identity_processor() -> BoxProcessor {
        BoxProcessor::new(ProcessorFn::new(|ex: Exchange| async move { Ok(ex) }))
    }

    /// Walks the public error chain looking for the Tower timeout marker.
    ///
    /// The [`CamelError::ProcessorErrorWithSource`] payload is opaque, so the
    /// chain is traversed through [`std::error::Error::source`] exactly as a
    /// consumer would: the original error is the first node `source()` returns.
    fn source_chain_reaches_elapsed(err: &CamelError) -> bool {
        let mut current = std::error::Error::source(err);
        for _ in 0..3 {
            let Some(node) = current else { return false };
            if node
                .downcast_ref::<tower::timeout::error::Elapsed>()
                .is_some()
            {
                return true;
            }
            current = std::error::Error::source(node);
        }
        false
    }

    #[tokio::test]
    async fn layer_factory_wraps_timeout_layer() {
        let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
        let wrapped = factory
            .wrap(sleeping_processor(Duration::from_secs(5)))
            .expect("timeout layer must satisfy the ProcessorLayer bounds");

        let started = Instant::now();
        let result = tokio::time::timeout(
            Duration::from_secs(1),
            wrapped.oneshot(Exchange::new(Message::new("x"))),
        )
        .await
        .expect("the 50ms timeout must return well inside the 1s guard");

        let err = result.expect_err("the sleeping producer must be timed out");
        assert!(
            source_chain_reaches_elapsed(&err),
            "source chain must reach tower::timeout::error::Elapsed: {err:?}"
        );
        assert!(
            err.to_string().contains("middleware timeout elapsed"),
            "unexpected error classification: {err}"
        );
        assert!(
            started.elapsed() < Duration::from_secs(1),
            "timeout fired too late: {:?}",
            started.elapsed()
        );
    }

    #[tokio::test]
    async fn layer_factory_is_clone_and_debug_redacted() {
        let factory = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
        assert_eq!(format!("{factory:?}"), "LayerFactory(...)");

        let clone = factory.clone();
        let wrapped = clone
            .wrap(identity_processor())
            .expect("the clone must wrap the same layer");
        wrapped
            .oneshot(Exchange::new(Message::new("clone")))
            .await
            .expect("a fast call passes through the cloned factory");
    }

    #[test]
    fn layer_factory_partial_eq_ptr_identity() {
        let original = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));
        let cloned = original.clone();
        let different = LayerFactory::new(TimeoutLayer::new(Duration::from_millis(50)));

        assert_eq!(original, cloned, "a clone shares the same Arc");
        assert_ne!(
            original, different,
            "distinct layer values are distinct factories"
        );
    }

    #[tokio::test]
    async fn segment_processor_remarks_stopped_as_camel_stop_true() {
        // An earlier step explicitly cleared the stop marker; a Stopped outcome
        // must still leave the wrapper marked, unconditionally.
        let mut exchange = Exchange::new(Message::new("stopped"));
        exchange.set_property(crate::CAMEL_STOP, serde_json::Value::Bool(false));

        let processor = SegmentProcessor::new(OutcomeSegment::new(Box::new(StubPipeline::new(
            StubOutcome::Stopped,
        ))));

        let response = processor
            .oneshot(exchange)
            .await
            .expect("a stopped segment outcome is Ok at the Tower boundary");

        assert!(
            crate::is_camel_stop(&response),
            "SegmentProcessor must force CamelStop=true on the Stopped arm"
        );
    }

    #[tokio::test]
    async fn segment_processor_passes_completed_and_failed() {
        let completed = SegmentProcessor::new(OutcomeSegment::new(Box::new(StubPipeline::new(
            StubOutcome::Completed,
        ))));
        let response = completed
            .oneshot(Exchange::new(Message::new("done")))
            .await
            .expect("Completed maps to Ok");
        assert_eq!(response.input.body.as_text(), Some("done"));

        let failed = SegmentProcessor::new(OutcomeSegment::new(Box::new(StubPipeline::new(
            StubOutcome::Failed,
        ))));
        let err = failed
            .oneshot(Exchange::new(Message::new("boom")))
            .await
            .expect_err("Failed maps to Err");
        assert!(
            matches!(err, CamelError::ProcessorError(ref msg) if msg == "x"),
            "unexpected error: {err:?}"
        );
    }
}