decel 1.0.0

A Google Common Expression Language (CEL) implementation for D


To use this package, run the following command in your project's root directory:

Manual usage
Put the following dependency into your project's dependences section:

decel

A Common Expression Language (CEL) evaluator for D. CEL is a non-Turing-complete expression language designed for evaluating simple, safe expressions — typically policy rules, filters, or validation checks.

decel implements the CEL spec as a single-pass interpreter with no AST. See COMPATIBILITY.md for known deviations.

AI Notice

This repo is ~entirely developed by Claude 4.6 Opus. Good job Opus!

Installation

Add to your dub.sdl:

dependency "decel" version="~>1.0"

Or dub.json:

"dependencies": { "decel": "~>1.0" }

Usage

import decel;

// Evaluate an expression
auto result = evaluate(`1 + 2`, emptyContext());
assert(result == value(3L));

// With variables
auto ctx = contextFrom([
    "user":  value("alice"),
    "role":  value("admin"),
    "level": value(5L),
]);
auto allowed = evaluate(`role == "admin" || level >= 10`, ctx);
assert(allowed == value(true));

// Extract D types from results
auto n = evaluate(`2 + 3`, emptyContext()).get!long;    // 5
auto s = evaluate(`"hi"`, emptyContext()).get!string;    // "hi"

// Check for errors
auto err = evaluate(`1 / 0`, emptyContext());
assert(err.type == Value.Type.err);
assert(err.errMessage == "division by zero");

Types

CEL typeD storage typeLiteral examples
intlong42, -1, 0xFF
uintulong42u, 0xFFu
doubledouble3.14, 1.0
boolbooltrue, false
stringstring"hello", 'world', """multi"""
bytesimmutable(ubyte)[]b"abc"
null_typetypeof(null)null
listList (ArrayList)[1, 2, 3]
mapValue[string]{"key": "value"}
durationcore.time.Durationduration("PT1H30M")
timestampstd.datetime.SysTimetimestamp("2023-01-15T12:00:00Z")

Cross-type numeric operations work naturally: 1u == 1 is true, 1 + 1.5 promotes to double.

Operators

Arithmetic:   +  -  *  /  %
Comparison:   ==  !=  <  <=  >  >=
Logical:      &&  ||  !
Conditional:  ? :
Membership:   in
Index:        []
Member:       .

&& and || short-circuit: false && 1/0 evaluates to false. Logical operators enforce strict bool semantics — non-bool operands produce an error value, not implicit coercion.

Functions and Methods

// Size
size([1, 2, 3])        // 3
"hello".size()         // 5

// String methods
"hello world".contains("world")     // true
"hello".startsWith("hel")           // true
"hello".endsWith("llo")             // true
"abc123".matches("[a-z]+[0-9]+")    // true (full-string match)

// Type inspection
type(42)               // "int"
type("hello")          // "string"

// Type casts
int("42")              // 42
double(42)             // 42.0
string(42)             // "42"
uint(1)                // 1u

// Existence check
has(request.auth)      // true if auth field exists (not an error)

// Membership
"x" in {"x": 1}       // true
2 in [1, 2, 3]         // true
"el" in "hello"        // true

Comprehensions

List comprehensions use a list.method(var, expr) syntax:

[1, 2, 3, 4, 5].filter(x, x > 3)         // [4, 5]
[1, 2, 3].map(x, x * 2)                   // [2, 4, 6]
[1, 2, 3].all(x, x > 0)                   // true
[1, 2, 3].exists(x, x == 2)               // true
[1, 2, 3].exists_one(x, x > 2)            // true

Comprehensions chain: [1,2,3,4].map(x, x*2).filter(y, y > 4)[6, 8].

Duration and Timestamp

Durations use ISO 8601 format. Timestamps use RFC 3339.

duration("PT1H30M").minutes()     // 90
duration("PT1H") + duration("PT30M") == duration("PT1H30M")  // true

timestamp("2023-01-15T12:00:00Z").year()   // 2023
timestamp("2023-01-15T12:00:00Z") + duration("PT1H")
    == timestamp("2023-01-15T13:00:00Z")   // true

You can also pass D values directly via the context:

import core.time : seconds, hours;
import std.datetime.systime : SysTime, Clock;

auto ctx = contextFrom([
    "timeout": value(30.seconds),
    "created": value(Clock.currTime()),
]);
evaluate("timeout.seconds() > 10", ctx);  // true

Integrating Your Data

decel provides two abstract classes for exposing D data structures to CEL expressions without converting everything to Value up front.

Entry — Lazy Field Access

Subclass Entry to expose an object with named fields. The .field and ["field"] syntax both call resolve(). Return Value.err for unknown fields — this makes has() work automatically.

class HttpRequest : Entry
{
    private string _method;
    private string _path;
    private string[string] _headers;

    this(string method, string path, string[string] headers)
    {
        _method = method;
        _path = path;
        _headers = headers;
    }

