cel 0.5.1%2B1

SDKdartflutter
Platformandroidioswindowslinuxmacosweb

This project parses and evaluates Common Expression Language (CEL) programs against some inputs.

cel-dart

pub package Unit Tests

This project parses and evaluates Common Expression Language (CEL) programs against some inputs. For example, based on the code request.auth.claims.group=='admin' and a request object as input, the library will evaluate whether the statement is true or false. CEL (see the spec) is a language used by many security projects such as Firestore and Firebase Storage. This project is a simplified port of https://github.com/google/cel-go.

Usage

import 'package:cel/cel.dart';

void main() {
  final input = "request.auth.claims.group == 'admin'";
  final e = Environment.standard();
  final ast = e.compile(input);
  final p = e.makeProgram(ast);
  print(p.evaluate({
    'request': {
      'auth': {
        'claims': {'group': 'admin'}
      }
    }
  }));
}

Prints out true.

Differences with cel-go

The main difference is that cel-go supports checking types at compilation time, whereas we throw runtime errors at evaluation time. Also we don't support the timestamps nor durations Protobufs, type conversions and the type keyword.

Features

This table is based on https://github.com/google/cel-spec/blob/master/doc/langdef.md.

CEL LiteralDescriptionSupported
nullNull Literal
true and falseBool Literal
"abc"String Literal
-13, 0xffInt Literal
12uUint Literal
12.6Double Literal
b"abc"Bytes Literals
user.id == "abc"OperatorsSee table below
StructuresDescriptionSupported
[a, b]List
{'name': 'cel', 35 : true}Map
timestampgoogle.protobuf.Timestamp
durationgoogle.protobuf.Duration

Operators

This table comes from https://firebase.google.com/docs/rules/rules-language#operators_and_operator_precedence.

OperatorDescriptionSupported
a.ffield access
a()call
a[i]Index
!a, -aUnary negation
a/b, a%b, a*bMultiplicative operators
a+b, a-bAdditive operators
a>b, a>=bRelational operators
a in bExistence in list or map
a is typeType comparison, where type can be bool, int, float, number, string, list, map, timestamp, duration, path or latlng
a==b, a!=bComparison operators
a && bConditional AND
a || bConditional OR
a ? true_value : false_valueTernary expression

Functions

From https://github.com/google/cel-spec/blob/master/doc/langdef.md#functions.

