camel_language_js/
language.rs1use std::sync::Arc;
4
5use camel_language_api::{
6 Expression, JsLimitsConfig, Language, LanguageError, MutatingExpression, Predicate,
7};
8
9use crate::{
10 engine::JsEngine,
11 engines::boa::BoaEngine,
12 expression::{JsExpression, JsMutatingExpression, JsPredicate, validate_to_parse_error},
13};
14
15#[derive(Clone)]
52pub struct JsLanguage {
53 engine: Arc<dyn JsEngine>,
54 limits: JsLimitsConfig,
55}
56
57impl JsLanguage {
58 pub fn new() -> Self {
60 Self::with_limits(JsLimitsConfig::default())
61 }
62
63 pub fn with_limits(limits: JsLimitsConfig) -> Self {
67 Self {
68 engine: Arc::new(BoaEngine::new(limits.clone())),
69 limits,
70 }
71 }
72
73 pub fn with_engine<E: JsEngine>(engine: E) -> Self {
78 Self::with_engine_and_limits(engine, JsLimitsConfig::default())
79 }
80
81 pub fn with_engine_and_limits<E: JsEngine>(engine: E, limits: JsLimitsConfig) -> Self {
83 Self {
84 engine: Arc::new(engine),
85 limits,
86 }
87 }
88
89 fn execution_timeout_ms(&self) -> u64 {
91 self.limits.execution_timeout_ms.unwrap_or(5_000)
92 }
93}
94
95impl Default for JsLanguage {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101impl Language for JsLanguage {
102 fn name(&self) -> &'static str {
103 "js"
104 }
105
106 fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
107 validate_to_parse_error(&self.engine, script)?;
108 Ok(Box::new(JsExpression::new(
109 script.to_string(),
110 Arc::clone(&self.engine),
111 self.execution_timeout_ms(),
112 )))
113 }
114
115 fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
116 validate_to_parse_error(&self.engine, script)?;
117 Ok(Box::new(JsPredicate::new(
118 script.to_string(),
119 Arc::clone(&self.engine),
120 self.execution_timeout_ms(),
121 )))
122 }
123
124 fn create_mutating_expression(
125 &self,
126 script: &str,
127 ) -> Result<Box<dyn MutatingExpression>, LanguageError> {
128 validate_to_parse_error(&self.engine, script)?;
129 Ok(Box::new(JsMutatingExpression::new(
130 script.to_string(),
131 Arc::clone(&self.engine),
132 self.execution_timeout_ms(),
133 )))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use camel_language_api::{Body, Exchange, Message};
141 use serde_json::json;
142
143 async fn make_exchange() -> Exchange {
144 let mut msg = Message::default();
145 msg.headers.insert("env".to_string(), json!("prod"));
146 msg.body = Body::Text("payload".to_string());
147 let mut ex = Exchange::new(msg);
148 ex.properties.insert("trace".to_string(), json!("on"));
149 ex
150 }
151
152 #[tokio::test]
153 async fn test_language_name() {
154 assert_eq!(JsLanguage::new().name(), "js");
155 }
156
157 #[tokio::test]
158 async fn test_create_expression_valid() {
159 let lang = JsLanguage::new();
160 let result = lang.create_expression("1 + 1");
161 assert!(result.is_ok());
162 }
163
164 #[tokio::test]
165 async fn test_create_expression_invalid_syntax() {
166 let lang = JsLanguage::new();
167 let result = lang.create_expression("let x = {{{");
168 assert!(result.is_err());
169 assert!(matches!(result, Err(LanguageError::ParseError { .. })));
171 }
172
173 #[tokio::test]
174 async fn test_create_predicate_valid() {
175 let lang = JsLanguage::new();
176 assert!(lang.create_predicate("true").is_ok());
177 }
178
179 #[tokio::test]
180 async fn test_create_predicate_invalid() {
181 let lang = JsLanguage::new();
182 assert!(matches!(
183 lang.create_predicate("let !!!"),
184 Err(LanguageError::ParseError { .. })
185 ));
186 }
187
188 #[tokio::test]
189 async fn test_create_mutating_expression_invalid_syntax() {
190 let lang = JsLanguage::new();
191 let result = lang.create_mutating_expression("let !!!");
192 assert!(result.is_err());
193 assert!(matches!(result, Err(LanguageError::ParseError { .. })));
194 }
195
196 #[tokio::test]
197 async fn test_create_mutating_expression_valid() {
198 let lang = JsLanguage::new();
199 assert!(
200 lang.create_mutating_expression("camel.headers.set('k','v')")
201 .is_ok()
202 );
203 }
204
205 #[tokio::test]
206 async fn test_expression_evaluate() {
207 let lang = JsLanguage::new();
208 let expr = lang.create_expression("camel.headers.get('env')").unwrap();
209 let ex = make_exchange().await;
210 let val = expr.evaluate(&ex).await.unwrap();
211 assert_eq!(val.as_str().unwrap(), "prod");
212 }
213
214 #[tokio::test]
215 async fn test_predicate_matches() {
216 let lang = JsLanguage::new();
217 let pred = lang
218 .create_predicate("camel.headers.get('env') === 'prod'")
219 .unwrap();
220 let ex = make_exchange().await;
221 assert!(pred.matches(&ex).await.unwrap());
222 }
223
224 #[tokio::test]
225 async fn test_mutating_expression_propagates() {
226 let lang = JsLanguage::new();
227 let expr = lang
228 .create_mutating_expression(
229 "camel.headers.set('added', 'yes'); camel.body = 'new'; 'done'",
230 )
231 .unwrap();
232 let mut ex = make_exchange().await;
233 let result = expr.evaluate(&mut ex).await.unwrap();
234 assert_eq!(result.as_str().unwrap(), "done");
235 assert_eq!(
236 ex.input.headers.get("added").unwrap().as_str().unwrap(),
237 "yes"
238 );
239 assert_eq!(ex.input.body.as_text().unwrap(), "new");
240 }
241
242 #[tokio::test]
243 async fn test_default_creates_js_language() {
244 let lang = JsLanguage::default();
245 assert_eq!(lang.name(), "js");
246 }
247
248 #[tokio::test]
249 async fn test_clone_works() {
250 let lang = JsLanguage::new();
251 let lang2 = lang.clone();
252 assert_eq!(lang2.name(), "js");
253 let ex = make_exchange().await;
255 let expr = lang2.create_expression("42").unwrap();
256 let val = expr.evaluate(&ex).await.unwrap();
257 assert_eq!(val.as_i64().unwrap(), 42);
258 }
259}