3 releases
| 0.1.3 | Jan 19, 2026 |
|---|---|
| 0.1.1 | Jan 19, 2026 |
| 0.1.0 | Jan 19, 2026 |
#547 in Development tools
Used in cel-core-lsp
54KB
1.5K
SLoC
CEL-Core
A spec-complete Common Expression Language (CEL) implementation written in rust.
CEL-Core provides the full CEL pipeline — parsing, type checking, and evaluation — with wire compatibility to cel-go and cel-cpp through protobuf AST conversion. Built on top of the core library is a Language Server Protocol (LSP) server for IDE support.
Installation
[dependencies]
cel-core = "0.1"
# Optional: proto wire format interop with cel-go/cel-cpp
cel-core-proto = "0.1"
The CEL Environment
The Env is the entry point for working with CEL expressions. It holds variable declarations, function definitions, and extensions, then compiles expressions into type-checked ASTs and evaluable programs.
use cel_core::{Env, CelType, Value, MapActivation};
let env = Env::with_standard_library()
.with_variable("user", CelType::String)
.with_variable("age", CelType::Int);
let ast = env.compile("age >= 21 && user.startsWith('admin')")?;
let program = env.program(&ast)?;
let mut activation = MapActivation::new();
activation.insert("user", "admin_alice");
activation.insert("age", 25);
let result = program.eval(&activation);
assert_eq!(result, Value::Bool(true));
Compile once, evaluate many
Compilation (parsing + type checking) is the expensive step. Once you have a Program, evaluation is fast and can be called repeatedly with different variable bindings.
let ast = env.compile("age >= threshold")?;
let program = env.program(&ast)?;
for threshold in [18, 21, 65] {
let mut activation = MapActivation::new();
activation.insert("age", 25);
activation.insert("threshold", threshold);
let result = program.eval(&activation);
println!("threshold {}: {}", threshold, result);
}
Extensions
CEL-Core implements the standard CEL extension libraries. Enable them individually or all at once:
use cel_core::ext;
// All extensions
let env = Env::with_standard_library()
.with_all_extensions();
// Or individually
let env = Env::with_standard_library()
.with_extension(ext::string_extension())
.with_extension(ext::math_extension())
.with_extension(ext::encoders_extension())
.with_extension(ext::optionals_extension());
String extension — charAt, indexOf, lastIndexOf, substring, split, join, strings.quote, lowerAscii, upperAscii, replace, trim, reverse, format
Math extension — math.greatest, math.least, math.abs, math.sign, math.isNaN, math.isFinite, math.isInf, math.bitAnd, math.bitOr, math.bitNot, math.bitXor, math.bitShiftLeft, math.bitShiftRight, math.ceil, math.floor, math.round, math.trunc
Encoders extension — base64.encode, base64.decode
Optionals extension — optional.of, optional.none, optional.ofNonZeroValue, hasValue, value, or, orValue
Working with values
Variable bindings accept any type that implements Into<Value>. Results can be extracted back into Rust types with TryFrom:
let mut activation = MapActivation::new();
activation.insert("items", Value::list([1, 2, 3]));
activation.insert("config", Value::map([("host", "localhost"), ("port", "8080")]));
let result = program.eval(&activation);
// Extract Rust types from results
let n: i64 = (&result).try_into()?;
let s: &str = (&result).try_into()?;
let b: bool = (&result).try_into()?;
let list: &[Value] = (&result).try_into()?;
let map: &ValueMap = (&result).try_into()?;
Proto message types
CEL expressions can operate on protobuf messages. Create a ProstProtoRegistry, load your proto descriptors, and pass it to the environment:
use std::sync::Arc;
use cel_core_proto::ProstProtoRegistry;
// Create registry (includes well-known types like Timestamp, Duration, etc.)
let mut registry = ProstProtoRegistry::new();
// Load your proto definitions from a file descriptor set
// (generated by protoc --descriptor_set_out, buf build, or prost-build)
registry.add_file_descriptor_set(MY_FILE_DESCRIPTOR_SET)?;
let env = Env::with_standard_library()
.with_proto_registry(Arc::new(registry))
.with_variable("request", CelType::message("my.package.Request"));
Containers and abbreviations
Containers set a namespace for resolving qualified names. Abbreviations create shorthand aliases for fully-qualified type names:
let env = Env::with_standard_library()
.with_container("my.package")
.with_abbreviations(
Abbreviations::from_qualified_names(&["my.package.Request"])?
);
Error handling
CEL treats errors as values — evaluation never panics. Errors propagate through expressions following CEL's error semantics, including short-circuit behavior with logical operators:
let result = program.eval(&activation);
match &result {
Value::Error(err) => println!("evaluation error: {}", err),
value => println!("result: {}", value),
}
See the examples directory for more usage patterns including lists, maps, timestamps/durations, and error handling. For proto-related examples (enums, namespaces), see cel-core-proto/examples.
Proto Wire Format
The cel-core-proto crate provides bidirectional conversion between cel-core's AST and the cel-spec protobuf format. This enables wire compatibility with cel-go and cel-cpp — you can compile an expression in Rust and send the serialized AST to a Go or C++ service for evaluation, or vice versa.
use cel_core::{Env, CelType};
use cel_core_proto::AstToProto;
use prost::Message;
let env = Env::with_standard_library()
.with_variable("x", CelType::Int);
let ast = env.compile("x + 1")?;
// Convert to cel-spec proto format
let parsed_expr = ast.to_parsed_expr();
let checked_expr = ast.to_checked_expr()?;
// Serialize to bytes (wire-compatible with cel-go/cel-cpp)
let bytes = checked_expr.encode_to_vec();
// Deserialize from bytes received from another implementation
use cel_core_proto::CheckedExpr;
let decoded = CheckedExpr::decode(bytes.as_slice())?;
Language Server
The cel-core-lsp crate provides a Language Server Protocol implementation for CEL, built with tower-lsp. It works with any LSP-compatible editor.
- Diagnostics — Real-time parse and type checking errors as you type
- Hover — Type information and function documentation
- Semantic tokens — Accurate syntax highlighting
- Protovalidate — CEL validation support in
.protofiles
Install
cargo install --path crates/cel-core-lsp
Conformance
CEL-Core passes 100% of the official cel-spec conformance tests across all 29 test files and all three test phases (parse, check, eval), including tests that cel-go itself currently skips. The project maintains a zero-regression policy against the conformance baseline.
For details on what is tested and how to run the conformance suite, see the conformance README.
Crate Structure
| Crate | Description |
|---|---|
| cel-core | CEL environment, parser, type checker, evaluator, and standard library |
| cel-core-proto | Protobuf support: proto registry (ProstProtoRegistry), message values, AST conversion for wire compatibility with cel-go/cel-cpp |
| cel-core-lsp | Language Server Protocol implementation for IDE support |
| cel-core-conformance | Conformance testing against the official cel-spec test suite |
Development
This project uses mise for tool management and task running.
# First-time setup
mise trust && mise install
mise run conformance:setup # initialize cel-spec submodule
# Testing
mise run test # all tests (excludes conformance)
mise run conformance:test # cel-spec conformance tests
mise run conformance:report # conformance report with baseline comparison
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
References
- CEL Spec — Official specification and test suite
- cel-go — Reference implementation in Go
- CEL Language Definition — Language syntax and semantics
Dependencies
~42KB