Expand description
A small expression evaluator for the expressions a workflow carries.
acts-expr compiles a source string once into an Expr and evaluates it
against a Context as often as needed — the same compiled expression can
be evaluated from any number of threads with different contexts. It has no
dependencies, allocates one Vec per compiled expression (nodes reference
their children by index, not by pointer) and never touches a global.
§The expression surface
- literals:
12,1.5,1e3,'text',"text",true,false,null - arithmetic:
+ - * / %, unary-, with( )to group - comparison:
== != < <= > >=(which do not chain, so1 < 2 < 3is a parse error rather than a type error) - logic:
&&,||, unary!, both short-circuiting - access:
a.b,a['b'],list[0], and callsf(x),a.f(x)
== compares numbers across int, uint and float, lists and objects
element-wise, and values of different kinds are not equal (so a workflow
that compares a missing value with a string is false, not an error).
Arithmetic requires numbers, + also concatenates two strings, and
&& || ! require bools. Two ints stay an int (so / truncates and % is
the remainder) and an overflow is an error; a uint — what JSON gives a
host for a value above i64::MAX — stays exact against an int, and any
float either side makes the result a float.
§Built-in methods
A value carries a few methods of its own, so the common test needs nothing injected:
- a string:
length(),startsWith(p),endsWith(p),indexOf(p),contains(p), andis_match(pattern)— a regular expression, behind theregexfeature - a list:
length(),contains(value)(an element, compared by value) - a map:
length()(its entries)
length() and indexOf() count characters, not bytes. These are found
last: the object’s own method, then a function of that name in the context,
then the built-in — so a host that injects length defines what it means.
§Injection
Everything an expression reads is in the Context: scalars, lists and
objects (Value::Map) as values, and methods as Value::Functions.
A function is called as f(x), or as a method with its receiver first —
x.f() — and a function stored inside an object is called that way
through the object, so an injected object carries its own methods.
use acts_expr::{Context, Expr, Value};
let mut context = Context::new();
context
.set("count", 3)
.set("status", "ok")
.set("step1", Value::map([
("value", Value::from(7)),
("id", Value::from("step1")),
]))
.set(
"upper",
Value::function(|args| match args {
[Value::Str(text)] => Ok(Value::from(text.to_uppercase())),
_ => Err(acts_expr::Error::Type { operation: "upper", got: "other" }),
}),
);
let expr = Expr::compile("step1.value + count * 2 >= 13 && status != 'bad'")?;
assert_eq!(expr.eval(&context)?, Value::Bool(true));
// A method: `upper` from the context, called with `status` as its receiver.
let name = Expr::compile("status.upper()")?;
assert_eq!(name.eval(&context)?, Value::from("OK"));§Deliberately not in the surface
The evaluator answers the expressions a workflow writes — conditions,
arithmetic on task data, string and object access — and nothing more. There
is no assignment, no statement, no ternary (?:), no collection literal
([...], {...}), no in, no comprehension, and no method beyond the
built-ins above — every other method is one the host injected. Each of the
absent forms is rejected with a message that says so.
Structs§
Enums§
Functions§
- eval
- Compile and evaluate in one step, for a caller that has no reason to keep the compiled expression.
Type Aliases§
- Map
- An object: a string-keyed map of values. Usually behind an
Arc, so reading a member is a pointer clone. - Native
Function - A native method. The receiver of a method call is its first argument, so
name.upper()andupper(name)are the same call. - Result
- The crate’s result type: the error is
Errorunless a caller names another.