    override Value resolve(string name)
    {
        switch (name)
        {
            case "method":  return value(_method);
            case "path":    return value(_path);
            case "headers":
                Value[string] hmap;
                foreach (k, v; _headers)
                    hmap[k] = value(v);
                return Value(hmap);
            default:
                return Value.err("no such field: " ~ name);
        }
    }
}

auto req = new HttpRequest("GET", "/api/users",
    ["Content-Type": "application/json", "Authorization": "Bearer tok"]);
auto ctx = contextFrom(["request": Value(cast(Entry) req)]);

evaluate(`request.method == "GET"`, ctx);                // true
evaluate(`request.path.startsWith("/api")`, ctx);        // true
evaluate(`"Authorization" in request.headers`, ctx);     // true
evaluate(`has(request.method)`, ctx);                    // true
evaluate(`has(request.missing)`, ctx);                   // false

List — Lazy Indexing

Subclass List to expose a sequence with length() and index(i) without materializing a Value[] array. All list operations work: size(), [i], in, +, and comprehensions.

class DatabaseRows : List
{
    private long _count;

    this(long count) { _count = count; }

    override size_t length() { return cast(size_t) _count; }

    override Value index(size_t i)
    {
        // Fetch row i on demand from your database
        return value(cast(long) i);
    }
}

auto ctx = contextFrom(["rows": Value(cast(List) new DatabaseRows(1_000_000))]);
evaluate("rows[42]", ctx);                     // fetches only row 42
evaluate("size(rows)", ctx);                   // 1000000 (no materialization)
evaluate("rows.exists(r, r == 42)", ctx);      // iterates lazily

Combining Entry and List

For real-world data models, nest Entry and List to expose a complete object graph. CEL expressions navigate it naturally:

class User : Entry
{
    string name;
    string role;

    override Value resolve(string field)
    {
        switch (field)
        {
            case "name": return value(name);
            case "role": return value(role);
            default:     return Value.err("no such field: " ~ field);
        }
    }
}

class UserList : List
{
    User[] users;

    override size_t length() { return users.length; }

    override Value index(size_t i)
    {
        return Value(cast(Entry) users[i]);
    }
}

auto users = new UserList();
users.users = [makeUser("alice", "admin"), makeUser("bob", "viewer")];

auto ctx = contextFrom([
    "users": Value(cast(List) users),
    "minRole": value("admin"),
]);

// Navigate the full object graph from CEL
evaluate(`users[0].name`, ctx);                              // "alice"
evaluate(`users.exists(u, u.role == "admin")`, ctx);         // true
evaluate(`users.filter(u, u.role == minRole).size()`, ctx);  // 1
evaluate(`users.all(u, has(u.name))`, ctx);                  // true

Scalar Values

For simple bindings, use value() to wrap D types directly:

auto ctx = contextFrom([
    "name":      value("alice"),
    "level":     value(5L),
    "active":    value(true),
    "score":     value(3.14),
    "tags":      value([value("a"), value("b")]),
    "metadata":  value(["env": value("prod"), "region": value("us-east-1")]),
]);

value() accepts long, ulong, double, bool, string, Value[] (creates an ArrayList), Duration, and SysTime.

Note on casts: When passing Entry or List subclasses into a Value, use an explicit cast: Value(cast(Entry) myEntry) or Value(cast(List) myList). This is needed because D's SumType stores the abstract base class, not your concrete subclass.

Error Handling

Parse errors (syntax errors, unexpected tokens) throw EvalException:

try
    evaluate(`1 +`, emptyContext());
catch (EvalException e)
    writeln(e.msg);  // "at position 3: unexpected eof"

Evaluation errors (type mismatches, division by zero, missing keys) are returned as error values, not exceptions:

auto result = evaluate(`1 / 0`, emptyContext());
if (result.type == Value.Type.err)
    writeln(result.errMessage);  // "division by zero"

Errors propagate through operators and are absorbed by short-circuit logic: false && (1/0 == 1)false.

Custom Macros

Extend the evaluator with custom function-call macros:

import decel;

Macro[string] customs;
customs["always_true"] = delegate Value(ref TokenRange r, const Env env, Context ctx) {
    parseExpr(r, env, ctx, 0);  // parse and discard the argument
    r.expect(Token.Kind.rparen); // consume closing ')'
    return value(true);
};

evaluateWithMacros(`always_true(anything)`, emptyContext(), customs);  // true

Macros receive the token stream and are responsible for parsing their own arguments and consuming the closing ). All needed types (Macro, TokenRange, Token, parseExpr) are exported from the decel package.

License

BSL-1.0

Authors:
  • Mathis Beer
Dependencies:
none
Versions:
1.2.1 2026-Feb-17
1.2.0 2026-Feb-16
1.1.0 2026-Feb-16
1.0.2 2026-Feb-16
1.0.1 2026-Feb-16
Show all 7 versions
Download Stats:
  • 0 downloads today

  • 0 downloads this week

  • 22 downloads this month

  • 22 downloads total

Score:
0.3
Short URL:
decel.dub.pm