Skip to main content

camel_language_api/
lib.rs

1//! camel-language-api — traits and types for building Camel expression languages.
2//!
3//! Language SPI for rust-camel — defines the core traits all expression/predicate languages implement.
4//!
5//! Main traits: `Language`, `Expression`, `Predicate`, `PredicateGroup`, `MutatingExpression`,
6//! `MutatingPredicate`.
7//! Main modules: `error`, `eval`, `language_limits`.
8
9pub mod error;
10pub mod eval;
11pub mod language_limits;
12
13pub use async_trait::async_trait;
14pub use camel_api::Value;
15pub use camel_api::body::Body;
16pub use camel_api::exchange::Exchange;
17pub use camel_api::message::Message;
18pub use camel_api::{ErrorPosition, ExpressionErrorClass};
19pub use error::LanguageError;
20pub use eval::{
21    EvalMeta, LanguageExpressionEval, LanguagePredicateEval, is_expression_detail_cause,
22    to_expression_failed,
23};
24pub use language_limits::{
25    JsEngineConfig, JsLimitsConfig, LanguagesConfig, MinijinjaEngineConfig, MinijinjaLimitsConfig,
26    RhaiEngineConfig, RhaiLimitsConfig,
27};
28
29/// A Language factory: produces Expression and Predicate objects.
30pub trait Language: Send + Sync {
31    fn name(&self) -> &'static str;
32    fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError>;
33    fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError>;
34
35    /// Create a group of predicates that can be evaluated in one engine call.
36    ///
37    /// The default returns `Ok(None)`, meaning group evaluation is not
38    /// supported by this language; callers fall back to per-clause sequential
39    /// evaluation.
40    fn create_predicate_group(
41        &self,
42        scripts: &[&str],
43    ) -> Result<Option<Box<dyn PredicateGroup>>, LanguageError> {
44        let _ = scripts;
45        Ok(None)
46    }
47
48    /// Create a mutating expression. Default returns NotSupported.
49    fn create_mutating_expression(
50        &self,
51        _script: &str,
52    ) -> Result<Box<dyn MutatingExpression>, LanguageError> {
53        Err(LanguageError::NotSupported {
54            feature: "mutating expressions".into(),
55            language: self.name().into(),
56        })
57    }
58
59    /// Create a mutating predicate. Default returns NotSupported.
60    fn create_mutating_predicate(
61        &self,
62        _script: &str,
63    ) -> Result<Box<dyn MutatingPredicate>, LanguageError> {
64        Err(LanguageError::NotSupported {
65            feature: "mutating predicates".into(),
66            language: self.name().into(),
67        })
68    }
69}
70
71/// Evaluates to a Value against an Exchange.
72#[async_trait]
73pub trait Expression: Send + Sync {
74    async fn evaluate(&self, exchange: &Exchange) -> Result<Value, LanguageError>;
75}
76
77/// Expression that may modify the Exchange during evaluation.
78/// Changes to headers, properties, or body are propagated back.
79#[async_trait]
80pub trait MutatingExpression: Send + Sync {
81    async fn evaluate(&self, exchange: &mut Exchange) -> Result<Value, LanguageError>;
82}
83
84/// Evaluates to bool against an Exchange.
85#[async_trait]
86pub trait Predicate: Send + Sync {
87    async fn matches(&self, exchange: &Exchange) -> Result<bool, LanguageError>;
88}
89
90/// Predicate that may modify the Exchange during evaluation.
91/// Changes to headers, properties, or body are propagated back.
92///
93/// Reserved for future use. Not yet implemented by any language.
94#[async_trait]
95pub trait MutatingPredicate: Send + Sync {
96    async fn matches(&self, exchange: &mut Exchange) -> Result<bool, LanguageError>;
97}
98
99/// Error from evaluating one clause of a predicate group.
100///
101/// `Display` renders only the clause index plus the inner error's class and
102/// [`LanguageError::redacted_detail`], never the inner error's raw `Display`:
103/// free-form payloads such as `LanguageError::EvalError` may carry arbitrary
104/// exchange or provider data.
105#[derive(Debug)]
106pub struct IndexedPredicateError {
107    /// Index of the clause whose evaluation failed.
108    pub index: usize,
109    /// Underlying language error for that clause.
110    pub error: LanguageError,
111}
112
113impl std::fmt::Display for IndexedPredicateError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "predicate group clause {} failed", self.index)?;
116        if let Some(class) = self.error.class() {
117            write!(f, " ({class})")?;
118        }
119        if let Some(detail) = self.error.redacted_detail() {
120            write!(f, ": {detail}")?;
121        }
122        Ok(())
123    }
124}
125
126/// A group of predicates evaluated together in declaration order.
127#[async_trait]
128pub trait PredicateGroup: Send + Sync {
129    /// Evaluate predicates in declaration order and return the first match.
130    ///
131    /// `Ok(Some(i))` is the index of the first matching clause, `Ok(None)`
132    /// means no clause matched, and `Err(IndexedPredicateError)` means clause
133    /// `i` failed to evaluate. Clauses after a match, and skipped clauses, are
134    /// never evaluated.
135    async fn first_match(
136        &self,
137        exchange: &Exchange,
138    ) -> Result<Option<usize>, IndexedPredicateError>;
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    /// A minimal `Language` impl used to verify the trait compiles and methods work.
146    struct MockLanguage;
147
148    impl Language for MockLanguage {
149        fn name(&self) -> &'static str {
150            "mock"
151        }
152
153        fn create_expression(&self, _script: &str) -> Result<Box<dyn Expression>, LanguageError> {
154            Ok(Box::new(MockExpression))
155        }
156
157        fn create_predicate(&self, _script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
158            Ok(Box::new(MockPredicate))
159        }
160    }
161
162    struct MockExpression;
163
164    #[async_trait]
165    impl Expression for MockExpression {
166        async fn evaluate(&self, _exchange: &Exchange) -> Result<Value, LanguageError> {
167            Ok(Value::String("mock".into()))
168        }
169    }
170
171    struct MockPredicate;
172
173    #[async_trait]
174    impl Predicate for MockPredicate {
175        async fn matches(&self, _exchange: &Exchange) -> Result<bool, LanguageError> {
176            Ok(true)
177        }
178    }
179
180    #[test]
181    fn mock_language_returns_name() {
182        let lang = MockLanguage;
183        assert_eq!(lang.name(), "mock");
184    }
185
186    #[tokio::test]
187    async fn mock_language_creates_expression_and_evaluates() {
188        let lang = MockLanguage;
189        let expr = lang.create_expression("any").unwrap();
190        let ex = Exchange::new(Message::default());
191        let result = expr.evaluate(&ex).await.unwrap();
192        assert_eq!(result.as_str().unwrap(), "mock");
193    }
194
195    #[tokio::test]
196    async fn mock_language_creates_predicate_and_matches() {
197        let lang = MockLanguage;
198        let pred = lang.create_predicate("any").unwrap();
199        let ex = Exchange::new(Message::default());
200        assert!(pred.matches(&ex).await.unwrap());
201    }
202
203    #[test]
204    fn default_mutating_expression_returns_not_supported() {
205        let lang = MockLanguage;
206        let result = lang.create_mutating_expression("any");
207        assert!(matches!(result, Err(LanguageError::NotSupported { .. })));
208        if let Err(LanguageError::NotSupported { feature, language }) = result {
209            assert_eq!(feature, "mutating expressions");
210            assert_eq!(language, "mock");
211        }
212    }
213
214    #[test]
215    fn default_mutating_predicate_returns_not_supported() {
216        let lang = MockLanguage;
217        let result = lang.create_mutating_predicate("any");
218        assert!(matches!(result, Err(LanguageError::NotSupported { .. })));
219    }
220
221    #[test]
222    fn language_trait_is_send_sync() {
223        fn assert_send_sync<T: Send + Sync>() {}
224        assert_send_sync::<MockLanguage>();
225    }
226
227    #[test]
228    fn default_create_predicate_group_returns_none() {
229        let lang = MockLanguage;
230        let result = lang.create_predicate_group(&["a", "b"]);
231        assert!(matches!(result, Ok(None)));
232    }
233
234    #[test]
235    fn language_trait_group_hook_is_object_safe() {
236        let lang: &dyn Language = &MockLanguage;
237        let result: Result<Option<Box<dyn PredicateGroup>>, LanguageError> =
238            lang.create_predicate_group(&["a", "b"]);
239        assert!(matches!(result, Ok(None)));
240    }
241
242    #[test]
243    fn indexed_predicate_error_display_names_index() {
244        let err = IndexedPredicateError {
245            index: 2,
246            error: LanguageError::EvalError("SECRET".into()),
247        };
248        let rendered = format!("{err}");
249        assert!(rendered.contains('2'), "rendered: {rendered}");
250        assert!(!rendered.contains("SECRET"), "rendered: {rendered}");
251    }
252}