use std::sync::Arc;
use camel_language_api::{Body, Exchange, Value};
use camel_language_api::{Expression, LanguageError, MutatingExpression, Predicate};
use crate::{
engine::{JsEngine, JsEvalResult, JsExchange},
error::JsLanguageError,
};
fn js_err_to_lang_err(source: &str, e: JsLanguageError) -> LanguageError {
match e {
JsLanguageError::Parse { message } => LanguageError::ParseError {
expr: source.to_string(),
reason: message,
},
other => LanguageError::EvalError(other.to_string()),
}
}
pub(crate) fn validate_to_parse_error(
engine: &Arc<dyn JsEngine>,
script: &str,
) -> Result<(), LanguageError> {
engine
.validate(script)
.map_err(|e| LanguageError::ParseError {
expr: script.to_string(),
reason: e.to_string(),
})
}
fn snapshot_exchange(exchange: &Exchange) -> Result<JsExchange, LanguageError> {
let body = match &exchange.input.body {
Body::Json(v) => v.clone(),
Body::Text(s) | Body::Xml(s) => Value::String(s.clone()),
Body::Empty | Body::Bytes(_) => Value::Null,
Body::Stream(_) => {
return Err(LanguageError::EvalError(
"Body::Stream cannot be used in JS — add 'stream_cache' or 'convert_body_to' before this step".to_string(),
));
}
};
Ok(JsExchange::from_headers_body_properties(
exchange.input.headers.clone(),
body,
exchange.properties.clone(),
))
}
fn apply_result_to_exchange(result: &JsEvalResult, exchange: &mut Exchange) {
exchange.input.headers = result.headers.clone();
exchange.properties = result.properties.clone();
let original_was_representable = matches!(
&exchange.input.body,
Body::Json(_) | Body::Text(_) | Body::Xml(_)
);
if result.body != Value::Null || original_was_representable {
exchange.input.body = value_to_body(&result.body);
}
}
fn value_to_body(v: &Value) -> Body {
match v {
Value::String(s) => Body::from(s.as_str()),
other => Body::from(other.clone()),
}
}
pub struct JsExpression {
script: String,
engine: Arc<dyn JsEngine>,
}
impl JsExpression {
pub fn new(script: String, engine: Arc<dyn JsEngine>) -> Self {
Self { script, engine }
}
}
impl Expression for JsExpression {
fn evaluate(&self, exchange: &Exchange) -> Result<Value, LanguageError> {
let js_exchange = snapshot_exchange(exchange)?;
let result = self
.engine
.eval(&self.script, js_exchange)
.map_err(|e| js_err_to_lang_err(&self.script, e))?;
Ok(result.return_value)
}
}
pub struct JsMutatingExpression {
script: String,
engine: Arc<dyn JsEngine>,
}
impl JsMutatingExpression {
pub fn new(script: String, engine: Arc<dyn JsEngine>) -> Self {
Self { script, engine }
}
}
impl MutatingExpression for JsMutatingExpression {
fn evaluate(&self, exchange: &mut Exchange) -> Result<Value, LanguageError> {
let js_exchange = snapshot_exchange(exchange)?;
let original_headers = exchange.input.headers.clone();
let original_properties = exchange.properties.clone();
let original_body = exchange.input.body.clone();
match self.engine.eval(&self.script, js_exchange) {
Ok(result) => {
apply_result_to_exchange(&result, exchange);
Ok(result.return_value)
}
Err(e) => {
exchange.input.headers = original_headers;
exchange.properties = original_properties;
exchange.input.body = original_body;
Err(js_err_to_lang_err(&self.script, e))
}
}
}
}
pub struct JsPredicate {
script: String,
engine: Arc<dyn JsEngine>,
}
impl JsPredicate {
pub fn new(script: String, engine: Arc<dyn JsEngine>) -> Self {
Self { script, engine }
}
}
impl Predicate for JsPredicate {
fn matches(&self, exchange: &Exchange) -> Result<bool, LanguageError> {
let js_exchange = snapshot_exchange(exchange)?;
let result = self
.engine
.eval(&self.script, js_exchange)
.map_err(|e| js_err_to_lang_err(&self.script, e))?;
Ok(is_truthy(&result.return_value))
}
}
fn is_truthy(v: &Value) -> bool {
match v {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
Value::String(s) => !s.is_empty(),
Value::Array(_) | Value::Object(_) => true,
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use camel_language_api::Message;
use serde_json::json;
use super::*;
use crate::engines::boa::BoaEngine;
fn make_exchange() -> Exchange {
let mut msg = Message::default();
msg.headers.insert("foo".to_string(), json!("bar"));
msg.body = Body::Text("hello".to_string());
let mut ex = Exchange::new(msg);
ex.properties.insert("key".to_string(), json!("val"));
ex
}
fn engine() -> Arc<dyn JsEngine> {
Arc::new(BoaEngine::new())
}
#[test]
fn test_expression_arithmetic() {
let expr = JsExpression::new("1 + 2".to_string(), engine());
let ex = make_exchange();
let result = expr.evaluate(&ex).unwrap();
assert_eq!(result.as_i64().unwrap(), 3);
}
#[test]
fn test_expression_reads_header() {
let expr = JsExpression::new("camel.headers.get('foo')".to_string(), engine());
let ex = make_exchange();
let result = expr.evaluate(&ex).unwrap();
assert_eq!(result.as_str().unwrap(), "bar");
}
#[test]
fn test_expression_reads_body() {
let expr = JsExpression::new("camel.body".to_string(), engine());
let ex = make_exchange();
let result = expr.evaluate(&ex).unwrap();
assert_eq!(result.as_str().unwrap(), "hello");
}
#[test]
fn test_expression_does_not_mutate() {
let expr = JsExpression::new(
"camel.headers.set('x', 'y'); camel.body = 'changed'".to_string(),
engine(),
);
let ex = make_exchange();
let _ = expr.evaluate(&ex).unwrap();
assert!(!ex.input.headers.contains_key("x"));
assert_eq!(ex.input.body.as_text(), Some("hello"));
}
#[test]
fn test_mutating_expression_propagates_header() {
let expr = JsMutatingExpression::new(
"camel.headers.set('newkey', 'newval'); 'done'".to_string(),
engine(),
);
let mut ex = make_exchange();
let result = expr.evaluate(&mut ex).unwrap();
assert_eq!(result.as_str().unwrap(), "done");
assert_eq!(
ex.input.headers.get("newkey").unwrap().as_str().unwrap(),
"newval"
);
}
#[test]
fn test_mutating_expression_propagates_property() {
let expr = JsMutatingExpression::new(
"camel.set_property('newkey', 'newval'); 'done'".to_string(),
engine(),
);
let mut ex = make_exchange();
let result = expr.evaluate(&mut ex).unwrap();
assert_eq!(result.as_str().unwrap(), "done");
assert_eq!(
ex.properties.get("newkey").unwrap().as_str().unwrap(),
"newval"
);
}
#[test]
fn test_expression_reads_property_function() {
let expr = JsExpression::new("camel.property('key')".to_string(), engine());
let ex = make_exchange();
let result = expr.evaluate(&ex).unwrap();
assert_eq!(result.as_str().unwrap(), "val");
}
#[test]
fn test_mutating_expression_propagates_body() {
let expr =
JsMutatingExpression::new("camel.body = 'modified'; camel.body".to_string(), engine());
let mut ex = make_exchange();
let _ = expr.evaluate(&mut ex).unwrap();
assert_eq!(ex.input.body.as_text(), Some("modified"));
}
#[test]
fn test_mutating_expression_preserves_bytes_body() {
let expr =
JsMutatingExpression::new("camel.headers.set('x', '1'); 'done'".to_string(), engine());
let mut ex = Exchange::new(Message::default());
ex.input.body = Body::from(b"binary data".to_vec());
let _ = expr.evaluate(&mut ex).unwrap();
assert!(
matches!(ex.input.body, Body::Bytes(_)),
"Bytes body should be preserved"
);
}
#[test]
fn test_mutating_expression_preserves_empty_body() {
let expr =
JsMutatingExpression::new("camel.headers.set('x', '1'); 'done'".to_string(), engine());
let mut ex = Exchange::new(Message::default());
ex.input.body = Body::Empty;
let _ = expr.evaluate(&mut ex).unwrap();
assert!(
matches!(ex.input.body, Body::Empty),
"Empty body should be preserved when JS does not set camel.body"
);
}
#[test]
fn test_mutating_expression_rejects_stream_body() {
let expr =
JsMutatingExpression::new("camel.headers.set('x', '1'); 'done'".to_string(), engine());
let mut ex = Exchange::new(Message::default());
ex.input.body = Body::Stream(camel_api::StreamBody {
stream: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
metadata: camel_api::StreamMetadata::default(),
});
let result = expr.evaluate(&mut ex);
assert!(result.is_err(), "JS should reject Body::Stream with error");
}
#[test]
fn test_mutating_expression_can_override_bytes_body() {
let expr =
JsMutatingExpression::new("camel.body = 'replaced'; 'done'".to_string(), engine());
let mut ex = Exchange::new(Message::default());
ex.input.body = Body::from(b"binary".to_vec());
let _ = expr.evaluate(&mut ex).unwrap();
assert!(
!matches!(ex.input.body, Body::Bytes(_)),
"Body should be updated when JS explicitly sets camel.body"
);
assert_eq!(ex.input.body.as_text(), Some("replaced"));
}
#[test]
fn test_mutating_expression_atomic_rollback_on_error() {
let expr = JsMutatingExpression::new(
"camel.headers.set('x', 'y'); throw new Error('fail')".to_string(),
engine(),
);
let mut ex = make_exchange();
let result = expr.evaluate(&mut ex);
assert!(result.is_err());
assert!(!ex.input.headers.contains_key("x"));
assert_eq!(ex.input.body.as_text(), Some("hello"));
}
#[test]
fn test_predicate_true() {
let pred = JsPredicate::new("true".to_string(), engine());
let ex = make_exchange();
assert!(pred.matches(&ex).unwrap());
}
#[test]
fn test_predicate_false() {
let pred = JsPredicate::new("false".to_string(), engine());
let ex = make_exchange();
assert!(!pred.matches(&ex).unwrap());
}
#[test]
fn test_predicate_header_condition() {
let pred = JsPredicate::new("camel.headers.get('foo') === 'bar'".to_string(), engine());
let ex = make_exchange();
assert!(pred.matches(&ex).unwrap());
}
#[test]
fn test_predicate_truthy_string() {
let pred = JsPredicate::new("'non-empty'".to_string(), engine());
let ex = make_exchange();
assert!(pred.matches(&ex).unwrap());
}
#[test]
fn test_predicate_falsy_null() {
let pred = JsPredicate::new("null".to_string(), engine());
let ex = make_exchange();
assert!(!pred.matches(&ex).unwrap());
}
#[test]
fn test_is_truthy_values() {
assert!(is_truthy(&json!(true)));
assert!(is_truthy(&json!(1)));
assert!(is_truthy(&json!("x")));
assert!(is_truthy(&json!({})));
assert!(!is_truthy(&json!(false)));
assert!(!is_truthy(&json!(0)));
assert!(!is_truthy(&json!("")));
assert!(!is_truthy(&Value::Null));
}
}