Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ default-members = ["rscel"]
resolver = "2"

[workspace.package]
version = "1.0.6"
version = "1.0.7"
edition = "2021"
description = "Cel interpreter in rust"
license = "MIT"
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# rscel

RsCel is a CEL evaluator written in Rust. CEL is a google project that
describes a turing-incomplete language that can be used to evaluate
a user provdided expression. The language specification can be found
RsCel is a CEL evaluator written in Rust. CEL is a Turing-incomplete
language that can be used to evaluate
a user-provided expression. The language specification can be found
[here](https://github.com/google/cel-spec/blob/master/doc/langdef.md).

The design goals of this project were are as follows:
The design goals of this project are as follows:
* Isolated execution of CEL expressions
* Flexible enough to allow for a user to bend the spec if needed
* Sandbox'ed in such a way that only specific values can be bound
* Can be used as a wasm depenedency (or other ffi)
* Can be used as a WASM depenedency (or other FFI)

The basic example of how to use:
```rust
Expand All @@ -24,7 +24,7 @@ let res = ctx.exec("main", &exec_ctx).unwrap(); // CelValue::Int(6)
assert_eq!(res, 6.into());
```

As of 0.10.0 binding protobuf messages from the protobuf crate is now available! Given
As of 0.10.0, binding protobuf messages from the protobuf crate is now available! Given
the following protobuf message:
```protobuf

Expand All @@ -39,7 +39,7 @@ The following code can be used to evaluate a CEL expression on a Point message:
```rust
use rscel::{CelContext, BindContext};

// currently rscel required protobuf messages to be in a box
// currently rscel requires protobuf messages to be in a box
let p = Box::new(protos::Point::new());
p.x = 4;
p.y = 5;
Expand Down
161 changes: 161 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# RsCel Usage Guide

RsCel implements the [Common Expression Language (CEL)](https://github.com/google/cel-spec) in Rust. This guide describes the expression syntax exposed by `rscel` and the default macros, type constructors, and functions that are preloaded when you build a `BindContext` with `BindContext::new()`.

## Running an Expression

```rust
use rscel::{BindContext, CelContext, CelValue};

fn main() -> rscel::CelResult<()> {
let mut programs = CelContext::new();
let mut bindings = BindContext::new();

programs.add_program_str("main", "greeting + ' ' + subject")?;
bindings.bind_param("greeting", "hello".into());
bindings.bind_param("subject", "world".into());

let value = programs.exec("main", &bindings)?;
assert_eq!(value, "hello world".into());
Ok(())
}
```

`BindContext::new()` registers the macros and functions listed below and exposes CEL types ("int", "string", etc.) so that they can be compared or invoked as constructors inside an expression.

## Language Basics

- **Literals**: signed integers (`123`), unsigned integers (`123u`), floating point numbers (`3.14`, `.5`, `1.`), quoted strings (`"foo"` or `'foo'` with `\u`/`\x` escapes), byte strings (`b"abc"`), booleans, and `null`.
- **Lists**: `[expr1, expr2, ...]`. Indexing uses zero-based integers; negative indices are allowed when compiled with the `neg_index` feature.
- **Maps/Objects**: `{ 'key': value, other_key: value }`. Keys must resolve to strings at runtime.
- **Access**: `obj.field` looks up a field or method; `value[index]` indexes lists, strings, bytes, or maps.
- **Operators**: arithmetic (`+ - * / %`), comparison (`< <= > >= == !=`), logical (`!`, `&&`, `||` with short-circuit semantics), and membership (`lhs in rhs`). String membership checks substring containment; map membership checks for a key.
- **Conditionals**: `condition ? when_true : when_false`.
- **Match expressions**: `match value { case x < 0: ..., case type(string): ..., case _: ... }` supports comparison patterns, type tests (`int`, `uint`, `float`, `string`, `bool`, `bytes`, `list`, `object`, `null`, `timestamp`, `duration`), and a wildcard case.
- **Format strings**: `f"hello {name}!"` interpolates expressions that must evaluate to strings.
- **Truthiness**: numbers are truthy when non-zero, collections when non-empty, timestamps/durations/types always truthy, and `null`/errors are falsy. Logical operators and macros rely on this notion.
- **Errors**: runtime errors propagate as special values; most helpers short-circuit when they encounter `CelError` instances.

## Default Macros (`default_macros.rs`)

Macros operate on unresolved bytecode and therefore require identifiers for loop variables. All macros are available both at compile and runtime.

| Macro | Signature | Description |
| --- | --- | --- |
| `has(expr)` | `has(expr)` | Evaluates the expression and returns `true` unless a binding or attribute error occurs; other errors propagate. |
| `coalesce(expr...)` | `coalesce(e1, e2, ...)` | Returns the first argument that does not resolve to `null` and is not a binding/attribute error; resolves each expression in order. |
| `list.all(var, predicate)` | `[1,2,3].all(x, x < 5)` | Binds each element to `var` and ensures every iteration is truthy. Returns `false` upon the first falsy result. |
| `list.exists(var, predicate)` | `[items].exists(x, test)` | Returns `true` as soon as one predicate evaluates truthy. |
| `list.exists_one(var, predicate)` | `[items].exists_one(x, test)` | Requires exactly one truthy evaluation; returns `false` if zero or more than one match. |
| `list.filter(var, predicate)` | `[items].filter(x, keep?)` | Builds a list of elements whose predicate is truthy. When invoked on a map, the identifier receives each key and returns the list of kept keys. |
| `list.map(var, mapper)` | `[items].map(x, expr)` | Collects the mapper result for every element. A ternary form `[items].map(x, predicate, mapper)` first evaluates `predicate` and only maps elements where it is truthy. With maps, the variable receives each key and returns a list of mapped values. |
| `list.reduce(acc, item, step, initial)` | `[items].reduce(curr, next, step_expr, seed)` | Initializes `curr` with `seed`; for each element binds `next` to the element, `curr` to the running total, evaluates `step_expr`, and stores the result back in `curr`. Returns the final accumulator. |

## Type Constructors (`type_funcs.rs`)

These helpers can be invoked either as global functions (`int(value)`) or as methods (`value.int()` when dispatched that way) and mirror CEL's built-in type conversion rules.

| Function | Accepted Inputs | Output / Notes |
| --- | --- | --- |
| `bool(x)` | `bool`, supported strings (`"1"`, `"0"`, `"t"`, `"f"`, case-insensitive `"true"`/`"false"`), and under the optional `type_prop` feature any value | Returns a boolean or raises `value()` on invalid strings. |
| `int(x)` | `int`, `uint`, `float`, `bool`, `string` (parsed as base-10), `timestamp` | Converts to `i64`; parsing failures raise `value()` errors. |
| `uint(x)` | `uint`, `int`, `float`, `bool`, `string` | Converts to `u64`; negative numbers or invalid strings raise errors. |
| `double(x)` / `float(x)` | `float`, `int`, `uint`, `bool`, `string` | Produces an `f64`; parsing errors surface as `value()` errors. |
| `string(x)` | numbers, strings, UTF-8 bytes, timestamps (RFC3339), durations, others | Converts to string; non UTF-8 bytes produce an error. |
| `bytes(x)` | strings, existing bytes | Returns a byte array (`CelBytes`). |
| `timestamp()` | no args, RFC3339 / RFC2822 / `DateTime<Utc>` / epoch seconds (`int`/`uint`) | Returns a UTC timestamp; invalid formats raise errors. |
| `duration(x)` | ISO-like duration strings understood by `duration_str`, integer seconds, `(seconds, nanos)` pair, `chrono::Duration` | Returns a `Duration`; invalid formats raise errors. |
| `dyn(x)` | any value | Identity; exposes the dynamic value type. |
| `type(x)` | any value | Returns the CEL type descriptor (e.g., `int`, `string`, `timestamp`). |

The constructor names (`int`, `uint`, `double`, `bytes`, etc.) are also exported as type values in the global scope, allowing comparisons such as `type(value) == int`.

## Default Functions (`default_funcs.rs`)

All functions can be called as free functions (`size(list)`) or as methods (`list.size()`, `'text'.contains('t')`). They return CEL values and propagate errors when arguments are invalid.

### Collection helpers

- `size(value)` – Length of a string, bytes, or list.
- `sort(list)` – Returns a new list sorted using CEL ordering; non-comparable members yield `invalid_op` errors.
- `zip(list1, list2, ...)` – Zips multiple lists into a list of same-length tuples (shortest list wins); arguments must all be lists.
- `min(arg1, arg2, ...)` / `max(...)` – Vararg numeric/string comparator that returns the min/max; at least one argument required.

### String and text helpers

- `contains`, `containsI` – Substring containment (case-sensitive / case-insensitive).
- `startsWith`, `startsWithI`, `endsWith`, `endsWithI` – Prefix/suffix checks.
- `matches` – Returns `true` if a regex matches the entire string; invalid regex patterns raise a `value()` error.
- `matchCaptures` – Returns a list of capture groups (entire match first) or `null` if the regex does not match.
- `matchReplace`, `matchReplaceOnce` – Regex replacement across all matches or only the first match.
- `remove` – Removes all non-overlapping occurrences of a literal substring.
- `replace` – Literal string replacement.
- `split`, `rsplit` – Split on a literal delimiter from the left/right.
- `splitAt` – Splits at an index, returning `[left, right]`.
- `splitWhiteSpace` – Splits on any Unicode whitespace.
- `trim`, `trimStart`, `trimEnd` – Trim ASCII whitespace.
- `trimStartMatches`, `trimEndMatches` – Trim a literal prefix/suffix repeatedly.
- `toLower`, `toUpper` – Case conversion.

All string helpers expect `this` to be a string; non-string inputs produce `value()` errors.

### Math & numeric helpers

- `abs(number)` – Absolute value for `int`, `uint`, and `double`.
- `sqrt(number)` – Square root returning `double`.
- `pow(base, exponent)` – Exponentiation for numeric combinations (integer exponents for integral bases).
- `log(number)` – Base-10 logarithm (`ilog10` for integers/unsigned integers).
- `lg(number)` - Base-2 logarithm
- `ceil(number)`, `floor(number)`, `round(number)` – Standard rounding family; integral inputs are returned unchanged.

### Time & date helpers

All time functions operate on `timestamp()` or `duration()` results. Where noted, a second optional argument is an IANA timezone string (e.g., `"America/Los_Angeles"`) which is resolved using `chrono_tz`.

- `getDate(timestamp[, timezone])` – Day of month (1–31).
- `getDayOfMonth(timestamp[, timezone])` – Zero-based day of month (0–30).
- `getDayOfWeek(timestamp[, timezone])` – Day of week (`0` = Sunday).
- `getDayOfYear(timestamp[, timezone])` – Zero-based day of year.
- `getFullYear(timestamp[, timezone])` – Four-digit year.
- `getMonth(timestamp[, timezone])` – Zero-based month (`0` = January).
- `getHours(timestamp | duration[, timezone])` – Hour of day or total hours of a duration.
- `getMinutes(timestamp | duration[, timezone])` – Minute of hour or total minutes of a duration.
- `getSeconds(timestamp | duration[, timezone])` – Second of minute or total seconds of a duration.
- `getMilliseconds(timestamp | duration[, timezone])` – Millisecond component or total milliseconds of a duration.
- `now()` – Current UTC timestamp (no arguments).

### Unit conversion

- `uomConvert(value, from_unit, to_unit)` – Converts between supported units using the [`uom`](https://docs.rs/uom) crate. Units are case-insensitive and trimmed of leading `°`. Supported categories:
- **Mass**: kilogram (`kg`), gram (`g`), milligram, pound (`lb`, `lbs`), ounce (`oz`), stone, slug, ton/tonne.
- **Volume**: liter (`l`), milliliter, gallon, quart (liquid/dry), pint (liquid/dry), cup, fluid ounce, tablespoon, teaspoon, cubic meter/foot/yard.
- **Speed**: meter per second (`m/s`), kilometer per hour (`km/h`, `kph`), mile per hour (`mph`), knot, foot per second (`ft/s`, `fps`).
- **Temperature**: Celsius (`C`), Fahrenheit (`F`), Kelvin (`K`).

Conversions only succeed within the same category. Invalid or mixed-unit requests raise argument errors.

### Miscellaneous helpers

- `size(value)` – See Collections.
- `sort(list)` – See Collections.
- `zip(list, ...)` – See Collections.

## Putting it together

```text
// Filter, map, and aggregate a bound list of structs.
accounts.map(a,
a.balance_cents > 0,
{
'id': a.id,
'balance': a.balance_cents / 100
}
).reduce(total, acct, total + acct.balance, 0)
```

Combine macros and helpers freely. Errors or type mismatches surface as `CelError` instances, so guard with `has()` or `coalesce()` where appropriate.

## Extending the environment

You can bind additional values, functions, and macros via `BindContext::bind_param`, `bind_func`, and `bind_macro`. All defaults documented above remain available unless you intentionally replace them.

57 changes: 33 additions & 24 deletions rscel-macro/src/types/dispatch_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,44 @@ impl DispatchArg {
self.this
}

pub fn as_tuple_struct(&self, ident: &str) -> PatTupleStruct {
PatTupleStruct {
attrs: Vec::new(),
qself: None,
path: syn::Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident::new("CelValue", Span::call_site()),
arguments: syn::PathArguments::None,
},
PathSegment {
ident: Ident::new(self.arg_type.celvalue_enum(), Span::call_site()),
arguments: syn::PathArguments::None,
},
]
.into_iter()
.collect(),
},
paren_token: token::Paren::default(),
elems: [Pat::Ident(PatIdent {
pub fn as_pat(&self, ident: &str) -> Pat {
match self.arg_type {
DispatchArgType::CelValue => Pat::Ident(PatIdent {
attrs: Vec::new(),
by_ref: None,
mutability: None,
ident: Ident::new(ident, Span::call_site()),
subpat: None,
})]
.into_iter()
.collect(),
}),
_ => Pat::TupleStruct(PatTupleStruct {
attrs: Vec::new(),
qself: None,
path: syn::Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident::new("CelValue", Span::call_site()),
arguments: syn::PathArguments::None,
},
PathSegment {
ident: Ident::new(self.arg_type.celvalue_enum(), Span::call_site()),
arguments: syn::PathArguments::None,
},
]
.into_iter()
.collect(),
},
paren_token: token::Paren::default(),
elems: [Pat::Ident(PatIdent {
attrs: Vec::new(),
by_ref: None,
mutability: None,
ident: Ident::new(ident, Span::call_site()),
subpat: None,
})]
.into_iter()
.collect(),
}),
}
}
}
6 changes: 6 additions & 0 deletions rscel-macro/src/types/dispatch_arg_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ pub enum DispatchArgType {
Duration,
CelResult,
CelValue,
Null,
}

