Skip to content

Commit c6c70cc

Browse files
1BADragonCedarMatt
andauthored
Merge 1.0.7 (#51)
* Convert type functions to distpatch, add usage guide (#50) * Start uom function * Add uom function with tests * Add now for timestamp * Update type functions * Split up default macros and type functions * Create USAGE document and a bugfix * Fix build error * Add new bindings for wasm * Update toolchain * Bump version --------- Co-authored-by: matt <[email protected]> * remove unnessary ref * Some more touches * Proof read README --------- Co-authored-by: matt <[email protected]>
1 parent ac75285 commit c6c70cc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1853
-652
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default-members = ["rscel"]
44
resolver = "2"
55

66
[workspace.package]
7-
version = "1.0.6"
7+
version = "1.0.7"
88
edition = "2021"
99
description = "Cel interpreter in rust"
1010
license = "MIT"

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# rscel
22

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

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

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

27-
As of 0.10.0 binding protobuf messages from the protobuf crate is now available! Given
27+
As of 0.10.0, binding protobuf messages from the protobuf crate is now available! Given
2828
the following protobuf message:
2929
```protobuf
3030
@@ -39,7 +39,7 @@ The following code can be used to evaluate a CEL expression on a Point message:
3939
```rust
4040
use rscel::{CelContext, BindContext};
4141

42-
// currently rscel required protobuf messages to be in a box
42+
// currently rscel requires protobuf messages to be in a box
4343
let p = Box::new(protos::Point::new());
4444
p.x = 4;
4545
p.y = 5;

USAGE.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# RsCel Usage Guide
2+
3+
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()`.
4+
5+
## Running an Expression
6+
7+
```rust
8+
use rscel::{BindContext, CelContext, CelValue};
9+
10+
fn main() -> rscel::CelResult<()> {
11+
let mut programs = CelContext::new();
12+
let mut bindings = BindContext::new();
13+
14+
programs.add_program_str("main", "greeting + ' ' + subject")?;
15+
bindings.bind_param("greeting", "hello".into());
16+
bindings.bind_param("subject", "world".into());
17+
18+
let value = programs.exec("main", &bindings)?;
19+
assert_eq!(value, "hello world".into());
20+
Ok(())
21+
}
22+
```
23+
24+
`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.
25+
26+
## Language Basics
27+
28+
- **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`.
29+
- **Lists**: `[expr1, expr2, ...]`. Indexing uses zero-based integers; negative indices are allowed when compiled with the `neg_index` feature.
30+
- **Maps/Objects**: `{ 'key': value, other_key: value }`. Keys must resolve to strings at runtime.
31+
- **Access**: `obj.field` looks up a field or method; `value[index]` indexes lists, strings, bytes, or maps.
32+
- **Operators**: arithmetic (`+ - * / %`), comparison (`< <= > >= == !=`), logical (`!`, `&&`, `||` with short-circuit semantics), and membership (`lhs in rhs`). String membership checks substring containment; map membership checks for a key.
33+
- **Conditionals**: `condition ? when_true : when_false`.
34+
- **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.
35+
- **Format strings**: `f"hello {name}!"` interpolates expressions that must evaluate to strings.
36+
- **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.
37+
- **Errors**: runtime errors propagate as special values; most helpers short-circuit when they encounter `CelError` instances.
38+
39+
## Default Macros (`default_macros.rs`)
40+
41+
Macros operate on unresolved bytecode and therefore require identifiers for loop variables. All macros are available both at compile and runtime.
42+
43+
| Macro | Signature | Description |
44+
| --- | --- | --- |
45+
| `has(expr)` | `has(expr)` | Evaluates the expression and returns `true` unless a binding or attribute error occurs; other errors propagate. |
46+
| `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. |
47+
| `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. |
48+
| `list.exists(var, predicate)` | `[items].exists(x, test)` | Returns `true` as soon as one predicate evaluates truthy. |
49+
| `list.exists_one(var, predicate)` | `[items].exists_one(x, test)` | Requires exactly one truthy evaluation; returns `false` if zero or more than one match. |
50+
| `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. |
51+
| `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. |
52+
| `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. |
53+
54+
## Type Constructors (`type_funcs.rs`)
55+
56+
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.
57+
58+
| Function | Accepted Inputs | Output / Notes |
59+
| --- | --- | --- |
60+
| `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. |
61+
| `int(x)` | `int`, `uint`, `float`, `bool`, `string` (parsed as base-10), `timestamp` | Converts to `i64`; parsing failures raise `value()` errors. |
62+
| `uint(x)` | `uint`, `int`, `float`, `bool`, `string` | Converts to `u64`; negative numbers or invalid strings raise errors. |
63+
| `double(x)` / `float(x)` | `float`, `int`, `uint`, `bool`, `string` | Produces an `f64`; parsing errors surface as `value()` errors. |
64+
| `string(x)` | numbers, strings, UTF-8 bytes, timestamps (RFC3339), durations, others | Converts to string; non UTF-8 bytes produce an error. |
65+
| `bytes(x)` | strings, existing bytes | Returns a byte array (`CelBytes`). |
66+
| `timestamp()` | no args, RFC3339 / RFC2822 / `DateTime<Utc>` / epoch seconds (`int`/`uint`) | Returns a UTC timestamp; invalid formats raise errors. |
67+
| `duration(x)` | ISO-like duration strings understood by `duration_str`, integer seconds, `(seconds, nanos)` pair, `chrono::Duration` | Returns a `Duration`; invalid formats raise errors. |
68+
| `dyn(x)` | any value | Identity; exposes the dynamic value type. |
69+
| `type(x)` | any value | Returns the CEL type descriptor (e.g., `int`, `string`, `timestamp`). |
70+
71+
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`.
72+
73+
## Default Functions (`default_funcs.rs`)
74+
75+
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.
76+
77+
### Collection helpers
78+
79+
- `size(value)` – Length of a string, bytes, or list.
80+
- `sort(list)` – Returns a new list sorted using CEL ordering; non-comparable members yield `invalid_op` errors.
81+
- `zip(list1, list2, ...)` – Zips multiple lists into a list of same-length tuples (shortest list wins); arguments must all be lists.
82+
- `min(arg1, arg2, ...)` / `max(...)` – Vararg numeric/string comparator that returns the min/max; at least one argument required.
83+
84+
### String and text helpers
85+
86+
- `contains`, `containsI` – Substring containment (case-sensitive / case-insensitive).
87+
- `startsWith`, `startsWithI`, `endsWith`, `endsWithI` – Prefix/suffix checks.
88+
- `matches` – Returns `true` if a regex matches the entire string; invalid regex patterns raise a `value()` error.
89+
- `matchCaptures` – Returns a list of capture groups (entire match first) or `null` if the regex does not match.
90+
- `matchReplace`, `matchReplaceOnce` – Regex replacement across all matches or only the first match.
91+
- `remove` – Removes all non-overlapping occurrences of a literal substring.
92+
- `replace` – Literal string replacement.
93+
- `split`, `rsplit` – Split on a literal delimiter from the left/right.
94+
- `splitAt` – Splits at an index, returning `[left, right]`.
95+
- `splitWhiteSpace` – Splits on any Unicode whitespace.
96+
- `trim`, `trimStart`, `trimEnd` – Trim ASCII whitespace.
97+
- `trimStartMatches`, `trimEndMatches` – Trim a literal prefix/suffix repeatedly.
98+
- `toLower`, `toUpper` – Case conversion.
99+
100+
All string helpers expect `this` to be a string; non-string inputs produce `value()` errors.
101+
102+
### Math & numeric helpers
103+
104+
- `abs(number)` – Absolute value for `int`, `uint`, and `double`.
105+
- `sqrt(number)` – Square root returning `double`.
106+
- `pow(base, exponent)` – Exponentiation for numeric combinations (integer exponents for integral bases).
107+
- `log(number)` – Base-10 logarithm (`ilog10` for integers/unsigned integers).
108+
- `lg(number)` - Base-2 logarithm
109+
- `ceil(number)`, `floor(number)`, `round(number)` – Standard rounding family; integral inputs are returned unchanged.
110+
111+
### Time & date helpers
112+
113+
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`.
114+
115+
- `getDate(timestamp[, timezone])` – Day of month (1–31).
116+
- `getDayOfMonth(timestamp[, timezone])` – Zero-based day of month (0–30).
117+
- `getDayOfWeek(timestamp[, timezone])` – Day of week (`0` = Sunday).
118+
- `getDayOfYear(timestamp[, timezone])` – Zero-based day of year.
119+
- `getFullYear(timestamp[, timezone])` – Four-digit year.
120+
- `getMonth(timestamp[, timezone])` – Zero-based month (`0` = January).
121+
- `getHours(timestamp | duration[, timezone])` – Hour of day or total hours of a duration.
122+
- `getMinutes(timestamp | duration[, timezone])` – Minute of hour or total minutes of a duration.
123+
- `getSeconds(timestamp | duration[, timezone])` – Second of minute or total seconds of a duration.
124+
- `getMilliseconds(timestamp | duration[, timezone])` – Millisecond component or total milliseconds of a duration.
125+
- `now()` – Current UTC timestamp (no arguments).
126+
127+
### Unit conversion
128+
129+
- `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:
130+
- **Mass**: kilogram (`kg`), gram (`g`), milligram, pound (`lb`, `lbs`), ounce (`oz`), stone, slug, ton/tonne.
131+
- **Volume**: liter (`l`), milliliter, gallon, quart (liquid/dry), pint (liquid/dry), cup, fluid ounce, tablespoon, teaspoon, cubic meter/foot/yard.
132+
- **Speed**: meter per second (`m/s`), kilometer per hour (`km/h`, `kph`), mile per hour (`mph`), knot, foot per second (`ft/s`, `fps`).
133+
- **Temperature**: Celsius (`C`), Fahrenheit (`F`), Kelvin (`K`).
134+
135+
Conversions only succeed within the same category. Invalid or mixed-unit requests raise argument errors.
136+
137+
### Miscellaneous helpers
138+
139+
- `size(value)` – See Collections.
140+
- `sort(list)` – See Collections.
141+
- `zip(list, ...)` – See Collections.
142+
143+
## Putting it together
144+
145+
```text
146+
// Filter, map, and aggregate a bound list of structs.
147+
accounts.map(a,
148+
a.balance_cents > 0,
149+
{
150+
'id': a.id,
151+
'balance': a.balance_cents / 100
152+
}
153+
).reduce(total, acct, total + acct.balance, 0)
154+
```
155+
156+
Combine macros and helpers freely. Errors or type mismatches surface as `CelError` instances, so guard with `has()` or `coalesce()` where appropriate.
157+
158+
## Extending the environment
159+
160+
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.
161+

rscel-macro/src/types/dispatch_arg.rs

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,35 +45,44 @@ impl DispatchArg {
4545
self.this
4646
}
4747

48-
pub fn as_tuple_struct(&self, ident: &str) -> PatTupleStruct {
49-
PatTupleStruct {
50-
attrs: Vec::new(),
51-
qself: None,
52-
path: syn::Path {
53-
leading_colon: None,
54-
segments: [
55-
PathSegment {
56-
ident: Ident::new("CelValue", Span::call_site()),
57-
arguments: syn::PathArguments::None,
58-
},
59-
PathSegment {
60-
ident: Ident::new(self.arg_type.celvalue_enum(), Span::call_site()),
61-
arguments: syn::PathArguments::None,
62-
},
63-
]
64-
.into_iter()
65-
.collect(),
66-
},
67-
paren_token: token::Paren::default(),
68-
elems: [Pat::Ident(PatIdent {
48+
pub fn as_pat(&self, ident: &str) -> Pat {
49+
match self.arg_type {
50+
DispatchArgType::CelValue => Pat::Ident(PatIdent {
6951
attrs: Vec::new(),
7052
by_ref: None,
7153
mutability: None,
7254
ident: Ident::new(ident, Span::call_site()),
7355
subpat: None,
74-
})]
75-
.into_iter()
76-
.collect(),
56+
}),
57+
_ => Pat::TupleStruct(PatTupleStruct {
58+
attrs: Vec::new(),
59+
qself: None,
60+
path: syn::Path {
61+
leading_colon: None,
62+
segments: [
63+
PathSegment {
64+
ident: Ident::new("CelValue", Span::call_site()),
65+
arguments: syn::PathArguments::None,
66+
},
67+
PathSegment {
68+
ident: Ident::new(self.arg_type.celvalue_enum(), Span::call_site()),
69+
arguments: syn::PathArguments::None,
70+
},
71+
]
72+
.into_iter()
73+
.collect(),
74+
},
75+
paren_token: token::Paren::default(),
76+
elems: [Pat::Ident(PatIdent {
77+
attrs: Vec::new(),
78+
by_ref: None,
79+
mutability: None,
80+
ident: Ident::new(ident, Span::call_site()),
81+
subpat: None,
82+
})]
83+
.into_iter()
84+
.collect(),
85+
}),
7786
}
7887
}
7988
}

rscel-macro/src/types/dispatch_arg_type.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ pub enum DispatchArgType {
1515
Duration,
1616
CelResult,
1717
CelValue,
18+
Null,
1819
}
1920

2021
impl DispatchArgType {
2122
pub fn from_type(pat: &Type) -> Self {
2223
match pat {
24+
Type::Tuple(tuple) if tuple.elems.is_empty() => DispatchArgType::Null,
2325
Type::Path(path) => {
2426
if path.qself.is_some() {
2527
panic!("Path qualifiers not allowed");
@@ -54,6 +56,8 @@ impl DispatchArgType {
5456
"CelResult" => DispatchArgType::CelResult,
5557
"CelBytes" => DispatchArgType::Bytes,
5658
"CelValue" => DispatchArgType::CelValue,
59+
"CelValueMap" => DispatchArgType::Map,
60+
"HashMap" => DispatchArgType::Map,
5761
other => panic!("Unknown type: {}", other),
5862
},
5963
None => panic!("No type info"),
@@ -77,6 +81,7 @@ impl DispatchArgType {
7781
DispatchArgType::Duration => 'y',
7882
DispatchArgType::CelResult => 'r',
7983
DispatchArgType::CelValue => 'z',
84+
DispatchArgType::Null => 'n',
8085
}
8186
}
8287

@@ -92,6 +97,7 @@ impl DispatchArgType {
9297
DispatchArgType::Map => "Map",
9398
DispatchArgType::Timestamp => "TimeStamp",
9499
DispatchArgType::Duration => "Duration",
100+
DispatchArgType::Null => "Null",
95101
_ => unreachable!(),
96102
}
97103
}

rscel-macro/src/types/dispatch_func.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use proc_macro2::Span;
2-
use syn::{punctuated::Punctuated, token, Arm, Ident, ItemFn, Pat, PatPath, PatTuple, PathSegment};
2+
use syn::{
3+
punctuated::Punctuated, token, Arm, Attribute, Ident, ItemFn, Pat, PatPath, PatTuple,
4+
PathSegment,
5+
};
36

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

@@ -46,12 +49,23 @@ impl DispatchFunc {
4649
}
4750

4851
pub fn as_arm(&self, max_args: usize) -> Arm {
52+
let attrs: Vec<Attribute> = self
53+
.func
54+
.attrs
55+
.iter()
56+
.filter(|attr| {
57+
let path = attr.path();
58+
path.is_ident("cfg") || path.is_ident("cfg_attr")
59+
})
60+
.cloned()
61+
.collect();
62+
4963
let mut elems = Vec::new();
5064
let mut args: Vec<syn::Expr> = Vec::new();
5165
let mut arg_index = 0usize;
5266

5367
if self.args.len() > arg_index && self.args[arg_index].is_this() {
54-
elems.push(Pat::TupleStruct(self.args[0].as_tuple_struct("this")));
68+
elems.push(self.args[0].as_pat("this"));
5569
args.push(syn::Expr::Path(syn::ExprPath {
5670
attrs: Vec::new(),
5771
qself: None,
@@ -103,7 +117,7 @@ impl DispatchFunc {
103117
}));
104118
} else {
105119
let a = format!("a{}", i);
106-
elems.push(Pat::TupleStruct(self.args[arg_index].as_tuple_struct(&a)));
120+
elems.push(self.args[arg_index].as_pat(&a));
107121
args.push(syn::Expr::Path(syn::ExprPath {
108122
attrs: Vec::new(),
109123
qself: None,
@@ -114,7 +128,7 @@ impl DispatchFunc {
114128
}
115129

116130
Arm {
117-
attrs: Vec::new(),
131+
attrs,
118132
pat: Pat::Tuple(PatTuple {
119133
attrs: Vec::new(),
120134
paren_token: token::Paren::default(),

rscel/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ duration-str = "0.13.0"
3030
protobuf = { version = "3.7.1", optional = true }
3131
chrono-tz = "0.10.1"
3232
num-traits = "0.2.19"
33+
uom = "0.36.0"

0 commit comments

Comments
 (0)