@libdbm/libcel-ts 1.0.2-rc.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +342 -31
- package/dist/index.cjs +12 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1072 -118
- package/dist/index.mjs +2723 -681
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,11 +9,14 @@ CEL is a non-Turing complete expression language designed for simplicity, speed,
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Complete CEL Implementation**: All CEL operators, functions, and macros
|
|
12
|
+
- **Extension Libraries**: strings, lists, sets, math, regex and base64 functions using the cel-go names
|
|
13
|
+
- **Round-trip Printing**: render a parsed expression back to CEL source, compact or pretty printed
|
|
14
|
+
- **Exact 64-bit Integers**: CEL ints are `bigint`, with overflow detection and exact mixed int/double comparison
|
|
12
15
|
- **Type Safe**: Leverages TypeScript's type system with strict typing
|
|
13
16
|
- **High Performance**: Hand-written recursive descent parser with AST compilation
|
|
14
17
|
- **Extensible**: Easy to add custom functions
|
|
15
|
-
- **Well Tested**:
|
|
16
|
-
- **Zero External Dependencies**: Pure TypeScript implementation
|
|
18
|
+
- **Well Tested**: 275 comprehensive tests, including a specification conformance suite
|
|
19
|
+
- **Zero External Dependencies**: Pure TypeScript implementation, runs in Node and browsers
|
|
17
20
|
- **Vite-Powered**: Modern tooling with fast builds and excellent DX
|
|
18
21
|
|
|
19
22
|
## Installation
|
|
@@ -35,8 +38,8 @@ import { CEL } from '@libdbm/libcel-ts';
|
|
|
35
38
|
|
|
36
39
|
const cel = new CEL();
|
|
37
40
|
|
|
38
|
-
// Simple expression evaluation
|
|
39
|
-
console.log(cel.eval('2 + 3 * 4', {})); //
|
|
41
|
+
// Simple expression evaluation; CEL ints are JavaScript bigints
|
|
42
|
+
console.log(cel.eval('2 + 3 * 4', {})); // 14n
|
|
40
43
|
|
|
41
44
|
// Using variables
|
|
42
45
|
const vars = { name: 'Alice', age: 30 };
|
|
@@ -56,8 +59,8 @@ const cel = new CEL();
|
|
|
56
59
|
const program = cel.compile('price * quantity * (1 - discount)');
|
|
57
60
|
|
|
58
61
|
// Reuse with different variables
|
|
59
|
-
const result1 = program.evaluate({ price: 10, quantity: 5, discount: 0.1 });
|
|
60
|
-
const result2 = program.evaluate({ price: 20, quantity: 3, discount: 0.2 });
|
|
62
|
+
const result1 = program.evaluate({ price: 10, quantity: 5, discount: 0.1 }); // 45
|
|
63
|
+
const result2 = program.evaluate({ price: 20, quantity: 3, discount: 0.2 }); // 48
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
### Working with Complex Data
|
|
@@ -96,9 +99,11 @@ Extend the standard library with custom functions:
|
|
|
96
99
|
import { CEL, StandardFunctions } from '@libdbm/libcel-ts';
|
|
97
100
|
|
|
98
101
|
class CustomFunctions extends StandardFunctions {
|
|
99
|
-
callFunction(name: string, args:
|
|
102
|
+
override callFunction(name: string, args: unknown[]): unknown {
|
|
100
103
|
if (name === 'reverse') {
|
|
101
|
-
return (args[0] as string)
|
|
104
|
+
return Array.from(args[0] as string)
|
|
105
|
+
.reverse()
|
|
106
|
+
.join('');
|
|
102
107
|
}
|
|
103
108
|
return super.callFunction(name, args);
|
|
104
109
|
}
|
|
@@ -108,6 +113,37 @@ const cel = new CEL(new CustomFunctions());
|
|
|
108
113
|
console.log(cel.eval("reverse('hello')", {})); // "olleh"
|
|
109
114
|
```
|
|
110
115
|
|
|
116
|
+
Results returned by custom functions are converted into the CEL value model: plain objects
|
|
117
|
+
become maps and `Date` becomes a timestamp. A JavaScript `number` returned by a function is a
|
|
118
|
+
CEL double; return a `bigint` to produce a CEL int.
|
|
119
|
+
|
|
120
|
+
## Value Model
|
|
121
|
+
|
|
122
|
+
CEL values are represented by JavaScript values as follows.
|
|
123
|
+
|
|
124
|
+
| CEL type | Result type | Accepted as a variable |
|
|
125
|
+
| ----------- | ------------------------------ | ------------------------------------------------------ |
|
|
126
|
+
| `int` | `bigint` | `bigint`, or a `number` with an integer value |
|
|
127
|
+
| `uint` | `bigint` (not a distinct type) | as `int` |
|
|
128
|
+
| `double` | `number` | a `number` with a fractional part, `NaN` or infinities |
|
|
129
|
+
| `bool` | `boolean` | `boolean` |
|
|
130
|
+
| `string` | `string` | `string` |
|
|
131
|
+
| `bytes` | `Uint8Array` | `Uint8Array` |
|
|
132
|
+
| `list` | `Array` | `Array` |
|
|
133
|
+
| `map` | `Map` | `Map`, or a plain object (string keys) |
|
|
134
|
+
| `null` | `null` | `null` or `undefined` |
|
|
135
|
+
| `timestamp` | `Timestamp` | `Timestamp` or `Date` |
|
|
136
|
+
| `duration` | `Duration` | `Duration` |
|
|
137
|
+
| `type` | `Type` | `Type` |
|
|
138
|
+
|
|
139
|
+
Variables are normalised when evaluation starts: an integer-valued `number` such as `30` is a
|
|
140
|
+
CEL int, so `age / 7` truncates. Write `double(age)` in the expression when a double is wanted,
|
|
141
|
+
or pass a value with a fractional part. Map literals evaluate to `Map`, and maps may use keys
|
|
142
|
+
of any type; a key stored as `1` matches a probe of `1.0`.
|
|
143
|
+
|
|
144
|
+
`bigint` results cannot be passed to `JSON.stringify` directly; supply a replacer such as
|
|
145
|
+
`(_, v) => (typeof v === 'bigint' ? Number(v) : v)`.
|
|
146
|
+
|
|
111
147
|
## Supported Features
|
|
112
148
|
|
|
113
149
|
### Literals
|
|
@@ -118,35 +154,142 @@ console.log(cel.eval("reverse('hello')", {})); // "olleh"
|
|
|
118
154
|
- Unsigned: `42u`, `0xFFu`
|
|
119
155
|
- Doubles: `3.14`, `6.022e23`
|
|
120
156
|
- Strings: `"hello"`, `'world'`, `r"raw\nstring"`, `"""multi-line"""`
|
|
121
|
-
- Bytes: `b"data"`
|
|
157
|
+
- Bytes: `b"data"`, `b"\x00\xff"`
|
|
122
158
|
- Lists: `[1, 2, 3]`
|
|
123
159
|
- Maps: `{"key": "value"}`
|
|
124
160
|
|
|
125
161
|
### Operators
|
|
126
162
|
|
|
127
163
|
- **Arithmetic**: `+`, `-`, `*`, `/`, `%`
|
|
128
|
-
- **Comparison**: `<`, `<=`, `>`, `>=`, `==`,
|
|
164
|
+
- **Comparison**: `<`, `<=`, `>`, `>=`, `==`, `!=`, compared exactly across int and double, so an
|
|
165
|
+
int beyond the range a double can represent is not mistaken for the double it would round to
|
|
129
166
|
- **Logical**: `&&`, `||`, `!`
|
|
130
167
|
- **Conditional**: `condition ? trueValue : falseValue`
|
|
131
168
|
- **Membership**: `in` (for lists, maps, strings)
|
|
132
169
|
|
|
133
170
|
### Functions
|
|
134
171
|
|
|
135
|
-
- **Type conversions**: `int()`, `double()`, `string()`, `bool()`
|
|
136
|
-
- **Type checking**: `type()
|
|
172
|
+
- **Type conversions**: `int()`, `uint()`, `double()`, `string()`, `bool()`, `bytes()`, `dyn()`
|
|
173
|
+
- **Type checking**: `type()`, which returns a type value comparable against the bare type names
|
|
174
|
+
`null_type`, `bool`, `int`, `uint`, `double`, `string`, `bytes`, `list`, `map`, `timestamp`,
|
|
175
|
+
`duration` and `type`
|
|
137
176
|
- **Collections**: `size()`, `has()`
|
|
138
|
-
- **String methods**: `contains()`, `startsWith()`, `endsWith()`, `toLowerCase()`, `toUpperCase()`, `trim()`, `replace()`, `split()`
|
|
139
|
-
- **Regex**: `matches()`
|
|
140
177
|
- **Math**: `max()`, `min()`
|
|
178
|
+
- **Regex**: `matches()`, as a function and as a string method
|
|
179
|
+
|
|
180
|
+
### String functions
|
|
181
|
+
|
|
182
|
+
Receiver style, following the cel-go strings extension:
|
|
183
|
+
|
|
184
|
+
`charAt()`, `indexOf()`, `lastIndexOf()`, `lowerAscii()`, `upperAscii()`, `substring()`, `reverse()`,
|
|
185
|
+
`trim()`, `contains()`, `startsWith()`, `endsWith()`, `replace(from, to[, limit])`,
|
|
186
|
+
`split(separator[, limit])`, `join([separator])` on a list of strings, `format(list)`, and the global
|
|
187
|
+
`strings.quote()`.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
cel.eval("'hello'.charAt(1)", {}); // "e"
|
|
191
|
+
cel.eval("'a,b,c'.split(',', 2)", {}); // ["a", "b,c"]
|
|
192
|
+
cel.eval("['a','b'].join('-')", {}); // "a-b"
|
|
193
|
+
cel.eval("'%d apples at %.2f'.format([3, 1.5])", {}); // "3 apples at 1.50"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`format()` supports the verbs `%s %d %f %e %b %o %x %X %%` with an optional precision such as `%.3f`.
|
|
197
|
+
|
|
198
|
+
Positions and sizes count Unicode code points, not UTF-16 code units, so `charAt()`, `substring()`,
|
|
199
|
+
`indexOf()`, `lastIndexOf()`, string indexing and `size()` never split a supplementary character:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
cel.eval("size('\\U0001F600b')", {}); // 2n
|
|
203
|
+
cel.eval("'\\U0001F600b'.charAt(0)", {}); // the whole emoji, not a lone surrogate
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Regex functions
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
cel.eval("regex.replace('a b c', ' ', '-')", {}); // "a-b-c"
|
|
210
|
+
cel.eval("regex.replace('ab', '(\\\\w)', '[\\\\1]')", {}); // "[a][b]"
|
|
211
|
+
cel.eval("regex.extract('id=123', '=([0-9]+)')", {}); // "123"
|
|
212
|
+
cel.eval("regex.extractAll('a1b2', '[0-9]')", {}); // ["1", "2"]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`regex.replace()` takes an optional fourth argument limiting the number of replacements. Capture
|
|
216
|
+
groups are referenced as `\1` through `\9`, and `\0` for the whole match. Patterns use the
|
|
217
|
+
JavaScript `RegExp` dialect.
|
|
218
|
+
|
|
219
|
+
### List and set functions
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
cel.eval('[1, 2, 2, 3].distinct()', {}); // [1n, 2n, 3n]
|
|
223
|
+
cel.eval('[[1, 2], [3]].flatten()', {}); // [1n, 2n, 3n]
|
|
224
|
+
cel.eval('[3, 1, 2].sort()', {}); // [1n, 2n, 3n]
|
|
225
|
+
cel.eval('[1, 2, 3, 4].slice(1, 3)', {}); // [2n, 3n]
|
|
226
|
+
cel.eval('[1, 2, 3].reverse()', {}); // [3n, 2n, 1n]
|
|
227
|
+
cel.eval('[1, 2].first()', {}); // 1n
|
|
228
|
+
cel.eval('lists.range(3)', {}); // [0n, 1n, 2n]
|
|
229
|
+
cel.eval('sets.contains([1, 2, 3], [1, 3])', {}); // true
|
|
230
|
+
cel.eval('sets.equivalent([1, 2, 2], [2, 1])', {}); // true
|
|
231
|
+
cel.eval('sets.intersects([1, 2], [2, 3])', {}); // true
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Math functions
|
|
235
|
+
|
|
236
|
+
`math.greatest()`, `math.least()`, `math.abs()`, `math.ceil()`, `math.floor()`, `math.round()`,
|
|
237
|
+
`math.trunc()`, `math.sign()`, `math.sqrt()`, `math.isNaN()`, `math.isInf()`, `math.isFinite()`,
|
|
238
|
+
`math.bitAnd()`, `math.bitOr()`, `math.bitXor()`, `math.bitNot()`, `math.bitShiftLeft()`,
|
|
239
|
+
`math.bitShiftRight()`.
|
|
240
|
+
|
|
241
|
+
`abs()` and `sign()` preserve the numeric type of their argument; `math.greatest()` and
|
|
242
|
+
`math.least()` also accept a single list. Bit operations work on the full signed 64-bit range.
|
|
243
|
+
|
|
244
|
+
### Bytes and base64
|
|
245
|
+
|
|
246
|
+
Bytes literals evaluate to a `Uint8Array`. They support `size()`, indexing, concatenation,
|
|
247
|
+
comparison and `string()` conversion (decoded as UTF-8).
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
cel.eval("base64.encode(b'hello')", {}); // "aGVsbG8="
|
|
251
|
+
cel.eval("string(base64.decode('aGVsbG8='))", {}); // "hello"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Timestamps and durations
|
|
255
|
+
|
|
256
|
+
`timestamp()` produces a `Timestamp` and `duration()` a `Duration`; both are exported classes with
|
|
257
|
+
nanosecond precision (`seconds: bigint`, `nanos: number`). A JavaScript `Date` passed as a variable
|
|
258
|
+
is accepted as a timestamp. Durations accept the full CEL syntax: signed, multi-unit and
|
|
259
|
+
fractional values with the units `ns`, `us`, `ms`, `s`, `m` and `h`, for example `1h30m`, `-1.5s`
|
|
260
|
+
or `100ms`. Components are accumulated exactly, and a duration beyond the specification's range
|
|
261
|
+
of `Utilities.SPAN` seconds either side of zero is rejected rather than silently clamped.
|
|
262
|
+
|
|
263
|
+
Accessors are available both as global functions and as methods, and default to UTC. Each takes an
|
|
264
|
+
optional IANA time zone as its last argument (resolved through `Intl.DateTimeFormat`).
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
cel.eval("timestamp('2024-03-05T14:30:45Z').getFullYear()", {}); // 2024n
|
|
268
|
+
cel.eval("getHours(timestamp('2024-03-05T14:30:45Z'), 'America/New_York')", {}); // 9n
|
|
269
|
+
cel.eval("duration('1h30m').getMinutes()", {}); // 90n
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The accessors are `getFullYear`, `getMonth` (zero based), `getDate`, `getDayOfWeek` (Sunday is 0),
|
|
273
|
+
`getDayOfYear` (zero based), `getHours`, `getMinutes`, `getSeconds` and `getMilliseconds`.
|
|
274
|
+
|
|
275
|
+
Timestamps and durations support arithmetic and comparison: `ts + dur`, `dur + ts`, `ts - dur`,
|
|
276
|
+
`ts - ts` (yielding a duration), `dur + dur`, `dur - dur` and `-dur`.
|
|
277
|
+
|
|
278
|
+
### Namespaced functions
|
|
279
|
+
|
|
280
|
+
`math`, `sets`, `lists`, `strings`, `base64` and `regex` are namespaces, not variables. A variable
|
|
281
|
+
of the same name always wins, so binding `math` in the evaluation variables makes `math.greatest`
|
|
282
|
+
resolve as an ordinary field selection again. Custom function libraries expose namespaced
|
|
283
|
+
functions by implementing `knows(name)`.
|
|
141
284
|
|
|
142
285
|
### Macro Functions
|
|
143
286
|
|
|
144
287
|
```typescript
|
|
145
288
|
// map - Transform each element
|
|
146
|
-
cel.eval('[1, 2, 3].map(x, x * 2)', {}); // [
|
|
289
|
+
cel.eval('[1, 2, 3].map(x, x * 2)', {}); // [2n, 4n, 6n]
|
|
147
290
|
|
|
148
291
|
// filter - Keep elements matching condition
|
|
149
|
-
cel.eval('[1, 2, 3, 4].filter(x, x % 2 == 0)', {}); // [
|
|
292
|
+
cel.eval('[1, 2, 3, 4].filter(x, x % 2 == 0)', {}); // [2n, 4n]
|
|
150
293
|
|
|
151
294
|
// exists - Check if any element matches
|
|
152
295
|
cel.eval('[1, 2, 3].exists(x, x > 2)', {}); // true
|
|
@@ -156,8 +299,46 @@ cel.eval('[1, 2, 3].all(x, x > 0)', {}); // true
|
|
|
156
299
|
|
|
157
300
|
// existsOne - Check if exactly one element matches
|
|
158
301
|
cel.eval('[1, 2, 3].existsOne(x, x == 2)', {}); // true
|
|
302
|
+
|
|
303
|
+
// has - Test field presence without erroring
|
|
304
|
+
cel.eval('has(user.email)', { user: { name: 'Alice' } }); // false
|
|
159
305
|
```
|
|
160
306
|
|
|
307
|
+
`sortBy` orders a list by a computed key, and `map` accepts an optional predicate between the
|
|
308
|
+
variable and the transform:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
cel.eval('users.sortBy(u, u.age).map(u, u.name)', data);
|
|
312
|
+
cel.eval('[1, 2, 3, 4].map(x, x % 2 == 0, x * 2)', {}); // [4n, 8n]
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Macros also accept a map as their target, iterating over its keys. Macro predicates must yield a
|
|
316
|
+
boolean.
|
|
317
|
+
|
|
318
|
+
## Printing expressions
|
|
319
|
+
|
|
320
|
+
`Printer` renders a parsed expression back into CEL source. The compact form is a single line that
|
|
321
|
+
parses back into an equivalent expression, with parentheses only where precedence requires them.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import { Parser, Printer, PrinterOptions } from '@libdbm/libcel-ts';
|
|
325
|
+
|
|
326
|
+
const expr = new Parser('(a + b) * c').parse();
|
|
327
|
+
Printer.print(expr); // "(a + b) * c"
|
|
328
|
+
Printer.print(new Parser('a + (b * c)').parse()); // "a + b * c"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The pretty form breaks lists, maps, structs, argument lists and logical chains once they no longer
|
|
332
|
+
fit the configured width:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
Printer.print(expr, PrinterOptions.pretty()); // 2 space indent, 100 column width
|
|
336
|
+
Printer.print(expr, new PrinterOptions(true, 4, 60));
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Original spacing and redundant parentheses are not preserved, and comprehension nodes, which the
|
|
340
|
+
parser never produces, are rendered in a diagnostic form that is not valid CEL.
|
|
341
|
+
|
|
161
342
|
## Building
|
|
162
343
|
|
|
163
344
|
```bash
|
|
@@ -173,7 +354,7 @@ npm run test:coverage
|
|
|
173
354
|
# Build the library
|
|
174
355
|
npm run build
|
|
175
356
|
|
|
176
|
-
# Type check
|
|
357
|
+
# Type check (sources and tests)
|
|
177
358
|
npm run typecheck
|
|
178
359
|
|
|
179
360
|
# Lint and format
|
|
@@ -185,9 +366,13 @@ npm run format
|
|
|
185
366
|
|
|
186
367
|
The project includes comprehensive test coverage:
|
|
187
368
|
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
369
|
+
- 37 parser tests
|
|
370
|
+
- 26 interpreter tests
|
|
371
|
+
- 60 integration tests
|
|
372
|
+
- 15 printer tests
|
|
373
|
+
- 42 extension function tests
|
|
374
|
+
- 69 conformance tests organized after the google/cel-spec corpus
|
|
375
|
+
- 26 value model tests
|
|
191
376
|
- All tests from the Java implementation ported and passing
|
|
192
377
|
|
|
193
378
|
```bash
|
|
@@ -203,28 +388,129 @@ npm run test:coverage
|
|
|
203
388
|
|
|
204
389
|
## Architecture
|
|
205
390
|
|
|
206
|
-
- **expression.ts**: Abstract Syntax Tree (AST) with visitor pattern
|
|
207
|
-
- **parser.ts**: Hand-written recursive descent parser
|
|
208
|
-
- **interpreter.ts**: AST evaluator using Visitor pattern
|
|
209
|
-
- **
|
|
391
|
+
- **ast/expression.ts**: Abstract Syntax Tree (AST) with visitor pattern
|
|
392
|
+
- **parser/lexer.ts, parser/parser.ts**: Hand-written lexer and recursive descent parser
|
|
393
|
+
- **interpreter/interpreter.ts**: AST evaluator using Visitor pattern
|
|
394
|
+
- **printer/printer.ts**: AST to CEL source renderer, also using Visitor pattern
|
|
395
|
+
- **values/**: The CEL value model: `Type`, `Timestamp`, `Duration`, bytes helpers, exact numeric
|
|
396
|
+
ordering, canonical keys for hashing, and boundary normalisation
|
|
397
|
+
- **functions/functions.ts**: Extensible function library interface
|
|
398
|
+
- **functions/standard-functions.ts**: Built-in and extension function dispatch
|
|
399
|
+
- **functions/strings.ts, lists.ts, sets.ts, maths.ts, regexes.ts, codecs.ts**: Extension
|
|
400
|
+
function implementations
|
|
401
|
+
- **functions/utilities.ts**: Conversions, equality, ordering and time helpers
|
|
402
|
+
- **errors.ts**: `EvaluationError` and `ArgumentError`
|
|
210
403
|
- **cel.ts**: Main API entry point
|
|
211
404
|
- **program.ts**: Compiled, reusable programs
|
|
212
405
|
|
|
213
406
|
## Functional Equivalence
|
|
214
407
|
|
|
215
|
-
This TypeScript implementation is functionally equivalent to the [Java libcel](https://github.com/libdbm/libcel-java) implementation:
|
|
408
|
+
This TypeScript implementation is functionally equivalent to the [Java libcel](https://github.com/libdbm/libcel-java) implementation (version 2.0.0):
|
|
216
409
|
|
|
217
410
|
- Same AST structure and expression types
|
|
218
411
|
- Identical parsing rules and operator precedence
|
|
219
|
-
- Same evaluation semantics
|
|
412
|
+
- Same evaluation semantics, including exact 64-bit integer arithmetic
|
|
220
413
|
- Equivalent macro function behavior
|
|
221
414
|
- Compatible error handling
|
|
222
415
|
|
|
223
416
|
All tests from the Java version have been ported to ensure equivalence.
|
|
224
417
|
|
|
418
|
+
## Specification compliance
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
cel.eval('1 / 2', {}); // 0n, integer division truncates
|
|
422
|
+
cel.eval('1.0 / 0.0', {}); // Infinity, not an error
|
|
423
|
+
cel.eval('has(a.b)', data); // presence test, false rather than an error
|
|
424
|
+
cel.eval('type(42) == int', {}); // true
|
|
425
|
+
cel.eval('false && missing', {}); // false, errors are absorbed by a false operand
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Known deviations from the specification, kept deliberately:
|
|
429
|
+
|
|
430
|
+
- `uint` is represented as a signed 64-bit int, so `type(1u)` is `int` and `1u == 1` is true.
|
|
431
|
+
- `"ab" * 3` and `[1] * 2` repeat a string or list, and `"a" in "abc"` tests for a substring. CEL
|
|
432
|
+
defines none of these; they are extensions this library adds.
|
|
433
|
+
- `has(map, key)` is also accepted as a two-argument function alongside the `has(a.b)` macro.
|
|
434
|
+
- Map keys are not restricted to the specification's int, uint, bool and string types, and they are
|
|
435
|
+
matched with the same numeric equality the rest of the language uses. A key stored as `1`
|
|
436
|
+
matches a `1.0` probe, and `{1: "a"}[1.0]` resolves. Membership, indexing, presence tests and
|
|
437
|
+
map equality all follow this one rule, so `key in map ? map[key] : fallback` never fails after
|
|
438
|
+
its guard succeeds, and a `Map` bound from JavaScript compares equal to the literal that
|
|
439
|
+
spells it.
|
|
440
|
+
- Bytes are ordered the way the Java implementation orders them, byte by byte as signed values.
|
|
441
|
+
|
|
442
|
+
### Evaluation limits
|
|
443
|
+
|
|
444
|
+
CEL exists to evaluate untrusted input, so operations whose size the expression itself controls are
|
|
445
|
+
bounded by `Utilities.LIMIT`, one million elements or characters. `lists.range(n)`, the repetition
|
|
446
|
+
operators and `format()` raise an `EvaluationError` rather than exhausting the heap:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
cel.eval('lists.range(9223372036854775807)', {}); // EvaluationError
|
|
450
|
+
cel.eval("'x' * 2000000000", {}); // EvaluationError
|
|
451
|
+
cel.eval("'%.2000000f'.format([1.5])", {}); // EvaluationError
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The ceiling bounds each operation's **output**, not only its inputs, so two separately legal values
|
|
455
|
+
cannot be combined into one that exhausts the heap. `join()`, `replace()`, `regex.replace()`,
|
|
456
|
+
`base64.encode()` and string, list and bytes concatenation are all bounded this way:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
cel.eval("lists.range(1000000).map(x, 'a').join('x' * 1000000)", {}); // EvaluationError
|
|
460
|
+
cel.eval("('x' * 1000000) + ('x' * 1000000)", {}); // EvaluationError
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Collection operations resolve membership through a hash index rather than a scan, so `distinct()`
|
|
464
|
+
and the `sets` functions stay linear in their input and cannot burn CPU quadratically at the size
|
|
465
|
+
the limit allows. The parser also refuses expressions nested more than 256 levels deep.
|
|
466
|
+
|
|
467
|
+
This is a ceiling on individual operations, not a budget for a whole expression: a deeply nested
|
|
468
|
+
expression can still allocate a multiple of the limit.
|
|
469
|
+
|
|
470
|
+
## Breaking changes in 2.0.0
|
|
471
|
+
|
|
472
|
+
- CEL ints are now `bigint`: `cel.eval('1 + 2')` returns `3n`, and every int result (including
|
|
473
|
+
`size()`, indexes and `int()`) is a `bigint`. Integer-valued JavaScript numbers passed as
|
|
474
|
+
variables are ints; write `double(x)` in the expression to force a double. `JSON.stringify`
|
|
475
|
+
needs a replacer for `bigint`.
|
|
476
|
+
- Map literals and plain-object variables evaluate to `Map` rather than plain objects, and maps may
|
|
477
|
+
have non-string keys.
|
|
478
|
+
- Bytes literals now evaluate to `Uint8Array` instead of `string`. Use `string(value)` to decode
|
|
479
|
+
them as UTF-8, or `bytes(value)` to convert the other way.
|
|
480
|
+
- `type()` returns a `Type` value rather than a string, so `type(42) == int` is true and
|
|
481
|
+
`type(42) == "int"` is false. Use `string(type(x))` for the old string form. `type(1.0)` is
|
|
482
|
+
now `double`; previously any integer-valued number reported `int`.
|
|
483
|
+
- Integer division returns an integer: `1 / 2` is `0n`, not `0.5`. Use a double operand for the
|
|
484
|
+
old behavior. Double division by zero yields infinity instead of raising an error, and `%` now
|
|
485
|
+
requires integer operands.
|
|
486
|
+
- Integer arithmetic raises `EvaluationError` on 64-bit overflow instead of losing precision.
|
|
487
|
+
- Mixed int and double comparison is exact, so `9223372036854775807 == 9223372036854775808.0` is
|
|
488
|
+
false.
|
|
489
|
+
- `&&`, `||`, `!` and `? :` now require boolean operands instead of treating any non-true value as
|
|
490
|
+
false. The logical operators absorb errors: `false && error` is false and `true || error` is
|
|
491
|
+
true. Macro predicates must likewise yield a boolean.
|
|
492
|
+
- `has(a.b)` is a presence test resolved at parse time; `has(1)` is a `ParseError`. The two-argument
|
|
493
|
+
`has(map, key)` function is still supported.
|
|
494
|
+
- Timestamps and durations are new: `timestamp(n)` reads `n` as epoch **seconds**, results are
|
|
495
|
+
`Timestamp` and `Duration` instances (a `Date` is accepted as input), accessors default to UTC
|
|
496
|
+
and return `bigint`, and `string(duration)` renders the CEL form (`"3600s"`).
|
|
497
|
+
- String ordering compares UTF-16 code units (previously locale-aware `localeCompare`), and string
|
|
498
|
+
positions and sizes count code points rather than UTF-16 code units.
|
|
499
|
+
- `int('4.5')` and `double('abc')` are errors; the conversions no longer accept partial numbers.
|
|
500
|
+
- `null` and `undefined` variables both evaluate to CEL `null`.
|
|
501
|
+
- Function argument errors throw the new `ArgumentError` rather than a plain `Error`;
|
|
502
|
+
`EvaluationError` is unchanged. Both are exported.
|
|
503
|
+
- `Utilities` signatures changed: `typeOf` returns `Type`, `sizeOf`, `asInt`, `asUInt` and the
|
|
504
|
+
time accessors return `bigint`, `deepEquals` is an alias of `equals`, and `compare` no longer
|
|
505
|
+
accepts `null`.
|
|
506
|
+
- The `Functions` interface gained an optional `knows(name)` method for namespaced functions, and
|
|
507
|
+
results returned from custom functions are normalised into the CEL value model.
|
|
508
|
+
- An integer literal outside the 64-bit range is a `ParseError`.
|
|
509
|
+
|
|
225
510
|
## Requirements
|
|
226
511
|
|
|
227
|
-
- Node.js 18+ or modern browser
|
|
512
|
+
- Node.js 18+ or a modern browser (BigInt, `TextEncoder` and `Intl.DateTimeFormat` with time
|
|
513
|
+
zone support)
|
|
228
514
|
- TypeScript 5.0+ (for development)
|
|
229
515
|
|
|
230
516
|
## API Documentation
|
|
@@ -235,10 +521,14 @@ All tests from the Java version have been ported to ensure equivalence.
|
|
|
235
521
|
class CEL {
|
|
236
522
|
constructor(functions?: Functions | null);
|
|
237
523
|
compile(expression: string): Program;
|
|
238
|
-
eval(expression: string, variables?: Record<string,
|
|
524
|
+
eval(expression: string, variables?: Record<string, unknown> | Map<string, unknown>): unknown;
|
|
239
525
|
|
|
240
526
|
static compile(expression: string, functions: Functions): Program;
|
|
241
|
-
static eval(
|
|
527
|
+
static eval(
|
|
528
|
+
expression: string,
|
|
529
|
+
functions: Functions,
|
|
530
|
+
variables: Record<string, unknown> | Map<string, unknown>
|
|
531
|
+
): unknown;
|
|
242
532
|
}
|
|
243
533
|
```
|
|
244
534
|
|
|
@@ -246,7 +536,7 @@ class CEL {
|
|
|
246
536
|
|
|
247
537
|
```typescript
|
|
248
538
|
class Program {
|
|
249
|
-
evaluate(variables?: Record<string,
|
|
539
|
+
evaluate(variables?: Record<string, unknown> | Map<string, unknown>): unknown;
|
|
250
540
|
}
|
|
251
541
|
```
|
|
252
542
|
|
|
@@ -256,15 +546,36 @@ class Program {
|
|
|
256
546
|
interface Functions {
|
|
257
547
|
callFunction(name: string, args: any[]): any;
|
|
258
548
|
callMethod(target: any, method: string, args: any[]): any;
|
|
549
|
+
knows?(name: string): boolean;
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Printer
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
class Printer {
|
|
557
|
+
static print(expr: Expression, options?: PrinterOptions): string;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
class PrinterOptions {
|
|
561
|
+
constructor(wrap: boolean, indent: number, width: number);
|
|
562
|
+
static compact(): PrinterOptions;
|
|
563
|
+
static pretty(): PrinterOptions;
|
|
259
564
|
}
|
|
260
565
|
```
|
|
261
566
|
|
|
567
|
+
### Values
|
|
568
|
+
|
|
569
|
+
`Type`, `Timestamp`, `Duration`, `EvaluationError` and `ArgumentError` are exported, as are the
|
|
570
|
+
`Utilities`, `Strings`, `Lists`, `Sets`, `Maths`, `Regexes` and `Codecs` namespaces for calling the
|
|
571
|
+
library functions directly.
|
|
572
|
+
|
|
262
573
|
## Examples
|
|
263
574
|
|
|
264
575
|
See the [examples](./examples) directory for more detailed examples:
|
|
265
576
|
|
|
266
577
|
- [quickstart.ts](./examples/quickstart.ts) - Comprehensive usage examples
|
|
267
|
-
- [parser-example.ts](./examples/parser-example.ts) - Parser API demonstration
|
|
578
|
+
- [parser-example.ts](./examples/parser-example.ts) - Parser and Printer API demonstration
|
|
268
579
|
- [interpreter-example.ts](./examples/interpreter-example.ts) - Interpreter API demonstration
|
|
269
580
|
|
|
270
581
|
## License
|