Skip to main content

camel_language_js/
language.rs

1//! [`JsLanguage`] — the JavaScript [`Language`] implementation for rust-camel.
2
3use std::sync::Arc;
4
5use camel_language_api::{Expression, Language, LanguageError, MutatingExpression, Predicate};
6
7use crate::{
8    engine::JsEngine,
9    engines::boa::BoaEngine,
10    expression::{JsExpression, JsMutatingExpression, JsPredicate, validate_to_parse_error},
11};
12
13/// JavaScript language plugin backed by [Boa](https://boajs.dev).
14///
15/// Implements the [`Language`] trait to produce JS-backed expressions, predicates,
16/// and mutating expressions for use in Apache Camel route definitions.
17///
18/// # Thread Safety
19///
20/// `JsLanguage` is `Clone + Send + Sync`. Each evaluation creates a fresh Boa
21/// `Context` (via `BoaEngine`), so no shared mutable state exists across evaluations.
22///
23/// # Example
24///
25/// ```no_run
26/// use camel_language_js::JsLanguage;
27/// use camel_language_api::Language;
28///
29/// let lang = JsLanguage::new();
30/// let expr = lang.create_expression("camel.headers.get('foo')").unwrap();
31/// ```
32#[derive(Clone)]
33pub struct JsLanguage {
34    engine: Arc<dyn JsEngine>,
35}
36
37impl JsLanguage {
38    /// Create a new `JsLanguage` with the default [`BoaEngine`].
39    pub fn new() -> Self {
40        Self {
41            engine: Arc::new(BoaEngine::new()),
42        }
43    }
44
45    /// Create a `JsLanguage` with a custom [`JsEngine`] implementation.
46    ///
47    /// Useful for testing or providing an alternative JS runtime.
48    pub fn with_engine<E: JsEngine>(engine: E) -> Self {
49        Self {
50            engine: Arc::new(engine),
51        }
52    }
53}
54
55impl Default for JsLanguage {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl Language for JsLanguage {
62    fn name(&self) -> &'static str {
63        "js"
64    }
65
66    fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
67        validate_to_parse_error(&self.engine, script)?;
68        Ok(Box::new(JsExpression::new(
69            script.to_string(),
70            Arc::clone(&self.engine),
71        )))
72    }
73
74    fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
75        validate_to_parse_error(&self.engine, script)?;
76        Ok(Box::new(JsPredicate::new(
77            script.to_string(),
78            Arc::clone(&self.engine),
79        )))
80    }
81
82    fn create_mutating_expression(
83        &self,
84        script: &str,
85    ) -> Result<Box<dyn MutatingExpression>, LanguageError> {
86        validate_to_parse_error(&self.engine, script)?;
87        Ok(Box::new(JsMutatingExpression::new(
88            script.to_string(),
89            Arc::clone(&self.engine),
90        )))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use camel_language_api::{Body, Exchange, Message};
98    use serde_json::json;
99
100    fn make_exchange() -> Exchange {
101        let mut msg = Message::default();
102        msg.headers.insert("env".to_string(), json!("prod"));
103        msg.body = Body::Text("payload".to_string());
104        let mut ex = Exchange::new(msg);
105        ex.properties.insert("trace".to_string(), json!("on"));
106        ex
107    }
108
109    #[test]
110    fn test_language_name() {
111        assert_eq!(JsLanguage::new().name(), "js");
112    }
113
114    #[test]
115    fn test_create_expression_valid() {
116        let lang = JsLanguage::new();
117        let result = lang.create_expression("1 + 1");
118        assert!(result.is_ok());
119    }
120
121    #[test]
122    fn test_create_expression_invalid_syntax() {
123        let lang = JsLanguage::new();
124        let result = lang.create_expression("let x = {{{");
125        assert!(result.is_err());
126        // Should be a ParseError
127        assert!(matches!(result, Err(LanguageError::ParseError { .. })));
128    }
129
130    #[test]
131    fn test_create_predicate_valid() {
132        let lang = JsLanguage::new();
133        assert!(lang.create_predicate("true").is_ok());
134    }
135
136    #[test]
137    fn test_create_predicate_invalid() {
138        let lang = JsLanguage::new();
139        assert!(matches!(
140            lang.create_predicate("let !!!"),
141            Err(LanguageError::ParseError { .. })
142        ));
143    }
144
145    #[test]
146    fn test_create_mutating_expression_invalid_syntax() {
147        let lang = JsLanguage::new();
148        let result = lang.create_mutating_expression("let !!!");
149        assert!(result.is_err());
150        assert!(matches!(result, Err(LanguageError::ParseError { .. })));
151    }
152
153    #[test]
154    fn test_create_mutating_expression_valid() {
155        let lang = JsLanguage::new();
156        assert!(
157            lang.create_mutating_expression("camel.headers.set('k','v')")
158                .is_ok()
159        );
160    }
161
162    #[test]
163    fn test_expression_evaluate() {
164        let lang = JsLanguage::new();
165        let expr = lang.create_expression("camel.headers.get('env')").unwrap();
166        let ex = make_exchange();
167        let val = expr.evaluate(&ex).unwrap();
168        assert_eq!(val.as_str().unwrap(), "prod");
169    }
170
171    #[test]
172    fn test_predicate_matches() {
173        let lang = JsLanguage::new();
174        let pred = lang
175            .create_predicate("camel.headers.get('env') === 'prod'")
176            .unwrap();
177        let ex = make_exchange();
178        assert!(pred.matches(&ex).unwrap());
179    }
180
181    #[test]
182    fn test_mutating_expression_propagates() {
183        let lang = JsLanguage::new();
184        let expr = lang
185            .create_mutating_expression(
186                "camel.headers.set('added', 'yes'); camel.body = 'new'; 'done'",
187            )
188            .unwrap();
189        let mut ex = make_exchange();
190        let result = expr.evaluate(&mut ex).unwrap();
191        assert_eq!(result.as_str().unwrap(), "done");
192        assert_eq!(
193            ex.input.headers.get("added").unwrap().as_str().unwrap(),
194            "yes"
195        );
196        assert_eq!(ex.input.body.as_text().unwrap(), "new");
197    }
198
199    #[test]
200    fn test_default_creates_js_language() {
201        let lang = JsLanguage::default();
202        assert_eq!(lang.name(), "js");
203    }
204
205    #[test]
206    fn test_clone_works() {
207        let lang = JsLanguage::new();
208        let lang2 = lang.clone();
209        assert_eq!(lang2.name(), "js");
210        // Both clones should work independently
211        let ex = make_exchange();
212        let expr = lang2.create_expression("42").unwrap();
213        let val = expr.evaluate(&ex).unwrap();
214        assert_eq!(val.as_i64().unwrap(), 42);
215    }
216}