impl DispatchArgType {
pub fn from_type(pat: &Type) -> Self {
match pat {
Type::Tuple(tuple) if tuple.elems.is_empty() => DispatchArgType::Null,
Type::Path(path) => {
if path.qself.is_some() {
panic!("Path qualifiers not allowed");
Expand Down Expand Up @@ -54,6 +56,8 @@ impl DispatchArgType {
"CelResult" => DispatchArgType::CelResult,
"CelBytes" => DispatchArgType::Bytes,
"CelValue" => DispatchArgType::CelValue,
"CelValueMap" => DispatchArgType::Map,
"HashMap" => DispatchArgType::Map,
other => panic!("Unknown type: {}", other),
},
None => panic!("No type info"),
Expand All @@ -77,6 +81,7 @@ impl DispatchArgType {
DispatchArgType::Duration => 'y',
DispatchArgType::CelResult => 'r',
DispatchArgType::CelValue => 'z',
DispatchArgType::Null => 'n',
}
}

Expand All @@ -92,6 +97,7 @@ impl DispatchArgType {
DispatchArgType::Map => "Map",
DispatchArgType::Timestamp => "TimeStamp",
DispatchArgType::Duration => "Duration",
DispatchArgType::Null => "Null",
_ => unreachable!(),
}
}
Expand Down
22 changes: 18 additions & 4 deletions rscel-macro/src/types/dispatch_func.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use proc_macro2::Span;
use syn::{punctuated::Punctuated, token, Arm, Ident, ItemFn, Pat, PatPath, PatTuple, PathSegment};
use syn::{
punctuated::Punctuated, token, Arm, Attribute, Ident, ItemFn, Pat, PatPath, PatTuple,
PathSegment,
};

use super::{dispatch_arg::DispatchArg, dispatch_arg_type::DispatchArgType};

Expand Down Expand Up @@ -46,12 +49,23 @@ impl DispatchFunc {
}

pub fn as_arm(&self, max_args: usize) -> Arm {
let attrs: Vec<Attribute> = self
.func
.attrs
.iter()
.filter(|attr| {
let path = attr.path();
path.is_ident("cfg") || path.is_ident("cfg_attr")
})
.cloned()
.collect();

let mut elems = Vec::new();
let mut args: Vec<syn::Expr> = Vec::new();
let mut arg_index = 0usize;

if self.args.len() > arg_index && self.args[arg_index].is_this() {
elems.push(Pat::TupleStruct(self.args[0].as_tuple_struct("this")));
elems.push(self.args[0].as_pat("this"));
args.push(syn::Expr::Path(syn::ExprPath {
attrs: Vec::new(),
qself: None,
Expand Down Expand Up @@ -103,7 +117,7 @@ impl DispatchFunc {
}));
} else {
let a = format!("a{}", i);
elems.push(Pat::TupleStruct(self.args[arg_index].as_tuple_struct(&a)));
elems.push(self.args[arg_index].as_pat(&a));
args.push(syn::Expr::Path(syn::ExprPath {
attrs: Vec::new(),
qself: None,
Expand All @@ -114,7 +128,7 @@ impl DispatchFunc {
}

Arm {
attrs: Vec::new(),
attrs,
pat: Pat::Tuple(PatTuple {
attrs: Vec::new(),
paren_token: token::Paren::default(),
Expand Down
1 change: 1 addition & 0 deletions rscel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ duration-str = "0.13.0"
protobuf = { version = "3.7.1", optional = true }
chrono-tz = "0.10.1"
num-traits = "0.2.19"
uom = "0.36.0"
Loading
Loading