Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c975e88
implement all the literals
Flaque Jun 26, 2025
d91f27b
add .all
Flaque Jun 26, 2025
2721792
add exists
Flaque Jun 26, 2025
1a0d9ca
add exists_one macro
Flaque Jun 26, 2025
7c3edaa
add filter
Flaque Jun 26, 2025
4bc9798
add map
Flaque Jun 26, 2025
b4ac87e
add larger, more real-world integration test suite
Flaque Jun 26, 2025
9b6f5d3
add duration & chained expressions
Flaque Jun 26, 2025
b0bf7a4
add contains, endsWith, trim, and split
Flaque Jun 26, 2025
edd358c
implement math functions
Flaque Jun 26, 2025
dfae51a
add types
Flaque Jun 26, 2025
e76ffa8
update demo and types
Flaque Jun 26, 2025
b158210
add conformance tests
Flaque Jun 27, 2025
7bff156
comparison 32% => 72%; string 58% => 64%; list from 41% to 61%
Flaque Jun 27, 2025
752ce18
pass even more basic tests
Flaque Jun 27, 2025
87f9567
more string compliance tests fixed
Flaque Jun 27, 2025
560e5cf
even more conformance tests pass
Flaque Jun 27, 2025
68c0623
even more conformance tests passed
Flaque Jun 27, 2025
7053b67
add parser tests
Flaque Jun 27, 2025
219cd6a
fix even more conformance tests
Flaque Jun 27, 2025
5170704
even more conformance tests
Flaque Jun 27, 2025
60ecc10
more conformance tests passing
Flaque Jun 27, 2025
308003a
fix more tests
Flaque Jun 27, 2025
6900cf1
add more passing tests
Flaque Jun 28, 2025
b46e5b4
add yet more conformance tests
Flaque Jun 28, 2025
f89ee79
pass more tests
Flaque Jun 29, 2025
2c400c5
pass more conformance tests
Flaque Jun 29, 2025
073a200
more conformance tests pass
Flaque Jul 1, 2025
4319c99
pass even more tests
Flaque Jul 1, 2025
6b23c39
fix more conformance tests
Flaque Jul 1, 2025
348fb50
more conformance tests
Flaque Jul 1, 2025
7635ea0
nuke the custom parser and use @bufbuild/cel-spec
Flaque Jul 1, 2025
ad23584
pass more conformance tests
Flaque Jul 1, 2025
9784d2a
add buf loader
Flaque Jul 1, 2025
274dceb
fix more conformance tests
Flaque Jul 1, 2025
7e9f524
more conformance tests
Flaque Jul 1, 2025
bd0cc26
fix more tests
Flaque Jul 20, 2025
4048eb5
even more tests passing
Flaque Jul 21, 2025
70aed91
add more datils
Flaque Jul 23, 2025
c5351d0
add more details
Flaque Jul 25, 2025
509226a
update demo
Flaque Jul 25, 2025
33af8c5
Resolve merge conflicts in PR #62
Flaque Jul 26, 2025
bf47663
Resolve merge conflicts from ChromeGG/cel-js main
Flaque Jul 28, 2025
be3aa7a
fix tests
Flaque Jul 28, 2025
fb8e75a
fix some lint errors
ChromeGG Jul 28, 2025
56cefb7
remove duplicated tests, fix demo.ts
ChromeGG Jul 28, 2025
1da35a2
format example file
ChromeGG Jul 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "cel-spec"]
path = cel-spec
url = https://github.com/google/cel-spec.git
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,23 @@ Code is not the only thing you can contribute. I truly appreciate contributions
- Review the pull request diff after each new commit. It's better that you catch mistakes early than the maintainers pointing it out and having to go back and forth.
- Be patient. Maintainers often have a lot of pull requests to review. Feel free to bump the pull request if you haven't received a reply in a couple of weeks.
- And most importantly, have fun! 👌🎉

## CEL Conformance Tests

The CEL conformance tests ensure compatibility with the official CEL specification. The test data is automatically loaded from the `@bufbuild/cel-spec` package, which provides pre-decoded protobuf test files.

