Skip to main content

camel_processor/
script_mutator.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::task::{Context, Poll};
5
6use tower::Service;
7
8use camel_api::CamelError;
9use camel_api::exchange::Exchange;
10use camel_language_api::{EvalMeta, MutatingExpression, to_expression_failed};
11
12/// Processor that executes a mutating expression, allowing scripts to modify the Exchange.
13/// Uses `Arc<dyn MutatingExpression>` to enable `Clone` (required by `BoxProcessor`).
14///
15/// Evaluation failures are mapped to [`CamelError::ExpressionFailed`] with
16/// the trusted route metadata supplied via [`ScriptMutator::with_meta`]
17/// (ParseError routes as class `Parse` via `LanguageError::class()`).
18#[derive(Clone)]
19pub struct ScriptMutator {
20    expression: Arc<dyn MutatingExpression>,
21    meta: EvalMeta,
22}
23
24impl ScriptMutator {
25    /// Create without route metadata. The default metadata reports language
26    /// `unknown` — tests only; production code should use
27    /// [`ScriptMutator::with_meta`].
28    pub fn new(expression: Box<dyn MutatingExpression>) -> Self {
29        Self::with_meta(
30            expression,
31            EvalMeta {
32                language: "unknown".to_string(),
33                route_id: String::new(),
34                step_id: String::new(),
35                verb: String::new(),
36                target: None,
37            },
38        )
39    }
40
41    /// Create with trusted route metadata used to enrich evaluation
42    /// failures as [`CamelError::ExpressionFailed`].
43    pub fn with_meta(expression: Box<dyn MutatingExpression>, meta: EvalMeta) -> Self {
44        Self {
45            expression: expression.into(),
46            meta,
47        }
48    }
49}
50
51impl Service<Exchange> for ScriptMutator {
52    type Response = Exchange;
53    type Error = CamelError;
54    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
55
56    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
57        Poll::Ready(Ok(()))
58    }
59
60    fn call(&mut self, mut exchange: Exchange) -> Self::Future {
61        let expression = self.expression.clone();
62        let meta = self.meta.clone();
63        Box::pin(async move {
64            let result = expression.evaluate(&mut exchange).await;
65            result
66                .map(|_| exchange)
67                .map_err(|e| to_expression_failed(e, &meta))
68        })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use camel_api::{CamelError, Exchange, ExpressionErrorClass, Message, Value};
75    use camel_language_api::{EvalMeta, LanguageError};
76    use tower::ServiceExt;
77
78    use super::*;
79
80    /// A mutating expression that always fails with a structured failure.
81    struct EvalFailureMutatingExpression;
82
83    #[async_trait::async_trait]
84    impl MutatingExpression for EvalFailureMutatingExpression {
85        async fn evaluate(&self, _exchange: &mut Exchange) -> Result<Value, LanguageError> {
86            Err(LanguageError::EvalFailure {
87                class: ExpressionErrorClass::Runtime,
88                position: None,
89                detail: None,
90            })
91        }
92    }
93
94    /// A simple test mutating expression that sets a header
95    struct TestMutatingExpression;
96
97    struct ParseErrorMutatingExpression;
98    struct NotSupportedMutatingExpression;
99    struct UnknownVariableMutatingExpression;
100
101    #[async_trait::async_trait]
102    impl MutatingExpression for TestMutatingExpression {
103        async fn evaluate(&self, exchange: &mut Exchange) -> Result<Value, LanguageError> {
104            exchange
105                .input
106                .headers
107                .insert("mutated".into(), Value::Bool(true));
108            Ok(Value::Null)
109        }
110    }
111
112    #[async_trait::async_trait]
113    impl MutatingExpression for ParseErrorMutatingExpression {
114        async fn evaluate(&self, _exchange: &mut Exchange) -> Result<Value, LanguageError> {
115            Err(LanguageError::ParseError {
116                expr: "x".to_string(),
117                reason: "bad".to_string(),
118            })
119        }
120    }
121
122    #[async_trait::async_trait]
123    impl MutatingExpression for NotSupportedMutatingExpression {
124        async fn evaluate(&self, _exchange: &mut Exchange) -> Result<Value, LanguageError> {
125            Err(LanguageError::NotSupported {
126                feature: "f".to_string(),
127                language: "l".to_string(),
128            })
129        }
130    }
131
132    #[async_trait::async_trait]
133    impl MutatingExpression for UnknownVariableMutatingExpression {
134        async fn evaluate(&self, _exchange: &mut Exchange) -> Result<Value, LanguageError> {
135            Err(LanguageError::UnknownVariable("foo".to_string()))
136        }
137    }
138
139    #[tokio::test]
140    async fn test_script_mutator_modifies_exchange() {
141        let exchange = Exchange::new(Message::new("test"));
142
143        let mutator = ScriptMutator::new(Box::new(TestMutatingExpression));
144
145        let result = mutator.oneshot(exchange).await.unwrap();
146        assert_eq!(result.input.header("mutated"), Some(&Value::Bool(true)));
147    }
148
149    #[tokio::test]
150    async fn test_script_mutator_preserves_body() {
151        let exchange = Exchange::new(Message::new("original body"));
152
153        let mutator = ScriptMutator::new(Box::new(TestMutatingExpression));
154
155        let result = mutator.oneshot(exchange).await.unwrap();
156        assert_eq!(result.input.body.as_text(), Some("original body"));
157    }
158
159    #[tokio::test]
160    async fn test_script_mutator_is_clone() {
161        let mutator = ScriptMutator::new(Box::new(TestMutatingExpression));
162        let _cloned = mutator.clone();
163    }
164
165    #[tokio::test]
166    async fn script_mutator_error_is_expression_failed() {
167        let meta = EvalMeta {
168            language: "rhai".to_string(),
169            route_id: "r1".to_string(),
170            step_id: "script#0".to_string(),
171            verb: "script".to_string(),
172            target: None,
173        };
174        let mutator = ScriptMutator::with_meta(Box::new(EvalFailureMutatingExpression), meta);
175
176        let result = mutator.oneshot(Exchange::new(Message::new("test"))).await;
177
178        assert!(
179            matches!(
180                result,
181                Err(CamelError::ExpressionFailed {
182                    ref language,
183                    ref verb,
184                    class: ExpressionErrorClass::Runtime,
185                    ..
186                }) if language == "rhai" && verb == "script"
187            ),
188            "mutating-eval failure must map to ExpressionFailed with the bound meta"
189        );
190    }
191
192    #[tokio::test]
193    async fn test_script_mutator_maps_parse_error() {
194        let exchange = Exchange::new(Message::new("test"));
195        let mutator = ScriptMutator::new(Box::new(ParseErrorMutatingExpression));
196        let result = mutator.oneshot(exchange).await;
197        assert!(matches!(
198            result,
199            Err(CamelError::ExpressionFailed {
200                class: ExpressionErrorClass::Parse,
201                ..
202            })
203        ));
204    }
205
206    #[tokio::test]
207    async fn test_script_mutator_maps_not_supported_error() {
208        let exchange = Exchange::new(Message::new("test"));
209        let mutator = ScriptMutator::new(Box::new(NotSupportedMutatingExpression));
210        let result = mutator.oneshot(exchange).await;
211        // NotSupported carries no class: to_expression_failed defaults it to
212        // Runtime under the default "unknown" language.
213        assert!(matches!(
214            result,
215            Err(CamelError::ExpressionFailed {
216                ref language,
217                class: ExpressionErrorClass::Runtime,
218                ..
219            }) if language == "unknown"
220        ));
221    }
222
223    #[tokio::test]
224    async fn test_script_mutator_maps_other_language_error() {
225        let exchange = Exchange::new(Message::new("test"));
226        let mutator = ScriptMutator::new(Box::new(UnknownVariableMutatingExpression));
227        let result = mutator.oneshot(exchange).await;
228        assert!(matches!(
229            result,
230            Err(CamelError::ExpressionFailed {
231                class: ExpressionErrorClass::Runtime,
232                ..
233            })
234        ));
235    }
236}