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: err.redacted_detail().map(|d| {
Box::new(CamelError::ProcessorErrorWithSource(
d,
Arc::new(ExpressionDetailCause),
))
}),
}
}
#[derive(Debug)]
struct ExpressionDetailCause;
impl std::fmt::Display for ExpressionDetailCause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("expression evaluation diagnostic")
}
}
impl std::error::Error for ExpressionDetailCause {}
pub fn is_expression_detail_cause(err: &CamelError) -> bool {
let CamelError::ExpressionFailed {
cause: Some(cause), ..
} = err
else {
return false;
};
match &**cause {
CamelError::ProcessorErrorWithSource(_, src) => src
.as_ref()
.downcast_ref::<ExpressionDetailCause>()
.is_some(),
_ => false,
}
}
#[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,
cause: None,
..
}
));
}
#[test]
fn to_expression_failed_carries_redacted_detail_as_cause() {
use std::error::Error;
let err = LanguageError::EvalFailure {
class: ExpressionErrorClass::Arithmetic,
position: None,
detail: Some("overflow".into()),
};
let out = to_expression_failed(err, &test_meta(None));
let CamelError::ExpressionFailed { cause, .. } = out else {
panic!("expected ExpressionFailed");
};
let cause = cause.expect("redacted detail must be attached as cause");
assert_eq!(cause.to_string(), "Processor error: overflow");
assert!(
cause.as_ref().source().is_some(),
"cause must expose the marker as its source"
);
}
#[test]
fn to_expression_failed_keeps_cause_none_for_free_form() {
let eval = to_expression_failed(LanguageError::EvalError("x".into()), &test_meta(None));
assert!(matches!(
eval,
CamelError::ExpressionFailed { cause: None, .. }
));
let parse = to_expression_failed(
LanguageError::ParseError {
expr: "e".into(),
reason: "r".into(),
},
&test_meta(None),
);
assert!(matches!(
parse,
CamelError::ExpressionFailed { cause: None, .. }
));
}
#[test]
fn redacted_detail_covers_typed_variants() {
let mismatch = LanguageError::TypeMismatch {
expected: "bool".into(),
actual: "string".into(),
position: None,
};
assert_eq!(
mismatch.redacted_detail(),
Some("expected bool, actual string".to_string())
);
let conversion = LanguageError::ConversionError {
source_type: "null".into(),
target: "f64".into(),
};
assert_eq!(
conversion.redacted_detail(),
Some("cannot convert null to f64".to_string())
);
assert_eq!(LanguageError::EvalError("x".into()).redacted_detail(), None);
assert_eq!(
LanguageError::ParseError {
expr: "e".into(),
reason: "r".into(),
}
.redacted_detail(),
None
);
}
#[test]
fn is_expression_detail_cause_is_exact() {
let mapped = to_expression_failed(
LanguageError::EvalFailure {
class: ExpressionErrorClass::Arithmetic,
position: None,
detail: Some("overflow".into()),
},
&test_meta(None),
);
assert!(is_expression_detail_cause(&mapped));
let hand_built = CamelError::ExpressionFailed {
language: "rhai".into(),
route_id: "r1".into(),
step_id: "set_property#0".into(),
verb: "set_property".into(),
class: ExpressionErrorClass::Runtime,
position: None,
conversion: None,
cause: Some(Box::new(CamelError::ProcessorError(
"caller diagnostic".into(),
))),
};
assert!(!is_expression_detail_cause(&hand_built));
let no_cause = CamelError::ExpressionFailed {
language: "rhai".into(),
route_id: "r1".into(),
step_id: "set_property#0".into(),
verb: "set_property".into(),
class: ExpressionErrorClass::Runtime,
position: None,
conversion: None,
cause: None,
};
assert!(!is_expression_detail_cause(&no_cause));
}
#[test]
fn mapped_typed_variants_carry_cause() {
let mismatch = to_expression_failed(
LanguageError::TypeMismatch {
expected: "bool".into(),
actual: "string".into(),
position: None,
},
&test_meta(None),
);
assert!(is_expression_detail_cause(&mismatch));
let CamelError::ExpressionFailed { class, cause, .. } = mismatch else {
panic!("expected ExpressionFailed");
};
assert_eq!(class, ExpressionErrorClass::TypeMismatch);
assert_eq!(
cause.expect("typed mismatch cause").to_string(),
"Processor error: expected bool, actual string"
);
let conversion = to_expression_failed(
LanguageError::ConversionError {
source_type: "null".into(),
target: "f64".into(),
},
&test_meta(None),
);
assert!(is_expression_detail_cause(&conversion));
let CamelError::ExpressionFailed { class, cause, .. } = conversion else {
panic!("expected ExpressionFailed");
};
assert_eq!(class, ExpressionErrorClass::Conversion);
assert_eq!(
cause.expect("typed conversion cause").to_string(),
"Processor error: cannot convert null to f64"
);
}
#[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 { .. }));
}
}