SymbolTypeDescription
!_(bool) -> boollogical not
-_(int) -> intnegation
(double) -> doublenegation
_!=_(A, A) -> boolinequality
_%_(int, int) -> intarithmetic
(uint, uint) -> uintarithmeticuntested
_&&_(bool, bool) -> boollogical and
(bool, ...) -> boollogical and (variadic)
_*_(int, int) -> intarithmetic
(uint, uint) -> uintarithmetic
(double, double) -> doublearithmetic
_+_(int, int) -> intarithmetic
(uint, uint) -> uintarithmetic
(double, double) -> doublearithmetic
(string, string) -> stringString concatenation.
(bytes, bytes) -> bytesbytes concatenation
(list(A), list(A)) -> list(A)List concatenation.
(google.protobuf.Timestamp, google.protobuf.Duration) -> google.protobuf.Timestamparithmetic
(google.protobuf.Duration, google.protobuf.Timestamp) -> google.protobuf.Timestamparithmetic
(google.protobuf.Duration, google.protobuf.Duration) -> google.protobuf.Durationarithmetic
_-_(int, int) -> intarithmetic
(uint, uint) -> uintarithmetic
(double, double) -> doublearithmetic
(google.protobuf.Timestamp, google.protobuf.Timestamp) -> google.protobuf.Durationarithmetic
(google.protobuf.Timestamp, google.protobuf.Duration) -> google.protobuf.Timestamparithmetic
(google.protobuf.Duration, google.protobuf.Duration) -> google.protobuf.Durationarithmetic
_/_(int, int) -> intarithmetic
(uint, uint) -> uintarithmetic
(double, double) -> doublearithmetic
_<=_(bool, bool) -> boolordering
(int, int) -> boolordering
(uint, uint) -> boolordering
(double, double) -> boolordering
(string, string) -> boolordering
(bytes, bytes) -> boolordering
(google.protobuf.Timestamp, google.protobuf.Timestamp) -> boolordering
(google.protobuf.Duration, google.protobuf.Duration) -> boolordering
_<_(bool, bool) -> boolordering
(int, int) -> boolordering
(uint, uint) -> boolordering
(double, double) -> boolordering
(string, string) -> boolordering
(bytes, bytes) -> boolordering
(google.protobuf.Timestamp, google.protobuf.Timestamp) -> boolordering
(google.protobuf.Duration, google.protobuf.Duration) -> boolordering
_==_(A, A) -> boolequality
_>=_(bool, bool) -> boolordering
(int, int) -> boolordering
(uint, uint) -> boolordering
(double, double) -> boolordering
(string, string) -> boolordering
(bytes, bytes) -> boolordering
(google.protobuf.Timestamp, google.protobuf.Timestamp) -> boolordering
(google.protobuf.Duration, google.protobuf.Duration) -> boolordering
_>_(bool, bool) -> boolordering
(int, int) -> boolordering
(uint, uint) -> boolordering
(double, double) -> boolordering
(string, string) -> boolordering
(bytes, bytes) -> boolordering
(google.protobuf.Timestamp, google.protobuf.Timestamp) -> boolordering
(google.protobuf.Duration, google.protobuf.Duration) -> boolordering
_?_:_(bool, A, A) -> AThe conditional operator. See above for evaluation semantics. Will evaluate the test and only one of the remaining sub-expressions.
_[_](list(A), int) -> Alist indexing.
(map(A, B), A) -> Bmap indexing.
in(A, list(A)) -> boollist membership.
(A, map(A, B)) -> boolmap key membership.
||(bool, bool) -> boollogical or
(bool, ...) -> boollogical or (variadic)
booltype(bool)type denotation
bytestype(bytes)type denotation
(string) -> bytestype conversion
containsstring.(string) -> boolTests whether the string operand contains the substring.
doubletype(double)type denotation
(int) -> doubletype conversion
(uint) -> doubletype conversion
(string) -> doubletype conversion
duration(string) -> google.protobuf.DurationType conversion. Duration strings should support the following suffixes: "h" (hour), "m" (minute), "s" (second), "ms" (millisecond), "us" (microsecond), and "ns" (nanosecond). Duration strings may be zero, negative, fractional, and/or compound. Examples: "0", "-1.5h", "1m6s"
dyntype(dyn)type denotation
(A) -> dyntype conversion
endsWithstring.(string) -> boolTests whether the string operand ends with the suffix argument.
getDategoogle.protobuf.Timestamp.() -> intget day of month from the date in UTC, one-based indexing
google.protobuf.Timestamp.(string) -> intget day of month from the date with timezone, one-based indexing
getDayOfMonthgoogle.protobuf.Timestamp.() -> intget day of month from the date in UTC, zero-based indexing
google.protobuf.Timestamp.(string) -> intget day of month from the date with timezone, zero-based indexing
getDayOfWeekgoogle.protobuf.Timestamp.() -> intget day of week from the date in UTC, zero-based, zero for Sunday
google.protobuf.Timestamp.(string) -> intget day of week from the date with timezone, zero-based, zero for Sunday
getDayOfYeargoogle.protobuf.Timestamp.() -> intget day of year from the date in UTC, zero-based indexing
google.protobuf.Timestamp.(string) -> intget day of year from the date with timezone, zero-based indexing
getFullYeargoogle.protobuf.Timestamp.() -> intget year from the date in UTC
google.protobuf.Timestamp.(string) -> intget year from the date with timezone
getHoursgoogle.protobuf.Timestamp.() -> intget hours from the date in UTC, 0-23
google.protobuf.Timestamp.(string) -> intget hours from the date with timezone, 0-23
google.protobuf.Duration.() -> intget hours from duration
getMillisecondsgoogle.protobuf.Timestamp.() -> intget milliseconds from the date in UTC, 0-999
google.protobuf.Timestamp.(string) -> intget milliseconds from the date with timezone, 0-999
google.protobuf.Duration.() -> intmilliseconds from duration, 0-999
getMinutesgoogle.protobuf.Timestamp.() -> intget minutes from the date in UTC, 0-59
google.protobuf.Timestamp.(string) -> intget minutes from the date with timezone, 0-59
google.protobuf.Duration.() -> intget minutes from duration
getMonthgoogle.protobuf.Timestamp.() -> intget month from the date in UTC, 0-11
google.protobuf.Timestamp.(string) -> intget month from the date with timezone, 0-11
getSecondsgoogle.protobuf.Timestamp.() -> intget seconds from the date in UTC, 0-59
google.protobuf.Timestamp.(string) -> intget seconds from the date with timezone, 0-59
google.protobuf.Duration.() -> intget seconds from duration
inttype(int)type denotation
(uint) -> inttype conversion
(double) -> intType conversion. Rounds toward zero, then errors if result is out of range.
(string) -> inttype conversion
(enum E) -> inttype conversion
(google.protobuf.Timestamp) -> intConvert timestamp to int64 in seconds since Unix epoch.
listtype(list(dyn))type denotation
maptype(map(dyn, dyn))type denotation
matches(string, string) -> boolMatches first argument against regular expression in second argument.
string.(string) -> boolMatches the self argument against regular expression in first argument.
null_typetype(null)type denotation
size(string) -> intstring length
(bytes) -> intbytes length
(list(A)) -> intlist size.
(map(A, B)) -> intmap size.
startsWithstring.(string) -> boolTests whether the string operand starts with the prefix argument.
stringtype(string)type denotation
(int) -> stringtype conversion
(uint) -> stringtype conversion
(double) -> stringtype conversion
(bytes) -> stringtype conversion
(timestamp) -> stringtype conversion, using the same format as timestamp string parsing
(duration) -> stringtype conversion, using the same format as duration string parsing
timestamp(string) -> google.protobuf.TimestampType conversion of strings to timestamps according to RFC3339. Example: "1972-01-01T10:00:20.021-05:00"
typetype(dyn)type denotation
(A) -> type(dyn)returns type of value
uinttype(uint)type denotation
(int) -> uinttype conversion
(double) -> uintType conversion. Rounds toward zero, then errors if result is out of range.
(string) -> uinttype conversion
E (for fully-qualified enumeration E)(int) -> enum Etype conversion when in int32 range, otherwise error
(string) -> enum Etype conversion for unqualified symbolic name, otherwise error