### Running Conformance Tests

```bash
pnpm test src/spec/conformance/conformance.spec.ts
```

### Updating Test Data

To update to the latest CEL conformance tests, simply bump the `@bufbuild/cel-spec` package version:

```bash
pnpm add @bufbuild/cel-spec@latest
```

The conformance tests are automatically synchronized with the upstream CEL specification - no manual parsing or file conversion is required.
1 change: 1 addition & 0 deletions cel-spec
Submodule cel-spec added at 9f069b
20 changes: 19 additions & 1 deletion demo/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## Demo

This is for testing the library from NPM registry.
This demo showcases the CEL-JS library features including:

- **Basic expressions**: Math, boolean, string operations
- **Collections**: Arrays and maps with indexing
- **String methods**: `contains()`, `endsWith()`, `trim()`, `split()` with chaining support
- **Math functions**: `abs()`, `max()`, `min()`, `floor()`, `ceil()`
- **Math extensions**: `math.greatest()`, `math.least()`, `math.isNaN()`, `math.isInf()`, `math.isFinite()`
- **String extensions**: `strings.quote()` for CEL literal escaping
- **Base64 encoding/decoding**: `base64.encode()` and `base64.decode()`
- **Optional types**: `optional.of()`, `optional.none()` for handling optional values
- **Dynamic type comparisons**: `dyn()` for cross-type comparisons
- **Collection operations**: `filter()`, `map()` (transform and filter+transform)
- **Collection macros**: `all()`, `exists()`, `exists_one()`, `size()`, `has()`
- **Timestamps and durations**: RFC3339 timestamps with arithmetic operations
- **Complex chained expressions**: Combine multiple operations
- **Custom functions**: Register your own functions
- **Comments**: Single and multi-line comment support

Run `pnpm exec tsx demo.ts` to see all features in action.
242 changes: 235 additions & 7 deletions demo/demo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// ? run "pnpm tsx demo" in the terminal to see the output

import { evaluate, parse } from 'cel-js'
import { evaluate, parse } from '../dist/index.js'

