use std::sync::Arc;
use camel_api::{
BoxBoolFuture, BoxValueFuture, CamelError, ConversionDetail, ExpressionErrorClass, Value,
};
use crate::error::LanguageError;
use crate::{Exchange, Expression, Predicate};
const GENERIC_TARGETS: [&str; 2] = ["value", "body"];
#[derive(Clone, Debug)]
pub struct EvalMeta {
pub language: String,
pub route_id: String,
pub step_id: String,
pub verb: String,
pub target: Option<String>,
}
pub fn to_expression_failed(err: LanguageError, meta: &EvalMeta) -> CamelError {
let class = err.class().unwrap_or(ExpressionErrorClass::Runtime);
let position = err.position();
let conversion = match &err {
LanguageError::ConversionError {
source_type,
target,
} => {
let target = match meta.target.as_deref() {
Some(trusted) if GENERIC_TARGETS.contains(&target.as_str()) => trusted.to_string(),
_ => target.clone(),
};
Some(ConversionDetail {
source_type: source_type.clone(),
target,
})
}
_ => None,
};
CamelError::ExpressionFailed {
language: meta.language.clone(),
route_id: meta.route_id.clone(),
step_id: meta.step_id.clone(),
verb: meta.verb.clone(),
class,
position,
conversion,
cause: None,
}
}
#[derive(Clone)]
pub struct LanguageExpressionEval {
expr: Arc<dyn Expression>,
meta: EvalMeta,
}
impl LanguageExpressionEval {
pub fn new(expr: Arc<dyn Expression>, meta: EvalMeta) -> Self {
Self { expr, meta }
}
pub fn meta(&self) -> &EvalMeta {
&self.meta
}
pub async fn evaluate(&self, exchange: &Exchange) -> Result<Value, CamelError> {
self.expr
.evaluate(exchange)
.await
.map_err(|err| to_expression_failed(err, &self.meta))
}
pub fn into_value_fn(self) -> Arc<dyn Fn(&Exchange) -> BoxValueFuture + Send + Sync> {
let expr = self.expr;
let meta = self.meta;
Arc::new(move |exchange: &Exchange| {
let expr = Arc::clone(&expr);
let meta = meta.clone();
let exchange = exchange.clone();
Box::pin(async move {
expr.evaluate(&exchange)
.await
.map_err(|err| to_expression_failed(err, &meta))
}) as BoxValueFuture
})
}
}
#[derive(Clone)]
pub struct LanguagePredicateEval {
pred: Arc<dyn Predicate>,
meta: EvalMeta,
}
impl LanguagePredicateEval {
pub fn new(pred: Arc<dyn Predicate>, meta: EvalMeta) -> Self {
Self { pred, meta }
}
pub fn meta(&self) -> &EvalMeta {
&self.meta
}
pub async fn matches(&self, exchange: &Exchange) -> Result<bool, CamelError> {
self.pred
.matches(exchange)
.await
.map_err(|err| to_expression_failed(err, &self.meta))
}
pub fn into_bool_fn(self) -> Arc<dyn Fn(&Exchange) -> BoxBoolFuture + Send + Sync> {
let pred = self.pred;
let meta = self.meta;
Arc::new(move |exchange: &Exchange| {
let pred = Arc::clone(&pred);
let meta = meta.clone();
let exchange = exchange.clone();
Box::pin(async move {
pred.matches(&exchange)
.await
.map_err(|err| to_expression_failed(err, &meta))
}) as BoxBoolFuture
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Exchange, Message};
use camel_api::{ErrorPosition, ExpressionErrorClass};
fn test_meta(target: Option<&str>) -> EvalMeta {
EvalMeta {
language: "rhai".into(),
route_id: "r1".into(),
step_id: "set_property#0".into(),
verb: "set_property".into(),
target: target.map(str::to_string),
}
}
#[test]
fn to_expression_failed_maps_class_and_position() {
let err = LanguageError::EvalFailure {
class: ExpressionErrorClass::Arithmetic,
position: Some(ErrorPosition { line: 3, column: 8 }),
detail: None,
};
let out = to_expression_failed(err, &test_meta(None));
assert!(matches!(
out,
CamelError::ExpressionFailed {
class: ExpressionErrorClass::Arithmetic,
position: Some(p),
..
} if p.line == 3 && p.column == 8
));
}
#[test]
fn to_expression_failed_defaults_eval_error_to_runtime() {
let out = to_expression_failed(LanguageError::EvalError("x".into()), &test_meta(None));
assert!(matches!(
out,
CamelError::ExpressionFailed {
class: ExpressionErrorClass::Runtime,
position: None,
..
}
));
}
#[test]
fn to_expression_failed_rewrites_generic_conversion_target() {
let err = LanguageError::ConversionError {
source_type: "f64".into(),
target: "value".into(),
};
let out = to_expression_failed(err, &test_meta(Some("property m")));
if let CamelError::ExpressionFailed {
conversion: Some(detail),
..
} = out
{
assert_eq!(detail.source_type, "f64");
assert_eq!(detail.target, "property m");
} else {
panic!("expected ExpressionFailed with conversion detail, got {out:?}");
}
}
#[test]
fn to_expression_failed_keeps_specific_conversion_target() {
let err = LanguageError::ConversionError {
source_type: "int".into(),
target: "f64".into(),
};
let out = to_expression_failed(err, &test_meta(Some("property m")));
if let CamelError::ExpressionFailed {
conversion: Some(detail),
..
} = out
{
assert_eq!(detail.target, "f64");
} else {
panic!("expected ExpressionFailed with conversion detail, got {out:?}");
}
}
struct FailingExpression;
#[async_trait::async_trait]
impl crate::Expression for FailingExpression {
async fn evaluate(&self, _exchange: &Exchange) -> Result<crate::Value, LanguageError> {
Err(LanguageError::EvalFailure {
class: ExpressionErrorClass::Arithmetic,
position: Some(ErrorPosition { line: 1, column: 1 }),
detail: None,
})
}
}
#[tokio::test]
async fn language_expression_eval_wraps_error() {
let expr: Arc<dyn crate::Expression> = Arc::new(FailingExpression);
let eval = LanguageExpressionEval::new(expr, test_meta(None));
let exchange = Exchange::new(Message::default());
let err = eval.evaluate(&exchange).await.unwrap_err();
assert!(matches!(
err,
CamelError::ExpressionFailed { ref verb, .. } if verb == "set_property"
));
}
#[tokio::test]
async fn value_fn_maps_errors_with_meta() {
let expr: Arc<dyn crate::Expression> = Arc::new(FailingExpression);
let value_fn = LanguageExpressionEval::new(expr, test_meta(Some("body"))).into_value_fn();
let exchange = Exchange::new(Message::default());
let err = value_fn(&exchange).await.unwrap_err();
assert!(matches!(
err,
CamelError::ExpressionFailed {
ref language,
ref route_id,
ref verb,
..
} if language == "rhai" && route_id == "r1" && verb == "set_property"
));
}
struct FailingPredicate;
#[async_trait::async_trait]
impl crate::Predicate for FailingPredicate {
async fn matches(&self, _exchange: &Exchange) -> Result<bool, LanguageError> {
Err(LanguageError::ConversionError {
source_type: "null".into(),
target: "header entry".into(),
})
}
}
#[tokio::test]
async fn predicate_matches_and_bool_fn_map_errors() {
let pred: Arc<dyn crate::Predicate> = Arc::new(FailingPredicate);
let eval = LanguagePredicateEval::new(pred, test_meta(Some("header x")));
let exchange = Exchange::new(Message::default());
let err = eval.matches(&exchange).await.unwrap_err();
if let CamelError::ExpressionFailed {
class: ExpressionErrorClass::Conversion,
conversion: Some(detail),
..
} = err.clone()
{
assert_eq!(detail.target, "header entry");
} else {
panic!("expected ExpressionFailed conversion, got {err:?}");
}
let bool_fn = eval.into_bool_fn();
let err = bool_fn(&exchange).await.unwrap_err();
assert!(matches!(err, CamelError::ExpressionFailed { .. }));
}
}