1use std::sync::Arc;
14
15use camel_api::{
16 BoxBoolFuture, BoxValueFuture, CamelError, ConversionDetail, ExpressionErrorClass, Value,
17};
18
19use crate::error::LanguageError;
20use crate::{Exchange, Expression, Predicate};
21
22const GENERIC_TARGETS: [&str; 2] = ["value", "body"];
34
35#[derive(Clone, Debug)]
41pub struct EvalMeta {
42 pub language: String,
44 pub route_id: String,
46 pub step_id: String,
48 pub verb: String,
50 pub target: Option<String>,
54}
55
56pub fn to_expression_failed(err: LanguageError, meta: &EvalMeta) -> CamelError {
72 let class = err.class().unwrap_or(ExpressionErrorClass::Runtime);
73 let position = err.position();
74 let conversion = match &err {
75 LanguageError::ConversionError {
76 source_type,
77 target,
78 } => {
79 let target = match meta.target.as_deref() {
80 Some(trusted) if GENERIC_TARGETS.contains(&target.as_str()) => trusted.to_string(),
81 _ => target.clone(),
82 };
83 Some(ConversionDetail {
84 source_type: source_type.clone(),
85 target,
86 })
87 }
88 _ => None,
89 };
90 CamelError::ExpressionFailed {
91 language: meta.language.clone(),
92 route_id: meta.route_id.clone(),
93 step_id: meta.step_id.clone(),
94 verb: meta.verb.clone(),
95 class,
96 position,
97 conversion,
98 cause: err.redacted_detail().map(|d| {
99 Box::new(CamelError::ProcessorErrorWithSource(
100 d,
101 Arc::new(ExpressionDetailCause),
102 ))
103 }),
104 }
105}
106
107#[derive(Debug)]
112struct ExpressionDetailCause;
113
114impl std::fmt::Display for ExpressionDetailCause {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.write_str("expression evaluation diagnostic")
117 }
118}
119
120impl std::error::Error for ExpressionDetailCause {}
121
122pub fn is_expression_detail_cause(err: &CamelError) -> bool {
132 let CamelError::ExpressionFailed {
133 cause: Some(cause), ..
134 } = err
135 else {
136 return false;
137 };
138 match &**cause {
139 CamelError::ProcessorErrorWithSource(_, src) => src
140 .as_ref()
141 .downcast_ref::<ExpressionDetailCause>()
142 .is_some(),
143 _ => false,
144 }
145}
146
147#[derive(Clone)]
150pub struct LanguageExpressionEval {
151 expr: Arc<dyn Expression>,
152 meta: EvalMeta,
153}
154
155impl LanguageExpressionEval {
156 pub fn new(expr: Arc<dyn Expression>, meta: EvalMeta) -> Self {
158 Self { expr, meta }
159 }
160
161 pub fn meta(&self) -> &EvalMeta {
163 &self.meta
164 }
165
166 pub async fn evaluate(&self, exchange: &Exchange) -> Result<Value, CamelError> {
169 self.expr
170 .evaluate(exchange)
171 .await
172 .map_err(|err| to_expression_failed(err, &self.meta))
173 }
174
175 pub fn into_value_fn(self) -> Arc<dyn Fn(&Exchange) -> BoxValueFuture + Send + Sync> {
187 let expr = self.expr;
188 let meta = self.meta;
189 Arc::new(move |exchange: &Exchange| {
190 let expr = Arc::clone(&expr);
191 let meta = meta.clone();
192 let exchange = exchange.clone();
193 Box::pin(async move {
194 expr.evaluate(&exchange)
195 .await
196 .map_err(|err| to_expression_failed(err, &meta))
197 }) as BoxValueFuture
198 })
199 }
200}
201
202#[derive(Clone)]
205pub struct LanguagePredicateEval {
206 pred: Arc<dyn Predicate>,
207 meta: EvalMeta,
208}
209
210impl LanguagePredicateEval {
211 pub fn new(pred: Arc<dyn Predicate>, meta: EvalMeta) -> Self {
213 Self { pred, meta }
214 }
215
216 pub fn meta(&self) -> &EvalMeta {
218 &self.meta
219 }
220
221 pub async fn matches(&self, exchange: &Exchange) -> Result<bool, CamelError> {
224 self.pred
225 .matches(exchange)
226 .await
227 .map_err(|err| to_expression_failed(err, &self.meta))
228 }
229
230 pub fn into_bool_fn(self) -> Arc<dyn Fn(&Exchange) -> BoxBoolFuture + Send + Sync> {
243 let pred = self.pred;
244 let meta = self.meta;
245 Arc::new(move |exchange: &Exchange| {
246 let pred = Arc::clone(&pred);
247 let meta = meta.clone();
248 let exchange = exchange.clone();
249 Box::pin(async move {
250 pred.matches(&exchange)
251 .await
252 .map_err(|err| to_expression_failed(err, &meta))
253 }) as BoxBoolFuture
254 })
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::{Exchange, Message};
262 use camel_api::{ErrorPosition, ExpressionErrorClass};
263
264 fn test_meta(target: Option<&str>) -> EvalMeta {
265 EvalMeta {
266 language: "rhai".into(),
267 route_id: "r1".into(),
268 step_id: "set_property#0".into(),
269 verb: "set_property".into(),
270 target: target.map(str::to_string),
271 }
272 }
273
274 #[test]
275 fn to_expression_failed_maps_class_and_position() {
276 let err = LanguageError::EvalFailure {
277 class: ExpressionErrorClass::Arithmetic,
278 position: Some(ErrorPosition { line: 3, column: 8 }),
279 detail: None,
280 };
281 let out = to_expression_failed(err, &test_meta(None));
282 assert!(matches!(
283 out,
284 CamelError::ExpressionFailed {
285 class: ExpressionErrorClass::Arithmetic,
286 position: Some(p),
287 ..
288 } if p.line == 3 && p.column == 8
289 ));
290 }
291
292 #[test]
293 fn to_expression_failed_defaults_eval_error_to_runtime() {
294 let out = to_expression_failed(LanguageError::EvalError("x".into()), &test_meta(None));
295 assert!(matches!(
296 out,
297 CamelError::ExpressionFailed {
298 class: ExpressionErrorClass::Runtime,
299 position: None,
300 cause: None,
301 ..
302 }
303 ));
304 }
305
306 #[test]
307 fn to_expression_failed_carries_redacted_detail_as_cause() {
308 use std::error::Error;
309
310 let err = LanguageError::EvalFailure {
311 class: ExpressionErrorClass::Arithmetic,
312 position: None,
313 detail: Some("overflow".into()),
314 };
315 let out = to_expression_failed(err, &test_meta(None));
316 let CamelError::ExpressionFailed { cause, .. } = out else {
317 panic!("expected ExpressionFailed");
318 };
319 let cause = cause.expect("redacted detail must be attached as cause");
320 assert_eq!(cause.to_string(), "Processor error: overflow");
321 assert!(
322 cause.as_ref().source().is_some(),
323 "cause must expose the marker as its source"
324 );
325 }
326
327 #[test]
328 fn to_expression_failed_keeps_cause_none_for_free_form() {
329 let eval = to_expression_failed(LanguageError::EvalError("x".into()), &test_meta(None));
330 assert!(matches!(
331 eval,
332 CamelError::ExpressionFailed { cause: None, .. }
333 ));
334
335 let parse = to_expression_failed(
336 LanguageError::ParseError {
337 expr: "e".into(),
338 reason: "r".into(),
339 },
340 &test_meta(None),
341 );
342 assert!(matches!(
343 parse,
344 CamelError::ExpressionFailed { cause: None, .. }
345 ));
346 }
347
348 #[test]
349 fn redacted_detail_covers_typed_variants() {
350 let mismatch = LanguageError::TypeMismatch {
351 expected: "bool".into(),
352 actual: "string".into(),
353 position: None,
354 };
355 assert_eq!(
356 mismatch.redacted_detail(),
357 Some("expected bool, actual string".to_string())
358 );
359
360 let conversion = LanguageError::ConversionError {
361 source_type: "null".into(),
362 target: "f64".into(),
363 };
364 assert_eq!(
365 conversion.redacted_detail(),
366 Some("cannot convert null to f64".to_string())
367 );
368
369 assert_eq!(LanguageError::EvalError("x".into()).redacted_detail(), None);
370 assert_eq!(
371 LanguageError::ParseError {
372 expr: "e".into(),
373 reason: "r".into(),
374 }
375 .redacted_detail(),
376 None
377 );
378 }
379
380 #[test]
381 fn is_expression_detail_cause_is_exact() {
382 let mapped = to_expression_failed(
383 LanguageError::EvalFailure {
384 class: ExpressionErrorClass::Arithmetic,
385 position: None,
386 detail: Some("overflow".into()),
387 },
388 &test_meta(None),
389 );
390 assert!(is_expression_detail_cause(&mapped));
391
392 let hand_built = CamelError::ExpressionFailed {
393 language: "rhai".into(),
394 route_id: "r1".into(),
395 step_id: "set_property#0".into(),
396 verb: "set_property".into(),
397 class: ExpressionErrorClass::Runtime,
398 position: None,
399 conversion: None,
400 cause: Some(Box::new(CamelError::ProcessorError(
401 "caller diagnostic".into(),
402 ))),
403 };
404 assert!(!is_expression_detail_cause(&hand_built));
405
406 let no_cause = CamelError::ExpressionFailed {
407 language: "rhai".into(),
408 route_id: "r1".into(),
409 step_id: "set_property#0".into(),
410 verb: "set_property".into(),
411 class: ExpressionErrorClass::Runtime,
412 position: None,
413 conversion: None,
414 cause: None,
415 };
416 assert!(!is_expression_detail_cause(&no_cause));
417 }
418
419 #[test]
420 fn mapped_typed_variants_carry_cause() {
421 let mismatch = to_expression_failed(
422 LanguageError::TypeMismatch {
423 expected: "bool".into(),
424 actual: "string".into(),
425 position: None,
426 },
427 &test_meta(None),
428 );
429 assert!(is_expression_detail_cause(&mismatch));
430 let CamelError::ExpressionFailed { class, cause, .. } = mismatch else {
431 panic!("expected ExpressionFailed");
432 };
433 assert_eq!(class, ExpressionErrorClass::TypeMismatch);
434 assert_eq!(
435 cause.expect("typed mismatch cause").to_string(),
436 "Processor error: expected bool, actual string"
437 );
438
439 let conversion = to_expression_failed(
440 LanguageError::ConversionError {
441 source_type: "null".into(),
442 target: "f64".into(),
443 },
444 &test_meta(None),
445 );
446 assert!(is_expression_detail_cause(&conversion));
447 let CamelError::ExpressionFailed { class, cause, .. } = conversion else {
448 panic!("expected ExpressionFailed");
449 };
450 assert_eq!(class, ExpressionErrorClass::Conversion);
451 assert_eq!(
452 cause.expect("typed conversion cause").to_string(),
453 "Processor error: cannot convert null to f64"
454 );
455 }
456
457 #[test]
458 fn to_expression_failed_rewrites_generic_conversion_target() {
459 let err = LanguageError::ConversionError {
463 source_type: "f64".into(),
464 target: "value".into(),
465 };
466 let out = to_expression_failed(err, &test_meta(Some("property m")));
467 if let CamelError::ExpressionFailed {
468 conversion: Some(detail),
469 ..
470 } = out
471 {
472 assert_eq!(detail.source_type, "f64");
473 assert_eq!(detail.target, "property m");
474 } else {
475 panic!("expected ExpressionFailed with conversion detail, got {out:?}");
476 }
477 }
478
479 #[test]
480 fn to_expression_failed_keeps_specific_conversion_target() {
481 let err = LanguageError::ConversionError {
484 source_type: "int".into(),
485 target: "f64".into(),
486 };
487 let out = to_expression_failed(err, &test_meta(Some("property m")));
488 if let CamelError::ExpressionFailed {
489 conversion: Some(detail),
490 ..
491 } = out
492 {
493 assert_eq!(detail.target, "f64");
494 } else {
495 panic!("expected ExpressionFailed with conversion detail, got {out:?}");
496 }
497 }
498
499 struct FailingExpression;
501
502 #[async_trait::async_trait]
503 impl crate::Expression for FailingExpression {
504 async fn evaluate(&self, _exchange: &Exchange) -> Result<crate::Value, LanguageError> {
505 Err(LanguageError::EvalFailure {
506 class: ExpressionErrorClass::Arithmetic,
507 position: Some(ErrorPosition { line: 1, column: 1 }),
508 detail: None,
509 })
510 }
511 }
512
513 #[tokio::test]
514 async fn language_expression_eval_wraps_error() {
515 let expr: Arc<dyn crate::Expression> = Arc::new(FailingExpression);
516 let eval = LanguageExpressionEval::new(expr, test_meta(None));
517 let exchange = Exchange::new(Message::default());
518 let err = eval.evaluate(&exchange).await.unwrap_err();
519 assert!(matches!(
520 err,
521 CamelError::ExpressionFailed { ref verb, .. } if verb == "set_property"
522 ));
523 }
524
525 #[tokio::test]
526 async fn value_fn_maps_errors_with_meta() {
527 let expr: Arc<dyn crate::Expression> = Arc::new(FailingExpression);
528 let value_fn = LanguageExpressionEval::new(expr, test_meta(Some("body"))).into_value_fn();
529 let exchange = Exchange::new(Message::default());
530 let err = value_fn(&exchange).await.unwrap_err();
531 assert!(matches!(
532 err,
533 CamelError::ExpressionFailed {
534 ref language,
535 ref route_id,
536 ref verb,
537 ..
538 } if language == "rhai" && route_id == "r1" && verb == "set_property"
539 ));
540 }
541
542 struct FailingPredicate;
545
546 #[async_trait::async_trait]
547 impl crate::Predicate for FailingPredicate {
548 async fn matches(&self, _exchange: &Exchange) -> Result<bool, LanguageError> {
549 Err(LanguageError::ConversionError {
550 source_type: "null".into(),
551 target: "header entry".into(),
552 })
553 }
554 }
555
556 #[tokio::test]
557 async fn predicate_matches_and_bool_fn_map_errors() {
558 let pred: Arc<dyn crate::Predicate> = Arc::new(FailingPredicate);
559 let eval = LanguagePredicateEval::new(pred, test_meta(Some("header x")));
560 let exchange = Exchange::new(Message::default());
561
562 let err = eval.matches(&exchange).await.unwrap_err();
563 if let CamelError::ExpressionFailed {
564 class: ExpressionErrorClass::Conversion,
565 conversion: Some(detail),
566 ..
567 } = err.clone()
568 {
569 assert_eq!(detail.target, "header entry");
572 } else {
573 panic!("expected ExpressionFailed conversion, got {err:?}");
574 }
575
576 let bool_fn = eval.into_bool_fn();
577 let err = bool_fn(&exchange).await.unwrap_err();
578 assert!(matches!(err, CamelError::ExpressionFailed { .. }));
579 }
580}