// Evaluate and log various types of expressions
{
Expand Down Expand Up @@ -63,7 +63,9 @@ import { evaluate, parse } from 'cel-js'

// Collection macros - filter()
const filterMacroExpr = 'numbers.filter(n, n > 3)'
console.log(`${filterMacroExpr} => ${evaluate(filterMacroExpr, collectionContext)}`) // => [4, 5]
console.log(
`${filterMacroExpr} => ${evaluate(filterMacroExpr, collectionContext)}`,
) // => [4, 5]

// Collection macros - map()
const mapMacroExpr = 'numbers.map(n, n * 2)'
Expand All @@ -75,23 +77,249 @@ import { evaluate, parse } from 'cel-js'

// Collection macros - exists()
const existsMacroExpr = 'numbers.exists(n, n > 5)'
console.log(`${existsMacroExpr} => ${evaluate(existsMacroExpr, collectionContext)}`) // => false
console.log(
`${existsMacroExpr} => ${evaluate(existsMacroExpr, collectionContext)}`,
) // => false

// Collection macros - exists_one()
const existsOneMacroExpr = 'numbers.exists_one(n, n == 5)'
console.log(`${existsOneMacroExpr} => ${evaluate(existsOneMacroExpr, collectionContext)}`) // => true
console.log(
`${existsOneMacroExpr} => ${evaluate(existsOneMacroExpr, collectionContext)}`,
) // => true

// Collection macros on maps
const mapFilterExpr = 'scores.filter(name, scores[name] > 80)'
console.log(`${mapFilterExpr} => ${evaluate(mapFilterExpr, collectionContext)}`) // => ['alice', 'bob']
console.log(
`${mapFilterExpr} => ${evaluate(mapFilterExpr, collectionContext)}`,
) // => ['alice', 'bob']

// Collection macros - map with predicate (3 arguments)
const mapWithPredicateExpr = 'numbers.map(n, n > 3, n * 10)'
console.log(`${mapWithPredicateExpr} => ${evaluate(mapWithPredicateExpr, collectionContext)}`) // => [40, 50]
console.log(
`${mapWithPredicateExpr} => ${evaluate(mapWithPredicateExpr, collectionContext)}`,
) // => [40, 50]

// Collection macros - extract properties
const extractPropsExpr = 'people.map(person, person.name)'
console.log(`${extractPropsExpr} => ${evaluate(extractPropsExpr, collectionContext)}`) // => ['Alice', 'Bob', 'Charlie']
console.log(
`${extractPropsExpr} => ${evaluate(extractPropsExpr, collectionContext)}`,
) // => ['Alice', 'Bob', 'Charlie']

// Math functions
console.log(`abs(-5) => ${evaluate('abs(-5)')}`) // => 5
console.log(`max(3, 7, 2) => ${evaluate('max(3, 7, 2)')}`) // => 7
console.log(`min(3, 7, 2) => ${evaluate('min(3, 7, 2)')}`) // => 2
console.log(`floor(3.7) => ${evaluate('floor(3.7)')}`) // => 3
console.log(`ceil(3.2) => ${evaluate('ceil(3.2)')}`) // => 4

// String methods
console.log(
`"hello world".contains("world") => ${evaluate('"hello world".contains("world")')}`,
) // => true
console.log(
`"filename.txt".endsWith(".txt") => ${evaluate('"filename.txt".endsWith(".txt")')}`,
) // => true
console.log(`" hello ".trim() => "${evaluate('" hello ".trim()')}"`) // => "hello"
console.log(
`"a,b,c".split(",") => ${JSON.stringify(evaluate('"a,b,c".split(",")'))}`,
) // => ["a","b","c"]

// String method chaining
console.log(
`" hello,world ".trim().split(",") => ${JSON.stringify(evaluate('" hello,world ".trim().split(",")'))}`,
) // => ["hello","world"]

// Timestamp and duration
console.log(
`timestamp("2023-01-01T00:00:00Z") => ${(evaluate('timestamp("2023-01-01T00:00:00Z")') as Date).toISOString()}`,
) // => 2023-01-01T00:00:00.000Z
console.log(
`duration("1h30m") => ${JSON.stringify(evaluate('duration("1h30m")'))}`,
) // => {"seconds":5400,"nanoseconds":0}
console.log(
`timestamp("2023-01-01T00:00:00Z") + duration("1h") => ${(evaluate('timestamp("2023-01-01T00:00:00Z") + duration("1h")') as Date).toISOString()}`,
) // => 2023-01-01T01:00:00.000Z

// List operations
// filter
console.log(
`[1, 2, 3, 4, 5].filter(v, v > 3) => ${JSON.stringify(evaluate('[1, 2, 3, 4, 5].filter(v, v > 3)'))}`,
) // => [4,5]

// map (transform)
console.log(
`[1, 2, 3].map(v, v * 2) => ${JSON.stringify(evaluate('[1, 2, 3].map(v, v * 2)'))}`,
) // => [2,4,6]

// map (filter and transform)
console.log(
`[1, 2, 3, 4, 5].map(v, v > 3, v * 2) => ${JSON.stringify(evaluate('[1, 2, 3, 4, 5].map(v, v > 3, v * 2)'))}`,
) // => [8,10]

// List macros
console.log(
`[1, 2, 3].all(v, v > 0) => ${evaluate('[1, 2, 3].all(v, v > 0)')}`,
) // => true
console.log(
`[1, 2, 3].exists(v, v > 2) => ${evaluate('[1, 2, 3].exists(v, v > 2)')}`,
) // => true
console.log(
`[1, 2, 3].exists_one(v, v > 2) => ${evaluate('[1, 2, 3].exists_one(v, v > 2)')}`,
) // => true

// Map operations
console.log(
`{"a": 1, "b": 2, "c": 3}.filter(v, v > 1) => ${JSON.stringify(evaluate('{"a": 1, "b": 2, "c": 3}.filter(v, v > 1)'))}`,
) // => {"b":2,"c":3}
console.log(
`{"a": 1, "b": 2}.map(v, v * 10) => ${JSON.stringify(evaluate('{"a": 1, "b": 2}.map(v, v * 10)'))}`,
) // => {"a":10,"b":20}

// Complex chained expressions
const chainedExpr =
'[" hello ", " world "].map(s, s.trim()).filter(s, s.contains("o"))'
console.log(`${chainedExpr} => ${JSON.stringify(evaluate(chainedExpr))}`) // => ["hello","world"]

// Math extension functions
console.log(`math.greatest(3, 7, 2) => ${evaluate('math.greatest(3, 7, 2)')}`) // => 7
console.log(`math.least(3, 7, 2) => ${evaluate('math.least(3, 7, 2)')}`) // => 2
console.log(`math.isNaN(0.0 / 0.0) => ${evaluate('math.isNaN(0.0 / 0.0)')}`) // => true
console.log(`math.isInf(1.0 / 0.0) => ${evaluate('math.isInf(1.0 / 0.0)')}`) // => true
console.log(`math.isFinite(42.0) => ${evaluate('math.isFinite(42.0)')}`) // => true

// String extension functions
console.log(
`strings.quote('hello "world"') => ${evaluate('strings.quote(\'hello "world"\')')}`,
) // => "hello \"world\""

// Base64 encoding/decoding
console.log(
`base64.encode(b'hello') => ${evaluate("base64.encode(b'hello')")}`,
) // => "aGVsbG8="
console.log(
`base64.decode('aGVsbG8=') => ${JSON.stringify(evaluate("base64.decode('aGVsbG8=')"))}`,
) // => [104,101,108,108,111]

// Optional types
console.log(
`optional.of(42) => ${JSON.stringify(evaluate('optional.of(42)'))}`,
) // => {"hasValue":true,"value":42}
console.log(
`optional.none() => ${JSON.stringify(evaluate('optional.none()'))}`,
) // => {"hasValue":false}

// Dynamic type comparisons
console.log(
`dyn('hello') == dyn('hello') => ${evaluate("dyn('hello') == dyn('hello')")}`,
) // => true

// Math functions
console.log(`abs(-5) => ${evaluate('abs(-5)')}`) // => 5
console.log(`max(3, 7, 2) => ${evaluate('max(3, 7, 2)')}`) // => 7
console.log(`min(3, 7, 2) => ${evaluate('min(3, 7, 2)')}`) // => 2
console.log(`floor(3.7) => ${evaluate('floor(3.7)')}`) // => 3
console.log(`ceil(3.2) => ${evaluate('ceil(3.2)')}`) // => 4

// String methods
console.log(
`"hello world".contains("world") => ${evaluate('"hello world".contains("world")')}`,
) // => true
console.log(
`"filename.txt".endsWith(".txt") => ${evaluate('"filename.txt".endsWith(".txt")')}`,
) // => true
console.log(`" hello ".trim() => "${evaluate('" hello ".trim()')}"`) // => "hello"
console.log(
`"a,b,c".split(",") => ${JSON.stringify(evaluate('"a,b,c".split(",")'))}`,
) // => ["a","b","c"]

// String method chaining
console.log(
`" hello,world ".trim().split(",") => ${JSON.stringify(evaluate('" hello,world ".trim().split(",")'))}`,
) // => ["hello","world"]

// Timestamp and duration
console.log(
`timestamp("2023-01-01T00:00:00Z") => ${(evaluate('timestamp("2023-01-01T00:00:00Z")') as Date).toISOString()}`,
) // => 2023-01-01T00:00:00.000Z
console.log(
`duration("1h30m") => ${JSON.stringify(evaluate('duration("1h30m")'))}`,
) // => {"seconds":5400,"nanoseconds":0}
console.log(
`timestamp("2023-01-01T00:00:00Z") + duration("1h") => ${(evaluate('timestamp("2023-01-01T00:00:00Z") + duration("1h")') as Date).toISOString()}`,
) // => 2023-01-01T01:00:00.000Z

// List operations
// filter
console.log(
`[1, 2, 3, 4, 5].filter(v, v > 3) => ${JSON.stringify(evaluate('[1, 2, 3, 4, 5].filter(v, v > 3)'))}`,
) // => [4,5]

// map (transform)
console.log(
`[1, 2, 3].map(v, v * 2) => ${JSON.stringify(evaluate('[1, 2, 3].map(v, v * 2)'))}`,
) // => [2,4,6]

// map (filter and transform)
console.log(
`[1, 2, 3, 4, 5].map(v, v > 3, v * 2) => ${JSON.stringify(evaluate('[1, 2, 3, 4, 5].map(v, v > 3, v * 2)'))}`,
) // => [8,10]

// List macros
console.log(
`[1, 2, 3].all(v, v > 0) => ${evaluate('[1, 2, 3].all(v, v > 0)')}`,
) // => true
console.log(
`[1, 2, 3].exists(v, v > 2) => ${evaluate('[1, 2, 3].exists(v, v > 2)')}`,
) // => true
console.log(
`[1, 2, 3].exists_one(v, v > 2) => ${evaluate('[1, 2, 3].exists_one(v, v > 2)')}`,
) // => true

// Map operations
console.log(
`{"a": 1, "b": 2, "c": 3}.filter(v, v > 1) => ${JSON.stringify(evaluate('{"a": 1, "b": 2, "c": 3}.filter(v, v > 1)'))}`,
) // => {"b":2,"c":3}
console.log(
`{"a": 1, "b": 2}.map(v, v * 10) => ${JSON.stringify(evaluate('{"a": 1, "b": 2}.map(v, v * 10)'))}`,
) // => {"a":10,"b":20}

// Complex chained expressions
const chainedExpr2 =
'[" hello ", " world "].map(s, s.trim()).filter(s, s.contains("o"))'
console.log(`${chainedExpr2} => ${JSON.stringify(evaluate(chainedExpr2))}`) // => ["hello","world"]

// Math extension functions
console.log(`math.greatest(3, 7, 2) => ${evaluate('math.greatest(3, 7, 2)')}`) // => 7
console.log(`math.least(3, 7, 2) => ${evaluate('math.least(3, 7, 2)')}`) // => 2
console.log(`math.isNaN(0.0 / 0.0) => ${evaluate('math.isNaN(0.0 / 0.0)')}`) // => true
console.log(`math.isInf(1.0 / 0.0) => ${evaluate('math.isInf(1.0 / 0.0)')}`) // => true
console.log(`math.isFinite(42.0) => ${evaluate('math.isFinite(42.0)')}`) // => true

// String extension functions
console.log(
`strings.quote('hello "world"') => ${evaluate('strings.quote(\'hello "world"\')')}`,
) // => "hello \"world\""

// Base64 encoding/decoding
console.log(
`base64.encode(b'hello') => ${evaluate("base64.encode(b'hello')")}`,
) // => "aGVsbG8="
console.log(
`base64.decode('aGVsbG8=') => ${JSON.stringify(evaluate("base64.decode('aGVsbG8=')"))}`,
) // => [104,101,108,108,111]

// Optional types
console.log(
`optional.of(42) => ${JSON.stringify(evaluate('optional.of(42)'))}`,
) // => {"hasValue":true,"value":42}
console.log(
`optional.none() => ${JSON.stringify(evaluate('optional.none()'))}`,
) // => {"hasValue":false}

// Dynamic type comparisons
console.log(
`dyn('hello') == dyn('hello') => ${evaluate("dyn('hello') == dyn('hello')")}`,
) // => true

// Custom function expressions
const functionExpr = 'max(2, 1, 3, 7)'
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"release": "release-it"
},
"dependencies": {
"@bufbuild/cel-spec": "0.2.0",
"@bufbuild/protobuf": "2.6.0",
"chevrotain": "11.0.3",
"ramda": "0.30.1"
},
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading