Skip to main content

camel_processor/
set_body.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tower::Service;
6
7use camel_api::body::Body;
8use camel_api::{CamelError, Exchange, Value, ValueSource};
9
10/// Map an evaluated expression value onto a message body.
11///
12/// `Null` maps to [`Body::Empty`], strings to [`Body::Text`], everything
13/// else to [`Body::Json`] (same rules as the camel-core `value_to_body`).
14fn value_to_body(value: Value) -> Body {
15    match value {
16        Value::Null => Body::Empty,
17        Value::String(s) => Body::Text(s),
18        other => Body::Json(other),
19    }
20}
21
22/// A processor that sets the message body from a fallible [`ValueSource`].
23///
24/// A failed evaluation fails the step WITHOUT replacing the body and WITHOUT
25/// invoking the inner service.
26#[derive(Clone)]
27pub struct SetBody<P> {
28    inner: P,
29    source: ValueSource,
30}
31
32impl<P> SetBody<P> {
33    pub fn new(inner: P, source: impl Into<ValueSource>) -> Self {
34        Self {
35            inner,
36            source: source.into(),
37        }
38    }
39}
40
41/// A Tower Layer that wraps an inner service with a [`SetBody`].
42#[derive(Clone)]
43pub struct SetBodyLayer {
44    source: ValueSource,
45}
46
47impl SetBodyLayer {
48    pub fn new(source: impl Into<ValueSource>) -> Self {
49        Self {
50            source: source.into(),
51        }
52    }
53}
54
55impl<S> tower::Layer<S> for SetBodyLayer {
56    type Service = SetBody<S>;
57
58    fn layer(&self, inner: S) -> Self::Service {
59        SetBody {
60            inner,
61            source: self.source.clone(),
62        }
63    }
64}
65
66impl<P> Service<Exchange> for SetBody<P>
67where
68    P: Service<Exchange, Response = Exchange, Error = CamelError> + Clone + Send + 'static,
69    P::Future: Send,
70{
71    type Response = Exchange;
72    type Error = CamelError;
73    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
74
75    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
76        self.inner.poll_ready(cx)
77    }
78
79    fn call(&mut self, mut exchange: Exchange) -> Self::Future {
80        let source = self.source.clone();
81        // Clone-and-replace: move the polled inner into the future so the
82        // async body owns its service; a fresh clone stays behind.
83        let clone = self.inner.clone();
84        let mut inner = std::mem::replace(&mut self.inner, clone);
85        Box::pin(async move {
86            let value = source.evaluate(&exchange).await?;
87            exchange.input.body = value_to_body(value);
88            inner.call(exchange).await
89        })
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use std::sync::Arc;
96    use std::sync::atomic::{AtomicUsize, Ordering};
97
98    use camel_api::{
99        BoxValueFuture, CamelError, Exchange, IdentityProcessor, Message, Value, ValueSource,
100    };
101    use tower::ServiceExt;
102
103    use super::*;
104
105    fn sync_source<F>(f: F) -> ValueSource
106    where
107        F: Fn(&Exchange) -> Value + Send + Sync + 'static,
108    {
109        ValueSource::Sync(Arc::new(f))
110    }
111
112    fn failing_source() -> ValueSource {
113        ValueSource::Async(Arc::new(|_: &Exchange| {
114            Box::pin(async { Err(CamelError::ProcessorError("expr boom".into())) })
115                as BoxValueFuture
116        }))
117    }
118
119    /// Inner service that records how many times it was invoked.
120    #[derive(Clone)]
121    struct CountingInner {
122        called: Arc<AtomicUsize>,
123    }
124
125    impl Service<Exchange> for CountingInner {
126        type Response = Exchange;
127        type Error = CamelError;
128        type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
129
130        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
131            Poll::Ready(Ok(()))
132        }
133
134        fn call(&mut self, exchange: Exchange) -> Self::Future {
135            self.called.fetch_add(1, Ordering::SeqCst);
136            Box::pin(async { Ok(exchange) })
137        }
138    }
139
140    #[tokio::test]
141    async fn set_body_dynamic_error_keeps_body() {
142        let called = Arc::new(AtomicUsize::new(0));
143        let svc = SetBody::new(
144            CountingInner {
145                called: Arc::clone(&called),
146            },
147            failing_source(),
148        );
149
150        // A failed body expression fails the step: the body mutation is never
151        // applied and the inner service is never invoked (so the original
152        // body is preserved — nothing downstream observes a mutation).
153        let result = svc.oneshot(Exchange::new(Message::new("original"))).await;
154
155        assert!(result.is_err(), "failed evaluation must fail the step");
156        assert_eq!(
157            called.load(Ordering::SeqCst),
158            0,
159            "inner must NOT run when evaluation fails"
160        );
161    }
162
163    #[tokio::test]
164    async fn test_set_body_static_replaces_body() {
165        let exchange = Exchange::new(Message::new("original"));
166        let svc = SetBody::new(
167            IdentityProcessor,
168            sync_source(|_ex: &Exchange| Value::String("replaced".into())),
169        );
170        let result = svc.oneshot(exchange).await.unwrap();
171        assert_eq!(result.input.body.as_text(), Some("replaced"));
172    }
173
174    #[tokio::test]
175    async fn test_set_body_dynamic_reads_exchange() {
176        let mut msg = Message::new("hello");
177        msg.set_header("suffix", camel_api::Value::String("!".into()));
178        let exchange = Exchange::new(msg);
179
180        let svc = SetBody::new(
181            IdentityProcessor,
182            sync_source(|ex: &Exchange| {
183                let base = ex.input.body.as_text().unwrap_or("");
184                let suffix = ex
185                    .input
186                    .header("suffix")
187                    .and_then(|v| v.as_str())
188                    .unwrap_or("");
189                Value::String(format!("{}{}", base, suffix))
190            }),
191        );
192
193        let result = svc.oneshot(exchange).await.unwrap();
194        assert_eq!(result.input.body.as_text(), Some("hello!"));
195    }
196
197    #[tokio::test]
198    async fn test_set_body_preserves_headers() {
199        let mut msg = Message::default();
200        msg.set_header("keep", camel_api::Value::Bool(true));
201        let exchange = Exchange::new(msg);
202
203        let svc = SetBody::new(
204            IdentityProcessor,
205            sync_source(|_ex: &Exchange| Value::String("new".into())),
206        );
207        let result = svc.oneshot(exchange).await.unwrap();
208        assert_eq!(
209            result.input.header("keep"),
210            Some(&camel_api::Value::Bool(true))
211        );
212        assert_eq!(result.input.body.as_text(), Some("new"));
213    }
214
215    #[tokio::test]
216    async fn test_set_body_layer_composes() {
217        use tower::ServiceBuilder;
218
219        let svc = ServiceBuilder::new()
220            .layer(SetBodyLayer::new(sync_source(|_ex: &Exchange| {
221                Value::String("layered".into())
222            })))
223            .service(IdentityProcessor);
224
225        let exchange = Exchange::new(Message::default());
226        let result = svc.oneshot(exchange).await.unwrap();
227        assert_eq!(result.input.body.as_text(), Some("layered"));
228    }
229}