Additional information

If you are curious how it was made, or want to contribute, you may find this reading list useful:

Architecture

Here's the mechanism from CEL code (a String) to evaluation:

  1. The user instantiates an [Environment]. In cel-go, they can pass some environment variables. We have skipped porting this so far.
  2. The user calls [Environment.compile] with CEL code (a String), and gets back an Abstract Syntax Tree (AST).
    1. Under the hood, [Environment.compile] relies on [Parser], which itself uses [CELParser], an ANTLR generated Parser for CEL.
    2. [CELParser] converts the CEL code into a CEL tree (a [StartContext]).
    3. Then Parser traverses the CEL tree into an [Expr], which is the actual AST.
    4. Finally Environment wraps the [Expr] into an [Ast].
  3. The user instantiates a [Program] by passing the Environment and the AST. Upon initialization, the Program calls [Planner.plan], which traverses the AST and converts it into an [Interpretable] for later use.
  4. Whenever the user wants to evaluate the Program, they call [Program.evaluate] with some inputs (eg a [Map]), and get a value as a result. It evaluates the Interpretable using the inputs into a return value.

The meat of the code is in [Parser.visit] and [Planner.plan].

Implementation details

  • Difference between [Value.value] and [Value.convertToNative]: While both are the same in the case of primitive wrappers such as [IntValue], [DoubleValue]... they are different for [ListValue] and [MapValue]. For example for a [ListValue], [ListValue.value] is a List<Value>, while [Value.convertToNative] will return List<non-Value type>.
  • environmentOptions and standardDeclarations don't actually do anything yet. In the future, they may be used to check whether some function has indeed been declared in Interpretable.planCall when it calls resolveFunction. Doing so might help throw an Exception early if the function name is not an declared function.
  • In cel-go, Parser.visit returns any. In cel-dart, we return Expr, making it more type safe.
  • How does a in b get processed? in is listed in standardOverloads. It is used in StdLibrary to add them to the Dispatcher during initialization. During evaluation, the planner finds the Overload implementation by calling Dispatcher.findOverload. Eventually, the CallExpr('@in') calls the [Overload] implementation with the call to contains.
  • In cel-go defines the Expr architecture with Protobuf, while this project defines Expr as native Dart. This is mostly to save time by avoiding a lot of boilerplate code. We might integrate Protobuf later if the need arises.