Skip to main content

bel/
lib.rs

1//! Better Expression Language (BEL), a non-Turing complete expression language
2//! designed for simplicity, speed, and safety.
3//!
4//! BEL is ideal for lightweight expression evaluation when a scripting language
5//! or a WASM module would be too resource intensive.
6//!
7//! ```text
8//! jwt.exp < now().unix()
9//! path.starts_with("/api") || path.matches(Regex("^/(admin|secret)"))
10//! headers.all(header, header.length() < 256)
11//! ```
12//!
13//! # Types
14//!
15//! `String`, `Int`, `Float`, `Regex`, `Timestamp`, `Duration`.
16//!
17//! # Example
18//!
19//! Create and execute a simple BEL expression:
20//!
21//! ```
22//! use bel::{Context, Program, Value};
23//!
24//! let program = Program::compile("add(40, 2) == 42").unwrap();
25//! let mut context = Context::default();
26//! context.add_function("add", |a: i64, b: i64| a + b);
27//! let value = program.execute(&context).unwrap();
28//! assert_eq!(value, Value::Bool(true));
29//! ```
30
31extern crate core;
32
33use std::{convert::TryFrom, sync::Arc};
34
35use thiserror::Error;
36
37mod macros;
38
39pub mod common;
40pub mod context;
41pub mod parser;
42
43pub use common::ast::IdedExpr;
44use common::ast::SelectExpr;
45pub use context::Context;
46pub use functions::FunctionContext;
47pub use objects::{ResolveResult, Value};
48use parser::{Expression, ExpressionReferences, Parser};
49pub use parser::{ParseError, ParseErrors};
50pub mod functions;
51mod magic;
52pub mod objects;
53mod resolvers;
54
55#[cfg(feature = "time")]
56pub mod duration;
57
58#[cfg(feature = "ip")]
59pub use ser::Ip;
60#[cfg(feature = "time")]
61pub use ser::{Duration, Timestamp};
62
63mod ser;
64pub use ser::{SerializationError, to_value};
65
66#[cfg(feature = "json")]
67mod json;
68#[cfg(feature = "json")]
69pub use json::ConvertToJsonError;
70use magic::FromContext;
71
72pub mod extractors {
73    pub use crate::magic::{Arguments, Identifier, This};
74}
75
76#[derive(Error, Clone, Debug, PartialEq)]
77#[non_exhaustive]
78pub enum ExecutionError {
79    #[error("Invalid argument count: expected {expected}, got {actual}")]
80    InvalidArgumentCount { expected: usize, actual: usize },
81    #[error("Invalid argument type: {:?}", .target)]
82    UnsupportedTargetType { target: Value },
83    #[error("Method '{method}' not supported on type '{target:?}'")]
84    NotSupportedAsMethod { method: String, target: Value },
85    /// Indicates that the script attempted to use a value as a key in a map,
86    /// but the type of the value was not supported as a key.
87    #[error("Unable to use value '{0:?}' as a key")]
88    UnsupportedKeyType(Value),
89    #[error("Unexpected type: got '{got}', want '{want}'")]
90    UnexpectedType { got: String, want: String },
91    /// Indicates that the script attempted to reference a key on a type that
92    /// was missing the requested key.
93    #[error("No such key: {0}")]
94    NoSuchKey(Arc<String>),
95    /// Indicates that the script used an existing operator or function with
96    /// values of one or more types for which no overload was declared.
97    #[error("No such overload")]
98    NoSuchOverload,
99    /// Indicates that the script attempted to reference an undeclared variable
100    /// method, or function.
101    #[error("Undeclared reference to '{0}'")]
102    UndeclaredReference(Arc<String>),
103    /// Indicates that a function expected to be called as a method, or to be
104    /// called with at least one parameter.
105    #[error("Missing argument or target")]
106    MissingArgumentOrTarget,
107    /// Indicates that a comparison could not be performed.
108    #[error("{0:?} can not be compared to {1:?}")]
109    ValuesNotComparable(Value, Value),
110    /// Indicates that an operator was used on a type that does not support it.
111    #[error("Unsupported unary operator '{0}': {1:?}")]
112    UnsupportedUnaryOperator(&'static str, Value),
113    /// Indicates that an unsupported binary operator was applied on two values
114    /// where it's unsupported, for example list + map.
115    #[error("Unsupported binary operator '{0}': {1:?}, {2:?}")]
116    UnsupportedBinaryOperator(&'static str, Value, Value),
117    /// Indicates that an unsupported type was used to index a map
118    #[error("Cannot use value as map index: {0:?}")]
119    UnsupportedMapIndex(Value),
120    /// Indicates that an unsupported type was used to index a list
121    #[error("Cannot use value as list index: {0:?}")]
122    UnsupportedListIndex(Value),
123    /// Indicates that an unsupported type was used to index a list
124    #[error("Cannot use value {0:?} to index {1:?}")]
125    UnsupportedIndex(Value, Value),
126    /// Indicates that a function call occurred without an [`Expression::Ident`]
127    /// as the function identifier.
128    #[error("Unsupported function call identifier type: {0:?}")]
129    UnsupportedFunctionCallIdentifierType(Expression),
130    /// Indicates that a [`Member::Fields`] construction was attempted
131    /// which is not yet supported.
132    #[error("Unsupported fields construction: {0:?}")]
133    UnsupportedFieldsConstruction(SelectExpr),
134    /// Indicates that a function had an error during execution.
135    #[error("Error executing function '{function}': {message}")]
136    FunctionError { function: String, message: String },
137    #[error("Division by zero of {0:?}")]
138    DivisionByZero(Value),
139    #[error("Remainder by zero of {0:?}")]
140    RemainderByZero(Value),
141    #[error("Overflow from binary operator '{0}': {1:?}, {2:?}")]
142    Overflow(&'static str, Value, Value),
143}
144
145impl ExecutionError {
146    pub fn no_such_key(name: &str) -> Self {
147        ExecutionError::NoSuchKey(Arc::new(name.to_string()))
148    }
149
150    pub fn undeclared_reference(name: &str) -> Self {
151        ExecutionError::UndeclaredReference(Arc::new(name.to_string()))
152    }
153
154    pub fn invalid_argument_count(expected: usize, actual: usize) -> Self {
155        ExecutionError::InvalidArgumentCount {
156            expected,
157            actual,
158        }
159    }
160
161    pub fn function_error<E: ToString>(function: &str, error: E) -> Self {
162        ExecutionError::FunctionError {
163            function: function.to_string(),
164            message: error.to_string(),
165        }
166    }
167
168    pub fn unsupported_target_type(target: Value) -> Self {
169        ExecutionError::UnsupportedTargetType {
170            target,
171        }
172    }
173
174    pub fn not_supported_as_method(method: &str, target: Value) -> Self {
175        ExecutionError::NotSupportedAsMethod {
176            method: method.to_string(),
177            target,
178        }
179    }
180
181    pub fn unsupported_key_type(value: Value) -> Self {
182        ExecutionError::UnsupportedKeyType(value)
183    }
184
185    pub fn missing_argument_or_target() -> Self {
186        ExecutionError::MissingArgumentOrTarget
187    }
188}
189
190#[derive(Debug, Clone)]
191pub struct Program {
192    expression: Expression,
193}
194
195impl Program {
196    pub fn compile(source: &str) -> Result<Program, ParseErrors> {
197        let parser = Parser::default();
198        parser.parse(source).map(|expression| Program {
199            expression,
200        })
201    }
202
203    pub fn execute(&self, context: &Context) -> ResolveResult {
204        Value::resolve(&self.expression, context)
205    }
206
207    /// Returns the variables and functions referenced by the CEL program
208    ///
209    /// # Example
210    /// ```rust
211    /// # use bel::Program;
212    /// let program = Program::compile("length(foo) > 0").unwrap();
213    /// let references = program.references();
214    ///
215    /// assert!(references.has_function("length"));
216    /// assert!(references.has_variable("foo"));
217    /// ```
218    pub fn references(&self) -> ExpressionReferences<'_> {
219        self.expression.references()
220    }
221
222    /// Returns the contained expression
223    pub fn expression(&self) -> &Expression {
224        &self.expression
225    }
226}
227
228impl TryFrom<&str> for Program {
229    type Error = ParseErrors;
230
231    fn try_from(value: &str) -> Result<Self, Self::Error> {
232        Program::compile(value)
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use std::{collections::HashMap, convert::TryInto};
239
240    use crate::{
241        ExecutionError, Program,
242        context::Context,
243        objects::{ResolveResult, Value},
244    };
245
246    /// Tests the provided script and returns the result. An optional context can be provided.
247    pub(crate) fn test_script(script: &str, ctx: Option<Context>) -> ResolveResult {
248        let program = match Program::compile(script) {
249            Ok(p) => p,
250            Err(e) => panic!("{}", e),
251        };
252        program.execute(&ctx.unwrap_or_default())
253    }
254
255    #[test]
256    fn parse() {
257        Program::compile("1 + 1").unwrap();
258    }
259
260    #[test]
261    fn from_str() {
262        let input = "1.1";
263        let _p: Program = input.try_into().unwrap();
264    }
265
266    #[test]
267    fn variables() {
268        fn assert_output(script: &str, expected: ResolveResult) {
269            let mut ctx = Context::default();
270            ctx.add_variable_from_value("foo", HashMap::from([("bar", 1i64)]));
271            ctx.add_variable_from_value("arr", vec![1i64, 2, 3]);
272            ctx.add_variable_from_value("str", "foobar".to_string());
273            assert_eq!(test_script(script, Some(ctx)), expected);
274        }
275
276        // Test methods
277        assert_output("length([1, 2, 3]) == 3", Ok(true.into()));
278        assert_output("length([length([42]), 2, 3]) == 3", Ok(true.into()));
279        assert_output("length([]) == 3", Ok(false.into()));
280
281        // Test variable attribute traversals
282        assert_output("foo.bar == 1", Ok(true.into()));
283
284        // Test that we can index into an array
285        assert_output("arr[0] == 1", Ok(true.into()));
286
287        // Test that we can index into a string
288        assert_output("str[0] == \"f\"", Ok(true.into()));
289    }
290
291    #[test]
292    fn references() {
293        let p = Program::compile("[1, 1].map(x, x * 2)").unwrap();
294        assert!(p.references().has_variable("x"));
295        assert_eq!(p.references().variables().len(), 1);
296    }
297
298    #[test]
299    fn test_execution_errors() {
300        let tests = vec![
301            ("no such key", "foo.baz.bar == 1", ExecutionError::no_such_key("baz")),
302            (
303                "undeclared reference",
304                "missing == 1",
305                ExecutionError::undeclared_reference("missing"),
306            ),
307            (
308                "undeclared method",
309                "1.missing()",
310                ExecutionError::undeclared_reference("missing"),
311            ),
312            (
313                "undeclared function",
314                "missing(1)",
315                ExecutionError::undeclared_reference("missing"),
316            ),
317            (
318                "unsupported key type",
319                "{null: true}",
320                ExecutionError::unsupported_key_type(Value::Null),
321            ),
322        ];
323
324        for (name, script, error) in tests {
325            let mut ctx = Context::default();
326            ctx.add_variable_from_value("foo", HashMap::from([("bar", 1)]));
327            let res = test_script(script, Some(ctx));
328            assert_eq!(res, error.into(), "{name}");
329        }
330    }
331}