use std::sync::Arc;
use camel_language_api::{
Expression, JsLimitsConfig, Language, LanguageError, MutatingExpression, Predicate,
};
use crate::{
engine::JsEngine,
engines::boa::BoaEngine,
expression::{JsExpression, JsMutatingExpression, JsPredicate, validate_to_parse_error},
};
#[derive(Clone)]
pub struct JsLanguage {
engine: Arc<dyn JsEngine>,
limits: JsLimitsConfig,
}
impl JsLanguage {
pub fn new() -> Self {
Self::with_limits(JsLimitsConfig::default())
}
pub fn with_limits(limits: JsLimitsConfig) -> Self {
Self {
engine: Arc::new(BoaEngine::new(limits.clone())),
limits,
}
}
pub fn with_engine<E: JsEngine>(engine: E) -> Self {
Self::with_engine_and_limits(engine, JsLimitsConfig::default())
}
pub fn with_engine_and_limits<E: JsEngine>(engine: E, limits: JsLimitsConfig) -> Self {
Self {
engine: Arc::new(engine),
limits,
}
}
fn execution_timeout_ms(&self) -> u64 {
self.limits.execution_timeout_ms.unwrap_or(5_000)
}
}
impl Default for JsLanguage {
fn default() -> Self {
Self::new()
}
}
impl Language for JsLanguage {
fn name(&self) -> &'static str {
"js"
}
fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
validate_to_parse_error(&self.engine, script)?;
Ok(Box::new(JsExpression::new(
script.to_string(),
Arc::clone(&self.engine),
self.execution_timeout_ms(),
)))
}
fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
validate_to_parse_error(&self.engine, script)?;
Ok(Box::new(JsPredicate::new(
script.to_string(),
Arc::clone(&self.engine),
self.execution_timeout_ms(),
)))
}
fn create_mutating_expression(
&self,
script: &str,
) -> Result<Box<dyn MutatingExpression>, LanguageError> {
validate_to_parse_error(&self.engine, script)?;
Ok(Box::new(JsMutatingExpression::new(
script.to_string(),
Arc::clone(&self.engine),
self.execution_timeout_ms(),
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use camel_language_api::{Body, Exchange, Message};
use serde_json::json;
async fn make_exchange() -> Exchange {
let mut msg = Message::default();
msg.headers.insert("env".to_string(), json!("prod"));
msg.body = Body::Text("payload".to_string());
let mut ex = Exchange::new(msg);
ex.properties.insert("trace".to_string(), json!("on"));
ex
}
#[tokio::test]
async fn test_language_name() {
assert_eq!(JsLanguage::new().name(), "js");
}
#[tokio::test]
async fn test_create_expression_valid() {
let lang = JsLanguage::new();
let result = lang.create_expression("1 + 1");
assert!(result.is_ok());
}
#[tokio::test]
async fn test_create_expression_invalid_syntax() {
let lang = JsLanguage::new();
let result = lang.create_expression("let x = {{{");
assert!(result.is_err());
assert!(matches!(result, Err(LanguageError::ParseError { .. })));
}
#[tokio::test]
async fn test_create_predicate_valid() {
let lang = JsLanguage::new();
assert!(lang.create_predicate("true").is_ok());
}
#[tokio::test]
async fn test_create_predicate_invalid() {
let lang = JsLanguage::new();
assert!(matches!(
lang.create_predicate("let !!!"),
Err(LanguageError::ParseError { .. })
));
}
#[tokio::test]
async fn test_create_mutating_expression_invalid_syntax() {
let lang = JsLanguage::new();
let result = lang.create_mutating_expression("let !!!");
assert!(result.is_err());
assert!(matches!(result, Err(LanguageError::ParseError { .. })));
}
#[tokio::test]
async fn test_create_mutating_expression_valid() {
let lang = JsLanguage::new();
assert!(
lang.create_mutating_expression("camel.headers.set('k','v')")
.is_ok()
);
}
#[tokio::test]
async fn test_expression_evaluate() {
let lang = JsLanguage::new();
let expr = lang.create_expression("camel.headers.get('env')").unwrap();
let ex = make_exchange().await;
let val = expr.evaluate(&ex).await.unwrap();
assert_eq!(val.as_str().unwrap(), "prod");
}
#[tokio::test]
async fn test_predicate_matches() {
let lang = JsLanguage::new();
let pred = lang
.create_predicate("camel.headers.get('env') === 'prod'")
.unwrap();
let ex = make_exchange().await;
assert!(pred.matches(&ex).await.unwrap());
}
#[tokio::test]
async fn test_mutating_expression_propagates() {
let lang = JsLanguage::new();
let expr = lang
.create_mutating_expression(
"camel.headers.set('added', 'yes'); camel.body = 'new'; 'done'",
)
.unwrap();
let mut ex = make_exchange().await;
let result = expr.evaluate(&mut ex).await.unwrap();
assert_eq!(result.as_str().unwrap(), "done");
assert_eq!(
ex.input.headers.get("added").unwrap().as_str().unwrap(),
"yes"
);
assert_eq!(ex.input.body.as_text().unwrap(), "new");
}
#[tokio::test]
async fn test_default_creates_js_language() {
let lang = JsLanguage::default();
assert_eq!(lang.name(), "js");
}
#[tokio::test]
async fn test_clone_works() {
let lang = JsLanguage::new();
let lang2 = lang.clone();
assert_eq!(lang2.name(), "js");
let ex = make_exchange().await;
let expr = lang2.create_expression("42").unwrap();
let val = expr.evaluate(&ex).await.unwrap();
assert_eq!(val.as_i64().unwrap(), 42);
}
}