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