├── .github ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .release-it.json ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── demo ├── README.md ├── demo.ts ├── package.json └── pnpm-lock.yaml ├── esbuild.mjs ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── src ├── cst-definitions.d.ts ├── errors │ ├── CelEvaluationError.ts │ ├── CelParseError.ts │ └── CelTypeError.ts ├── helper.ts ├── index.ts ├── lib.ts ├── parser.ts ├── spec │ ├── addition.spec.ts │ ├── atomic-expression.spec.ts │ ├── comments.spec.ts │ ├── comparisons.spec.ts │ ├── conditional-ternary.spec.ts │ ├── hex-integer.spec.ts │ ├── identifiers.spec.ts │ ├── index.spec.ts │ ├── lists.spec.ts │ ├── logical-operators.spec.ts │ ├── macros.spec.ts │ ├── maps.spec.ts │ ├── miscellaneous.spec.ts │ ├── multiplication.spec.ts │ ├── reserver-identifiers.spec.ts │ ├── unary-operators.spec.ts │ └── unsigned-integer.spec.ts ├── tokens.ts └── visitor.ts ├── ts-signatures-generator.ts ├── tsconfig.json └── vitest.config.ts /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | 4 | 5 | ## Issue ticket number/link 6 | 7 | 8 | 9 | ## Checklist before requesting a review 10 | 11 | - [ ] I have performed a self-review of my code 12 | - [ ] I have added tests (if possible) 13 | - [ ] I have updated the readme (if necessary) 14 | - [ ] I have updated the demo (if necessary) 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Workflow 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out code 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 20.10.0 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v2 23 | with: 24 | version: 8.11.0 25 | 26 | - name: Install dependencies 27 | run: pnpm install 28 | 29 | - name: Run lint 30 | run: pnpm run lint 31 | 32 | - name: Run type check 33 | run: pnpm run type-check 34 | 35 | - name: Run prettier 36 | run: pnpm prettier ./src --check 37 | 38 | test: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Check out code 42 | uses: actions/checkout@v3 43 | with: 44 | fetch-depth: 0 45 | 46 | - name: Install Node.js 47 | uses: actions/setup-node@v2 48 | with: 49 | node-version: 20.10.0 50 | 51 | - name: Install pnpm 52 | uses: pnpm/action-setup@v2 53 | with: 54 | version: 8.11.0 55 | 56 | - name: Install dependencies 57 | run: pnpm install 58 | 59 | - name: Run tests 60 | run: pnpm run test 61 | 62 | build: 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Check out code 66 | uses: actions/checkout@v3 67 | with: 68 | fetch-depth: 0 69 | 70 | - name: Install Node.js 71 | uses: actions/setup-node@v2 72 | with: 73 | node-version: 20.10.0 74 | 75 | - name: Install pnpm 76 | uses: pnpm/action-setup@v2 77 | with: 78 | version: 8.11.0 79 | 80 | - name: Install dependencies 81 | run: pnpm install 82 | 83 | - name: Run build 84 | run: pnpm run build 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix="" 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore: release v${version}", 4 | "tagName": "v${version}" 5 | }, 6 | "github": { 7 | "release": true 8 | }, 9 | "npm": { 10 | "publish": true 11 | }, 12 | "packageManager": "pnpm" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "cSpell.words": ["outfile"] 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | sindresorhus@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Copied from [Sindresorhus](https://github.com/sindresorhus/.github/blob/main/contributing.md) 🙂 4 | 5 | Be nice and respect the maintainers' time. ❤️ 6 | 7 | Please read this whole thing. Most of this applies to any repo on GitHub. 🙏 8 | 9 | ## Contribute 10 | 11 | Code is not the only thing you can contribute. I truly appreciate contributions in the form of: 12 | 13 | - Fixing typos. 14 | - Improving docs. 15 | - Triaging [issues](https://github.com/search?o=desc&q=user:sindresorhus+user:xojs+user:chalk+is:issue+is:open&s=updated&type=Issues). 16 | - Reviewing [pull requests](https://github.com/search?o=desc&q=user:sindresorhus+user:xojs+user:chalk+is:pr+is:open&s=updated&type=Issues). 17 | - Sharing your opinion on issues. 18 | 19 | ## Issues 20 | 21 | - Before opening a new issue, look for existing issues (even closed ones). 22 | - [Don't needlessly bump issues.](https://sindresorhus.com/blog/issue-bumping) 23 | - If you're reporting a bug, include as much information as possible. Ideally, include a test case that reproduces the bug. For example, a [Runkit](https://runkit.com) or [repl.it](https://repl.it) playground. Even better, submit a pull request with a [failing test](https://github.com/avajs/ava/blob/master/docs/01-writing-tests.md#failing-tests). 24 | 25 | ## Pull requests 26 | 27 | ### Prerequisite 28 | 29 | - If the changes are large or breaking, open an issue discussing it first. 30 | - Don't open a pull request if you don't plan to see it through. Maintainers waste a lot of time giving feedback on pull requests that eventually go stale. 31 | - Don't do unrelated changes. 32 | - Adhere to the existing code style. 33 | - If relevant, add tests, check for typos, and add docs and types. 34 | - Don't add editor-specific metafiles. Those should be added to your own [global gitignore](https://gist.github.com/subfuzion/db7f57fff2fb6998a16c). 35 | - Don't be sloppy. I expect you to do your best. 36 | - Squash your local commits into one commit before submitting the pull request, unless you have important atomic commits. 37 | - Double-check your contribution by going over the diff of your changes before submitting a pull request. It's a good way to catch bugs/typos and find ways to improve the code. 38 | - Do the pull request from a new branch. Never the default branch (`main`/`master`). 39 | 40 | ### Submission 41 | 42 | - Give the pull request a clear title and description. It's up to you to convince the maintainers why your changes should be merged. 43 | - If the pull request fixes an issue, reference it in the pull request description using the syntax `Fixes #123`. 44 | - Make sure the “Allow edits from maintainers” checkbox is checked. That way I can make certain minor changes myself, allowing your pull request to be merged sooner. 45 | 46 | ### Review 47 | 48 | - Push new commits when doing changes to the pull request. Don't squash as it makes it hard to see what changed since the last review. I will squash when merging. 49 | - It's better to present solutions than asking questions. 50 | - 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. 51 | - 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. 52 | - And most importantly, have fun! 👌🎉 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Adam Tkaczyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ## Demo 2 | 3 | This is for testing the library from NPM registry. 4 | -------------------------------------------------------------------------------- /demo/demo.ts: -------------------------------------------------------------------------------- 1 | // ? run "pnpm tsx demo" in the terminal to see the output 2 | 3 | import { evaluate, parse } from 'cel-js' 4 | 5 | // Evaluate and log various types of expressions 6 | { 7 | // Math expression 8 | const mathExpr = '2 + 2 * 2' 9 | console.log(`${mathExpr} => ${evaluate(mathExpr)}`) // => 6 10 | 11 | // Float expression 12 | const floatExpr = '0.1 + 0.2' 13 | console.log(`${floatExpr} => ${evaluate(floatExpr)}`) // => 0.30000000000000004, same as cel-go, due to floating point precision 14 | 15 | // Parenthesized expression 16 | const parenthesizedExpr = '(2 + 2) * 2' 17 | console.log(`${parenthesizedExpr} => ${evaluate(parenthesizedExpr)}`) // => 8 18 | 19 | // Boolean expression 20 | const booleanExpr = 'true && !false' 21 | console.log(`${booleanExpr} => ${evaluate(booleanExpr)}`) // => true 22 | 23 | // String concatenation 24 | const stringExpr = `"foo" + 'bar'` 25 | console.log(`${stringExpr} => ${evaluate(stringExpr)}`) // => 'foobar' 26 | 27 | // Identifier expression with context 28 | const identifierExpr = 'user.role == "admin"' 29 | const context = { user: { role: 'admin' } } 30 | console.log(`${identifierExpr} => ${evaluate(identifierExpr, context)}`) // => true 31 | 32 | // Ternary operator 33 | const ternaryExpr = 'user.role == "admin" ? "owner" : "user"' 34 | console.log(`${ternaryExpr} => ${evaluate(ternaryExpr, context)}`) // => 'owner' 35 | 36 | // Array expressions 37 | const arrayExpr = '[1, 2]' 38 | console.log(`${arrayExpr} => ${evaluate(arrayExpr)}`) // => [1, 2] 39 | 40 | // Map expressions 41 | const mapExpr = '{"a": 1, "b": 2}' 42 | console.log(`${mapExpr} => ${JSON.stringify(evaluate(mapExpr))}`) // => { a: 1, b: 2 } 43 | 44 | // Macro expressions 45 | // size() 46 | const macroExpr = 'size([1, 2])' 47 | console.log(`${macroExpr} => ${evaluate(macroExpr)}`) // => 2 48 | 49 | // has() 50 | const hasExpr = 'has(user.role)' 51 | console.log(`${hasExpr} => ${evaluate(hasExpr, context)}`) // => true 52 | 53 | // Custom function expressions 54 | const functionExpr = 'max(2, 1, 3, 7)' 55 | console.log( 56 | `${functionExpr} => ${evaluate(functionExpr, {}, { max: Math.max })}`, 57 | ) // => 7 58 | 59 | // Comment support 60 | const commentedExpr = `// multi-line comment 61 | "foo" + // some comment 62 | "bar" 63 | ` 64 | console.log(`${commentedExpr} => ${evaluate(commentedExpr)}`) // => 'foobar' 65 | } 66 | 67 | // Parse an expression, useful for validation purposes before persisting 68 | { 69 | const result = parse('2 + 2') 70 | 71 | if (!result.isSuccess) { 72 | throw new Error('Invalid syntax') 73 | } 74 | 75 | // Reuse the result of `parse` to evaluate the expression 76 | console.log(evaluate(result.cst)) // => 4 77 | } 78 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cel-js", 3 | "version": "1.0.0", 4 | "description": "A simple usage of the CEL library", 5 | "type": "module", 6 | "dependencies": { 7 | "cel-js": "0.7.0" 8 | }, 9 | "devDependencies": { 10 | "tsx": "4.10.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demo/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | cel-js: 12 | specifier: 0.7.0 13 | version: 0.7.0 14 | devDependencies: 15 | tsx: 16 | specifier: 4.10.0 17 | version: 4.10.0 18 | 19 | packages: 20 | 21 | '@chevrotain/cst-dts-gen@11.0.3': 22 | resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} 23 | 24 | '@chevrotain/gast@11.0.3': 25 | resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} 26 | 27 | '@chevrotain/regexp-to-ast@11.0.3': 28 | resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} 29 | 30 | '@chevrotain/types@11.0.3': 31 | resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} 32 | 33 | '@chevrotain/utils@11.0.3': 34 | resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} 35 | 36 | '@esbuild/aix-ppc64@0.20.2': 37 | resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} 38 | engines: {node: '>=12'} 39 | cpu: [ppc64] 40 | os: [aix] 41 | 42 | '@esbuild/android-arm64@0.20.2': 43 | resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} 44 | engines: {node: '>=12'} 45 | cpu: [arm64] 46 | os: [android] 47 | 48 | '@esbuild/android-arm@0.20.2': 49 | resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} 50 | engines: {node: '>=12'} 51 | cpu: [arm] 52 | os: [android] 53 | 54 | '@esbuild/android-x64@0.20.2': 55 | resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} 56 | engines: {node: '>=12'} 57 | cpu: [x64] 58 | os: [android] 59 | 60 | '@esbuild/darwin-arm64@0.20.2': 61 | resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} 62 | engines: {node: '>=12'} 63 | cpu: [arm64] 64 | os: [darwin] 65 | 66 | '@esbuild/darwin-x64@0.20.2': 67 | resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} 68 | engines: {node: '>=12'} 69 | cpu: [x64] 70 | os: [darwin] 71 | 72 | '@esbuild/freebsd-arm64@0.20.2': 73 | resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} 74 | engines: {node: '>=12'} 75 | cpu: [arm64] 76 | os: [freebsd] 77 | 78 | '@esbuild/freebsd-x64@0.20.2': 79 | resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} 80 | engines: {node: '>=12'} 81 | cpu: [x64] 82 | os: [freebsd] 83 | 84 | '@esbuild/linux-arm64@0.20.2': 85 | resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} 86 | engines: {node: '>=12'} 87 | cpu: [arm64] 88 | os: [linux] 89 | 90 | '@esbuild/linux-arm@0.20.2': 91 | resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} 92 | engines: {node: '>=12'} 93 | cpu: [arm] 94 | os: [linux] 95 | 96 | '@esbuild/linux-ia32@0.20.2': 97 | resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} 98 | engines: {node: '>=12'} 99 | cpu: [ia32] 100 | os: [linux] 101 | 102 | '@esbuild/linux-loong64@0.20.2': 103 | resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} 104 | engines: {node: '>=12'} 105 | cpu: [loong64] 106 | os: [linux] 107 | 108 | '@esbuild/linux-mips64el@0.20.2': 109 | resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} 110 | engines: {node: '>=12'} 111 | cpu: [mips64el] 112 | os: [linux] 113 | 114 | '@esbuild/linux-ppc64@0.20.2': 115 | resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} 116 | engines: {node: '>=12'} 117 | cpu: [ppc64] 118 | os: [linux] 119 | 120 | '@esbuild/linux-riscv64@0.20.2': 121 | resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} 122 | engines: {node: '>=12'} 123 | cpu: [riscv64] 124 | os: [linux] 125 | 126 | '@esbuild/linux-s390x@0.20.2': 127 | resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} 128 | engines: {node: '>=12'} 129 | cpu: [s390x] 130 | os: [linux] 131 | 132 | '@esbuild/linux-x64@0.20.2': 133 | resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} 134 | engines: {node: '>=12'} 135 | cpu: [x64] 136 | os: [linux] 137 | 138 | '@esbuild/netbsd-x64@0.20.2': 139 | resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} 140 | engines: {node: '>=12'} 141 | cpu: [x64] 142 | os: [netbsd] 143 | 144 | '@esbuild/openbsd-x64@0.20.2': 145 | resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} 146 | engines: {node: '>=12'} 147 | cpu: [x64] 148 | os: [openbsd] 149 | 150 | '@esbuild/sunos-x64@0.20.2': 151 | resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} 152 | engines: {node: '>=12'} 153 | cpu: [x64] 154 | os: [sunos] 155 | 156 | '@esbuild/win32-arm64@0.20.2': 157 | resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} 158 | engines: {node: '>=12'} 159 | cpu: [arm64] 160 | os: [win32] 161 | 162 | '@esbuild/win32-ia32@0.20.2': 163 | resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} 164 | engines: {node: '>=12'} 165 | cpu: [ia32] 166 | os: [win32] 167 | 168 | '@esbuild/win32-x64@0.20.2': 169 | resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} 170 | engines: {node: '>=12'} 171 | cpu: [x64] 172 | os: [win32] 173 | 174 | cel-js@0.7.0: 175 | resolution: {integrity: sha512-1t/lkk+AG0Q0vLoaeWMHoy0O37gqaIValX8jfpaGZ+EkWfUhnDIT1iM1wL3A/pdz8cQ6yfP3pq6wpxlOgvxrHw==} 176 | engines: {node: '>=18.0.0'} 177 | 178 | chevrotain@11.0.3: 179 | resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} 180 | 181 | esbuild@0.20.2: 182 | resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} 183 | engines: {node: '>=12'} 184 | hasBin: true 185 | 186 | fsevents@2.3.3: 187 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 188 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 189 | os: [darwin] 190 | 191 | get-tsconfig@4.7.5: 192 | resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} 193 | 194 | lodash-es@4.17.21: 195 | resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} 196 | 197 | ramda@0.30.1: 198 | resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==} 199 | 200 | resolve-pkg-maps@1.0.0: 201 | resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 202 | 203 | tsx@4.10.0: 204 | resolution: {integrity: sha512-Ct/j4Yv49EFlr1z5CT++ld+BUhjLRLtimE4hIDaW9zEVIp3xJOQdTDAan+KEXeme7GcYIGzFD421Zcqf9dHomw==} 205 | engines: {node: '>=18.0.0'} 206 | hasBin: true 207 | 208 | snapshots: 209 | 210 | '@chevrotain/cst-dts-gen@11.0.3': 211 | dependencies: 212 | '@chevrotain/gast': 11.0.3 213 | '@chevrotain/types': 11.0.3 214 | lodash-es: 4.17.21 215 | 216 | '@chevrotain/gast@11.0.3': 217 | dependencies: 218 | '@chevrotain/types': 11.0.3 219 | lodash-es: 4.17.21 220 | 221 | '@chevrotain/regexp-to-ast@11.0.3': {} 222 | 223 | '@chevrotain/types@11.0.3': {} 224 | 225 | '@chevrotain/utils@11.0.3': {} 226 | 227 | '@esbuild/aix-ppc64@0.20.2': 228 | optional: true 229 | 230 | '@esbuild/android-arm64@0.20.2': 231 | optional: true 232 | 233 | '@esbuild/android-arm@0.20.2': 234 | optional: true 235 | 236 | '@esbuild/android-x64@0.20.2': 237 | optional: true 238 | 239 | '@esbuild/darwin-arm64@0.20.2': 240 | optional: true 241 | 242 | '@esbuild/darwin-x64@0.20.2': 243 | optional: true 244 | 245 | '@esbuild/freebsd-arm64@0.20.2': 246 | optional: true 247 | 248 | '@esbuild/freebsd-x64@0.20.2': 249 | optional: true 250 | 251 | '@esbuild/linux-arm64@0.20.2': 252 | optional: true 253 | 254 | '@esbuild/linux-arm@0.20.2': 255 | optional: true 256 | 257 | '@esbuild/linux-ia32@0.20.2': 258 | optional: true 259 | 260 | '@esbuild/linux-loong64@0.20.2': 261 | optional: true 262 | 263 | '@esbuild/linux-mips64el@0.20.2': 264 | optional: true 265 | 266 | '@esbuild/linux-ppc64@0.20.2': 267 | optional: true 268 | 269 | '@esbuild/linux-riscv64@0.20.2': 270 | optional: true 271 | 272 | '@esbuild/linux-s390x@0.20.2': 273 | optional: true 274 | 275 | '@esbuild/linux-x64@0.20.2': 276 | optional: true 277 | 278 | '@esbuild/netbsd-x64@0.20.2': 279 | optional: true 280 | 281 | '@esbuild/openbsd-x64@0.20.2': 282 | optional: true 283 | 284 | '@esbuild/sunos-x64@0.20.2': 285 | optional: true 286 | 287 | '@esbuild/win32-arm64@0.20.2': 288 | optional: true 289 | 290 | '@esbuild/win32-ia32@0.20.2': 291 | optional: true 292 | 293 | '@esbuild/win32-x64@0.20.2': 294 | optional: true 295 | 296 | cel-js@0.7.0: 297 | dependencies: 298 | chevrotain: 11.0.3 299 | ramda: 0.30.1 300 | 301 | chevrotain@11.0.3: 302 | dependencies: 303 | '@chevrotain/cst-dts-gen': 11.0.3 304 | '@chevrotain/gast': 11.0.3 305 | '@chevrotain/regexp-to-ast': 11.0.3 306 | '@chevrotain/types': 11.0.3 307 | '@chevrotain/utils': 11.0.3 308 | lodash-es: 4.17.21 309 | 310 | esbuild@0.20.2: 311 | optionalDependencies: 312 | '@esbuild/aix-ppc64': 0.20.2 313 | '@esbuild/android-arm': 0.20.2 314 | '@esbuild/android-arm64': 0.20.2 315 | '@esbuild/android-x64': 0.20.2 316 | '@esbuild/darwin-arm64': 0.20.2 317 | '@esbuild/darwin-x64': 0.20.2 318 | '@esbuild/freebsd-arm64': 0.20.2 319 | '@esbuild/freebsd-x64': 0.20.2 320 | '@esbuild/linux-arm': 0.20.2 321 | '@esbuild/linux-arm64': 0.20.2 322 | '@esbuild/linux-ia32': 0.20.2 323 | '@esbuild/linux-loong64': 0.20.2 324 | '@esbuild/linux-mips64el': 0.20.2 325 | '@esbuild/linux-ppc64': 0.20.2 326 | '@esbuild/linux-riscv64': 0.20.2 327 | '@esbuild/linux-s390x': 0.20.2 328 | '@esbuild/linux-x64': 0.20.2 329 | '@esbuild/netbsd-x64': 0.20.2 330 | '@esbuild/openbsd-x64': 0.20.2 331 | '@esbuild/sunos-x64': 0.20.2 332 | '@esbuild/win32-arm64': 0.20.2 333 | '@esbuild/win32-ia32': 0.20.2 334 | '@esbuild/win32-x64': 0.20.2 335 | 336 | fsevents@2.3.3: 337 | optional: true 338 | 339 | get-tsconfig@4.7.5: 340 | dependencies: 341 | resolve-pkg-maps: 1.0.0 342 | 343 | lodash-es@4.17.21: {} 344 | 345 | ramda@0.30.1: {} 346 | 347 | resolve-pkg-maps@1.0.0: {} 348 | 349 | tsx@4.10.0: 350 | dependencies: 351 | esbuild: 0.20.2 352 | get-tsconfig: 4.7.5 353 | optionalDependencies: 354 | fsevents: 2.3.3 355 | -------------------------------------------------------------------------------- /esbuild.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { fileURLToPath } from 'url' 5 | 6 | await esbuild.build({ 7 | entryPoints: ['src/index.ts'], 8 | bundle: true, 9 | outfile: 'dist/index.mjs', 10 | format: 'esm', 11 | minify: true, 12 | }) 13 | 14 | // TODO can we do it with esbuild/tsconfig.json? 15 | // remove all files with .d.ts extension from dist folder except index.d.ts 16 | const __filename = fileURLToPath(import.meta.url) 17 | const __dirname = path.dirname(__filename) 18 | 19 | const distDir = path.join(__dirname, 'dist') 20 | const files = fs.readdirSync(distDir) 21 | 22 | files.forEach((file) => { 23 | if (file.endsWith('.d.ts') && file !== 'index.d.ts') { 24 | fs.unlinkSync(path.join(distDir, file)) 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js' 2 | import tseslint from 'typescript-eslint' 3 | import sonarjs from 'eslint-plugin-sonarjs' 4 | import vitestPlugin from 'eslint-plugin-vitest' 5 | 6 | export default tseslint.config( 7 | // Base configurations 8 | eslint.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | 11 | // Add SonarJS rules 12 | { 13 | plugins: { 14 | sonarjs: sonarjs, 15 | }, 16 | rules: { 17 | ...sonarjs.configs.recommended.rules, 18 | }, 19 | }, 20 | 21 | // Global settings 22 | { 23 | ignores: ['dist/**'], 24 | rules: { 25 | 'sonarjs/slow-regex': 'off', 26 | 'sonarjs/concise-regex': 'off', 27 | 'sonarjs/cognitive-complexity': 'off', 28 | }, 29 | }, 30 | 31 | // Override for test files 32 | { 33 | files: ['**/*.spec.ts'], 34 | plugins: { 35 | vitest: vitestPlugin, 36 | }, 37 | rules: { 38 | ...vitestPlugin.configs.all.rules, 39 | 'vitest/prefer-to-be-truthy': 'off', 40 | 'vitest/prefer-to-be-falsy': 'off', 41 | 'vitest/no-focused-tests': 'error', 42 | 'vitest/consistent-test-filename': 'off', 43 | 'vitest/prefer-lowercase-title': 'off', 44 | 'vitest/prefer-expect-assertions': 'off', 45 | 'sonarjs/no-nested-functions': 'off', 46 | }, 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cel-js", 3 | "version": "0.7.1", 4 | "description": "Common Expression Language (CEL) evaluator for JavaScript", 5 | "keywords": [ 6 | "Common Expression Language", 7 | "CEL", 8 | "evaluator", 9 | "javascript", 10 | "typescript" 11 | ], 12 | "homepage": "https://github.com/ChromeGG/cel-js", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/ChromeGG/cel-js.git" 16 | }, 17 | "license": "MIT", 18 | "author": "Adam Tkaczyk", 19 | "type": "module", 20 | "main": "./dist/index.js", 21 | "types": "./dist/index.d.ts", 22 | "exports": { 23 | ".": "./dist/index.js" 24 | }, 25 | "files": [ 26 | "dist" 27 | ], 28 | "scripts": { 29 | "build": "pnpm run clean && tsc", 30 | "clean": "rm -rf ./dist", 31 | "generate": "tsx ./ts-signatures-generator", 32 | "lint": "eslint --ext .ts src", 33 | "prepack": "pnpm run build", 34 | "test": "vitest ./src --run", 35 | "test:watch": "vitest ./src", 36 | "type-check": "tsc --noEmit -p tsconfig.json", 37 | "release": "release-it" 38 | }, 39 | "dependencies": { 40 | "chevrotain": "11.0.3", 41 | "ramda": "0.30.1" 42 | }, 43 | "devDependencies": { 44 | "@eslint/js": "9.23.0", 45 | "@types/node": "22.13.4", 46 | "@types/ramda": "0.30.1", 47 | "esbuild": "0.25.0", 48 | "eslint": "9.23.0", 49 | "eslint-plugin-sonarjs": "3.0.2", 50 | "eslint-plugin-vitest": "0.5.4", 51 | "prettier": "3.5.1", 52 | "release-it": "18.1.2", 53 | "tsx": "4.19.3", 54 | "typescript": "5.7.3", 55 | "typescript-eslint": "8.27.0", 56 | "vitest": "3.0.6" 57 | }, 58 | "engines": { 59 | "node": ">=18.0.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cel-js 2 | 3 | `cel-js` is a powerful and efficient parser and evaluator for Google's [Common Expression Language](https://github.com/google/cel-spec) (CEL), built on the robust foundation of the [Chevrotain](https://chevrotain.io/docs/) parsing library. This library aims to provide a seamless and easy-to-use interface for working with CEL in JavaScript environments. 4 | 5 | ## Live Demo 🚀 6 | 7 | Try out `cel-js` in your browser with the [live demo](https://stackblitz.com/github/ChromeGG/cel-js/tree/main/demo?file=demo.ts). 8 | 9 | ## Features ✨ 10 | 11 | - 🚀 Fast and Efficient Parsing: Leverages Chevrotain for high-performance parsing and evaluation 12 | - 🌍 Isomorphic: Ready for server and browser 13 | - 📦 ESM support 14 | - 📚 Supported CEL Features: 15 | - [x] Literals 16 | - [x] int 17 | - [x] uint 18 | - [x] double 19 | - [x] bool 20 | - [x] string 21 | - [x] single-quote string 22 | - [x] double-quote string 23 | - [ ] raw string 24 | - [ ] triple-quote string 25 | - [ ] byte string 26 | - [x] hexadecimal 27 | - [ ] bytes 28 | - [x] list 29 | - [x] map 30 | - [x] null 31 | - [x] Conditional Operators 32 | - [x] Ternary (`condition ? true : false`) 33 | - [x] Logical And (`&&`) 34 | - [x] Logical Or (`||`) 35 | - [x] Equality Operators (`==`, `!=`) 36 | - [x] Relational Operators (`<`, `<=`, `>`, `>=`, `in`) 37 | - [x] Arithmetic Operators (`+`, `-`, `*`, `/`, `%`) 38 | - [x] Identifiers 39 | - [x] Dot Notation (`foo.bar`) 40 | - [x] Index Notation (`foo["bar"]`) 41 | - [x] [Macros](https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros): (`has`, `size`, etc.) 42 | - [ ] All (`e.all(x, p)`) 43 | - [ ] Exists (`e.exists(x, p)`) 44 | - [ ] Exists one (`e.exists_one(x, p)`) 45 | - [ ] Filter (`e.filter(x, p)`) 46 | - [x] Has (`has(foo.bar)`) 47 | - [ ] Map (`e.map(x, t)` and `e.map(x, p, t)`) 48 | - [x] Size (`size(foo)`) 49 | - [x] Unary Operators (`!true`, `-123`) 50 | - [x] Custom Functions (`myFunction()`) 51 | - [x] Comments (`// This is a comment`) 52 | 53 | ## Installation 54 | 55 | To install `cel-js`, use npm: 56 | 57 | ```bash 58 | npm i cel-js 59 | ``` 60 | 61 | ## Usage 62 | 63 | ### `evaluate` 64 | 65 | `evaluate` is the primary function for parsing and evaluating CEL expressions. It takes an expression string and an optional object of variables to use in the expression. 66 | 67 | ```ts 68 | import { evaluate, parse } from 'cel-js' 69 | 70 | // use `evaluate` to parse and evaluate an expression 71 | evaluate('2 + 2 * 2') // => 6 72 | 73 | evaluate('"foo" + "bar"') // => 'foobar' 74 | 75 | evaluate('user.role == "admin"', { user: { role: 'admin' } }) // => true 76 | ``` 77 | 78 | ### `parse` 79 | 80 | `parse` is a lower-level function that only parses an expression string into an AST. This can be useful if you want to evaluate the expression multiple times with different variables or if you want to validate the syntax of an expression. 81 | 82 | ```ts 83 | // use `parse` to parse an expression, useful for validation purposes 84 | const result = parse('2 + a') 85 | 86 | if (!result.isSuccess) { 87 | // your business logic 88 | } 89 | 90 | // you can reuse the result of `parse` to evaluate the expression 91 | evaluate(result.cst, { a: 2 }) // => 4 92 | evaluate(result.cst, { a: 4 }) // => 6 93 | ``` 94 | 95 | ## Known Issues 96 | 97 | - Errors types and messages are not 100% consistent with the cel-go implementation, 98 | -------------------------------------------------------------------------------- /src/cst-definitions.d.ts: -------------------------------------------------------------------------------- 1 | import type { CstNode, ICstVisitor, IToken } from 'chevrotain' 2 | 3 | export interface ExprCstNode extends CstNode { 4 | name: 'expr' 5 | children: ExprCstChildren 6 | } 7 | 8 | export type ExprCstChildren = { 9 | conditionalOr: ConditionalOrCstNode[] 10 | QuestionMark?: IToken[] 11 | lhs?: ExprCstNode[] 12 | Colon?: IToken[] 13 | rhs?: ExprCstNode[] 14 | } 15 | 16 | export interface ConditionalAndCstNode extends CstNode { 17 | name: 'conditionalAnd' 18 | children: ConditionalAndCstChildren 19 | } 20 | 21 | export type ConditionalAndCstChildren = { 22 | lhs: RelationCstNode[] 23 | LogicalAndOperator?: IToken[] 24 | rhs?: RelationCstNode[] 25 | } 26 | 27 | export interface ConditionalOrCstNode extends CstNode { 28 | name: 'conditionalOr' 29 | children: ConditionalOrCstChildren 30 | } 31 | 32 | export type ConditionalOrCstChildren = { 33 | lhs: ConditionalAndCstNode[] 34 | LogicalOrOperator?: IToken[] 35 | rhs?: ConditionalAndCstNode[] 36 | } 37 | 38 | export interface RelationCstNode extends CstNode { 39 | name: 'relation' 40 | children: RelationCstChildren 41 | } 42 | 43 | export type RelationCstChildren = { 44 | lhs: AdditionCstNode[] 45 | ComparisonOperator?: IToken[] 46 | rhs?: AdditionCstNode[] 47 | } 48 | 49 | export interface AdditionCstNode extends CstNode { 50 | name: 'addition' 51 | children: AdditionCstChildren 52 | } 53 | 54 | export type AdditionCstChildren = { 55 | lhs: MultiplicationCstNode[] 56 | AdditionOperator?: IToken[] 57 | rhs?: MultiplicationCstNode[] 58 | } 59 | 60 | export interface MultiplicationCstNode extends CstNode { 61 | name: 'multiplication' 62 | children: MultiplicationCstChildren 63 | } 64 | 65 | export type MultiplicationCstChildren = { 66 | lhs: UnaryExpressionCstNode[] 67 | MultiplicationOperator?: IToken[] 68 | rhs?: UnaryExpressionCstNode[] 69 | } 70 | 71 | export interface UnaryExpressionCstNode extends CstNode { 72 | name: 'unaryExpression' 73 | children: UnaryExpressionCstChildren 74 | } 75 | 76 | export type UnaryExpressionCstChildren = { 77 | UnaryOperator?: IToken[] 78 | atomicExpression: AtomicExpressionCstNode[] 79 | } 80 | 81 | export interface ParenthesisExpressionCstNode extends CstNode { 82 | name: 'parenthesisExpression' 83 | children: ParenthesisExpressionCstChildren 84 | } 85 | 86 | export type ParenthesisExpressionCstChildren = { 87 | open: IToken[] 88 | expr: ExprCstNode[] 89 | close: IToken[] 90 | } 91 | 92 | export interface ListExpressionCstNode extends CstNode { 93 | name: 'listExpression' 94 | children: ListExpressionCstChildren 95 | } 96 | 97 | export type ListExpressionCstChildren = { 98 | OpenBracket: IToken[] 99 | lhs?: ExprCstNode[] 100 | Comma?: IToken[] 101 | rhs?: ExprCstNode[] 102 | CloseBracket: IToken[] 103 | Index?: IndexExpressionCstNode[] 104 | } 105 | 106 | export interface MapExpressionCstNode extends CstNode { 107 | name: 'mapExpression' 108 | children: MapExpressionCstChildren 109 | } 110 | 111 | export type MapExpressionCstChildren = { 112 | OpenCurlyBracket: IToken[] 113 | keyValues?: MapKeyValuesCstNode[] 114 | CloseCurlyBracket: IToken[] 115 | identifierDotExpression?: IdentifierDotExpressionCstNode[] 116 | identifierIndexExpression?: IndexExpressionCstNode[] 117 | } 118 | 119 | export interface MapKeyValuesCstNode extends CstNode { 120 | name: 'mapKeyValues' 121 | children: MapKeyValuesCstChildren 122 | } 123 | 124 | export type MapKeyValuesCstChildren = { 125 | key: ExprCstNode[] 126 | Colon: IToken[] 127 | value: ExprCstNode[] 128 | Comma?: IToken[] 129 | } 130 | 131 | export interface MacrosExpressionCstNode extends CstNode { 132 | name: 'macrosExpression' 133 | children: MacrosExpressionCstChildren 134 | } 135 | 136 | export type MacrosExpressionCstChildren = { 137 | Identifier: IToken[] 138 | OpenParenthesis: IToken[] 139 | arg?: ExprCstNode[] 140 | Comma?: IToken[] 141 | args?: ExprCstNode[] 142 | CloseParenthesis: IToken[] 143 | } 144 | 145 | export interface IdentifierExpressionCstNode extends CstNode { 146 | name: 'identifierExpression' 147 | children: IdentifierExpressionCstChildren 148 | } 149 | 150 | export type IdentifierExpressionCstChildren = { 151 | Identifier: IToken[] 152 | identifierDotExpression?: IdentifierDotExpressionCstNode[] 153 | identifierIndexExpression?: IndexExpressionCstNode[] 154 | } 155 | 156 | export interface IdentifierDotExpressionCstNode extends CstNode { 157 | name: 'identifierDotExpression' 158 | children: IdentifierDotExpressionCstChildren 159 | } 160 | 161 | export type IdentifierDotExpressionCstChildren = { 162 | Dot: IToken[] 163 | Identifier: IToken[] 164 | } 165 | 166 | export interface IndexExpressionCstNode extends CstNode { 167 | name: 'indexExpression' 168 | children: IndexExpressionCstChildren 169 | } 170 | 171 | export type IndexExpressionCstChildren = { 172 | OpenBracket: IToken[] 173 | expr: ExprCstNode[] 174 | CloseBracket: IToken[] 175 | } 176 | 177 | export interface AtomicExpressionCstNode extends CstNode { 178 | name: 'atomicExpression' 179 | children: AtomicExpressionCstChildren 180 | } 181 | 182 | export type AtomicExpressionCstChildren = { 183 | parenthesisExpression?: ParenthesisExpressionCstNode[] 184 | BooleanLiteral?: IToken[] 185 | Null?: IToken[] 186 | StringLiteral?: IToken[] 187 | Float?: IToken[] 188 | HexUnsignedInteger?: IToken[] 189 | UnsignedInteger?: IToken[] 190 | HexInteger?: IToken[] 191 | Integer?: IToken[] 192 | ReservedIdentifiers?: IToken[] 193 | listExpression?: ListExpressionCstNode[] 194 | mapExpression?: MapExpressionCstNode[] 195 | macrosExpression?: MacrosExpressionCstNode[] 196 | identifierExpression?: IdentifierExpressionCstNode[] 197 | } 198 | 199 | export interface ICstNodeVisitor extends ICstVisitor { 200 | expr(children: ExprCstChildren, param?: IN): OUT 201 | conditionalAnd(children: ConditionalAndCstChildren, param?: IN): OUT 202 | conditionalOr(children: ConditionalOrCstChildren, param?: IN): OUT 203 | relation(children: RelationCstChildren, param?: IN): OUT 204 | addition(children: AdditionCstChildren, param?: IN): OUT 205 | multiplication(children: MultiplicationCstChildren, param?: IN): OUT 206 | unaryExpression(children: UnaryExpressionCstChildren, param?: IN): OUT 207 | parenthesisExpression( 208 | children: ParenthesisExpressionCstChildren, 209 | param?: IN, 210 | ): OUT 211 | listExpression(children: ListExpressionCstChildren, param?: IN): OUT 212 | mapExpression(children: MapExpressionCstChildren, param?: IN): OUT 213 | mapKeyValues(children: MapKeyValuesCstChildren, param?: IN): OUT 214 | macrosExpression(children: MacrosExpressionCstChildren, param?: IN): OUT 215 | identifierExpression( 216 | children: IdentifierExpressionCstChildren, 217 | param?: IN, 218 | ): OUT 219 | identifierDotExpression( 220 | children: IdentifierDotExpressionCstChildren, 221 | param?: IN, 222 | ): OUT 223 | indexExpression(children: IndexExpressionCstChildren, param?: IN): OUT 224 | atomicExpression(children: AtomicExpressionCstChildren, param?: IN): OUT 225 | } 226 | -------------------------------------------------------------------------------- /src/errors/CelEvaluationError.ts: -------------------------------------------------------------------------------- 1 | export class CelEvaluationError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | this.name = 'CelEvaluationError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/errors/CelParseError.ts: -------------------------------------------------------------------------------- 1 | export class CelParseError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | this.name = 'CelParseError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/errors/CelTypeError.ts: -------------------------------------------------------------------------------- 1 | import { Operations, getCelType } from '../helper.js' 2 | 3 | export class CelTypeError extends Error { 4 | /** 5 | * Creates a new CelTypeError for type incompatibilities in operations. 6 | * 7 | * @param operation - The operation being performed 8 | * @param left - The left operand value 9 | * @param right - The right operand value or null for unary operations 10 | * @param customMessage - Optional custom error message to use instead of the default 11 | */ 12 | constructor(operation: Operations | string, left: unknown, right: unknown) { 13 | const leftType = getCelType(left) 14 | const rightType = getCelType(right) 15 | 16 | let message: string 17 | switch (operation) { 18 | case 'unaric operation': 19 | message = 20 | `CelTypeError: Invalid or mixed unary operators ` + 21 | ` applied to ${leftType}` 22 | break 23 | case 'arithmetic negation': 24 | case 'logical negation': 25 | message = 26 | `CelTypeError: ${operation} operation cannot be ` + 27 | `applied to value of type ${leftType}` 28 | break 29 | default: 30 | message = 31 | `CelTypeError: ${operation} operation ` + 32 | `cannot be applied to (${leftType}, ${rightType})` 33 | } 34 | 35 | super(message) 36 | this.name = 'CelTypeError' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import { IToken, tokenMatcher } from 'chevrotain' 2 | import { 3 | Division, 4 | Equals, 5 | GreaterOrEqualThan, 6 | GreaterThan, 7 | In, 8 | LessOrEqualThan, 9 | LessThan, 10 | LogicalAndOperator, 11 | LogicalNotOperator, 12 | LogicalOrOperator, 13 | Minus, 14 | Modulo, 15 | MultiplicationToken, 16 | NotEquals, 17 | Plus, 18 | } from './tokens.js' 19 | import { CelTypeError } from './errors/CelTypeError.js' 20 | import { CelEvaluationError } from './errors/CelEvaluationError.js' 21 | import { 22 | IdentifierDotExpressionCstNode, 23 | IndexExpressionCstNode, 24 | } from './cst-definitions.js' 25 | import { equals } from 'ramda' 26 | 27 | export enum CelType { 28 | int = 'int', 29 | uint = 'uint', 30 | float = 'float', 31 | string = 'string', 32 | bool = 'bool', 33 | null = 'null', 34 | list = 'list', 35 | map = 'map', 36 | } 37 | 38 | const calculableTypes = [CelType.int, CelType.uint, CelType.float] 39 | 40 | export const isCalculable = (value: unknown): value is number => { 41 | const type = getCelType(value) 42 | return calculableTypes.includes(type) 43 | } 44 | 45 | const isInt = (value: unknown): value is number => 46 | getCelType(value) === CelType.int 47 | 48 | const isUint = (value: unknown): value is number => 49 | getCelType(value) === CelType.uint 50 | 51 | const isString = (value: unknown): value is string => 52 | getCelType(value) === CelType.string 53 | 54 | const isArray = (value: unknown): value is unknown[] => 55 | getCelType(value) === CelType.list 56 | 57 | const isBoolean = (value: unknown): value is boolean => 58 | getCelType(value) === CelType.bool 59 | 60 | const isMap = (value: unknown): value is Record => 61 | getCelType(value) === CelType.map 62 | 63 | export const getCelType = (value: unknown): CelType => { 64 | if (value === null) { 65 | return CelType.null 66 | } 67 | 68 | if (typeof value === 'number') { 69 | if (Number.isInteger(value) && value >= 0) { 70 | return CelType.uint 71 | } 72 | 73 | if (Number.isInteger(value) && value <= 0) { 74 | return CelType.int 75 | } 76 | 77 | if (value % 1) { 78 | return CelType.float 79 | } 80 | 81 | throw new Error(`Unknown number type: ${value}`) 82 | } 83 | 84 | if (typeof value === 'string') { 85 | return CelType.string 86 | } 87 | 88 | if (typeof value === 'boolean') { 89 | return CelType.bool 90 | } 91 | 92 | if (Array.isArray(value)) { 93 | return CelType.list 94 | } 95 | 96 | if (typeof value === 'object') { 97 | return CelType.map 98 | } 99 | 100 | throw new Error(`Unknown type: ${typeof value}`) 101 | } 102 | 103 | export enum Operations { 104 | addition = 'addition', 105 | subtraction = 'subtraction', 106 | multiplication = 'multiplication', 107 | division = 'division', 108 | modulo = 'modulo', 109 | logicalAnd = 'logicalAnd', 110 | logicalOr = 'logicalOr', 111 | lessThan = 'lessThan', 112 | lessOrEqualThan = 'lessOrEqualThan', 113 | greaterThan = 'greaterThan', 114 | greaterOrEqualThan = 'greaterOrEqualThan', 115 | equals = 'equals', 116 | notEquals = 'notEquals', 117 | in = 'in', 118 | } 119 | 120 | const additionOperation = (left: unknown, right: unknown) => { 121 | if (isCalculable(left) && isCalculable(right)) { 122 | return left + right 123 | } 124 | 125 | if (isString(left) && isString(right)) { 126 | return left + right 127 | } 128 | 129 | if (isArray(left) && isArray(right)) { 130 | if ( 131 | left.length !== 0 && 132 | right.length !== 0 && 133 | typeof left[0] !== typeof right[0] 134 | ) { 135 | throw new CelTypeError(Operations.addition, left[0], right[0]) 136 | } 137 | return [...left, ...right] 138 | } 139 | 140 | throw new CelTypeError(Operations.addition, left, right) 141 | } 142 | 143 | const subtractionOperation = (left: unknown, right: unknown) => { 144 | if (isCalculable(left) && isCalculable(right)) { 145 | return left - right 146 | } 147 | 148 | throw new CelTypeError(Operations.subtraction, left, right) 149 | } 150 | 151 | const multiplicationOperation = (left: unknown, right: unknown) => { 152 | if (isCalculable(left) && isCalculable(right)) { 153 | return left * right 154 | } 155 | 156 | throw new CelTypeError(Operations.multiplication, left, right) 157 | } 158 | 159 | const divisionOperation = (left: unknown, right: unknown) => { 160 | if (right === 0) { 161 | throw new CelEvaluationError('Division by zero') 162 | } 163 | 164 | // CEL does not support float division 165 | if ((isInt(left) || isUint(left)) && (isInt(right) || isUint(right))) { 166 | return left / right 167 | } 168 | 169 | throw new CelTypeError(Operations.division, left, right) 170 | } 171 | 172 | const moduloOperation = (left: unknown, right: unknown) => { 173 | if (right === 0) { 174 | throw new CelEvaluationError('Modulus by zero') 175 | } 176 | 177 | // CEL does not support float modulus 178 | if ((isInt(left) || isUint(left)) && (isInt(right) || isUint(right))) { 179 | return left % right 180 | } 181 | 182 | throw new CelTypeError(Operations.modulo, left, right) 183 | } 184 | 185 | const logicalAndOperation = (left: unknown, right: unknown) => { 186 | if (isBoolean(left) && isBoolean(right)) { 187 | return left && right 188 | } 189 | 190 | throw new CelTypeError(Operations.logicalAnd, left, right) 191 | } 192 | 193 | const logicalOrOperation = (left: unknown, right: unknown) => { 194 | if (isBoolean(left) && isBoolean(right)) { 195 | return left || right 196 | } 197 | 198 | throw new CelTypeError(Operations.logicalOr, left, right) 199 | } 200 | 201 | const comparisonInOperation = (left: unknown, right: unknown) => { 202 | if (isArray(right)) { 203 | return right.includes(left) 204 | } 205 | if (isMap(right)) { 206 | return Object.keys(right).includes(left as string) 207 | } 208 | throw new CelTypeError(Operations.in, left, right) 209 | } 210 | 211 | const comparisonOperation = ( 212 | operation: Operations, 213 | left: unknown, 214 | right: unknown, 215 | ) => { 216 | if ( 217 | (isCalculable(left) && isCalculable(right)) || 218 | (isString(left) && isString(right)) 219 | ) { 220 | switch (operation) { 221 | case Operations.lessThan: 222 | return left < right 223 | case Operations.lessOrEqualThan: 224 | return left <= right 225 | case Operations.greaterThan: 226 | return left > right 227 | case Operations.greaterOrEqualThan: 228 | return left >= right 229 | } 230 | } 231 | 232 | if (operation === Operations.equals) { 233 | return equals(left, right) 234 | } 235 | 236 | if (operation === Operations.notEquals) { 237 | return !equals(left, right) 238 | } 239 | 240 | if (operation === Operations.in) { 241 | return comparisonInOperation(left, right) 242 | } 243 | 244 | throw new CelTypeError(operation, left, right) 245 | } 246 | 247 | export const getResult = (operator: IToken, left: unknown, right: unknown) => { 248 | switch (true) { 249 | case tokenMatcher(operator, Plus): 250 | return additionOperation(left, right) 251 | case tokenMatcher(operator, Minus): 252 | return subtractionOperation(left, right) 253 | case tokenMatcher(operator, MultiplicationToken): 254 | return multiplicationOperation(left, right) 255 | case tokenMatcher(operator, Division): 256 | return divisionOperation(left, right) 257 | case tokenMatcher(operator, Modulo): 258 | return moduloOperation(left, right) 259 | case tokenMatcher(operator, LogicalAndOperator): 260 | return logicalAndOperation(left, right) 261 | case tokenMatcher(operator, LogicalOrOperator): 262 | return logicalOrOperation(left, right) 263 | case tokenMatcher(operator, LessThan): 264 | return comparisonOperation(Operations.lessThan, left, right) 265 | case tokenMatcher(operator, LessOrEqualThan): 266 | return comparisonOperation(Operations.lessOrEqualThan, left, right) 267 | case tokenMatcher(operator, GreaterThan): 268 | return comparisonOperation(Operations.greaterThan, left, right) 269 | case tokenMatcher(operator, GreaterOrEqualThan): 270 | return comparisonOperation(Operations.greaterOrEqualThan, left, right) 271 | case tokenMatcher(operator, Equals): 272 | return comparisonOperation(Operations.equals, left, right) 273 | case tokenMatcher(operator, NotEquals): 274 | return comparisonOperation(Operations.notEquals, left, right) 275 | case tokenMatcher(operator, In): 276 | return comparisonOperation(Operations.in, left, right) 277 | default: 278 | throw new Error('Operator not recognized') 279 | } 280 | } 281 | 282 | /** 283 | * Handles logical negation for a value. 284 | * 285 | * @param operand The value to negate 286 | * @param isEvenOperators Whether there's an even number of operators (affects result) 287 | * @returns Negated value 288 | */ 289 | function handleLogicalNegation( 290 | operand: unknown, 291 | isEvenOperators: boolean, 292 | ): boolean { 293 | if (operand === null) { 294 | return !isEvenOperators // Odd number gives true, even gives false 295 | } 296 | 297 | if (!isBoolean(operand)) { 298 | throw new CelTypeError('logical negation', operand, null) 299 | } 300 | 301 | return isEvenOperators ? (operand as boolean) : !operand 302 | } 303 | 304 | /** 305 | * Handles arithmetic negation for a value. 306 | * 307 | * @param operand The value to negate 308 | * @param isEvenOperators Whether there's an even number of operators (affects result) 309 | * @returns Negated value 310 | */ 311 | function handleArithmeticNegation( 312 | operand: unknown, 313 | isEvenOperators: boolean, 314 | ): number { 315 | if (!isCalculable(operand)) { 316 | throw new CelTypeError('arithmetic negation', operand, null) 317 | } 318 | 319 | // Handle -0 edge case by returning +0 320 | if (!isEvenOperators && operand === 0) { 321 | return 0 322 | } 323 | 324 | return isEvenOperators ? (operand as number) : -(operand as number) 325 | } 326 | 327 | /** 328 | * Applies unary operators to an operand according to CEL semantics. 329 | * 330 | * @param operators - Array of unary operator tokens to apply 331 | * @param operand - The value to apply the operators to 332 | * @returns The result of applying the operators to the operand 333 | * @throws CelTypeError if the operators cannot be applied to the operand type 334 | */ 335 | export const getUnaryResult = (operators: IToken[], operand: unknown) => { 336 | // If no operators, return the operand unchanged 337 | if (operators.length === 0) { 338 | return operand 339 | } 340 | 341 | const isEvenOperators = operators.length % 2 === 0 342 | 343 | // Check if all operators are logical negation 344 | if (operators.every((op) => tokenMatcher(op, LogicalNotOperator))) { 345 | return handleLogicalNegation(operand, isEvenOperators) 346 | } 347 | 348 | // Check if all operators are arithmetic negation 349 | if (operators.every((op) => tokenMatcher(op, Minus))) { 350 | return handleArithmeticNegation(operand, isEvenOperators) 351 | } 352 | 353 | // Mixed or unsupported operators 354 | throw new CelTypeError('unary operation', operand, null) 355 | } 356 | 357 | export const getPosition = ( 358 | ctx: IdentifierDotExpressionCstNode | IndexExpressionCstNode, 359 | ) => { 360 | if (ctx.name === 'identifierDotExpression') { 361 | return ctx.children.Dot[0].startOffset 362 | } 363 | 364 | return ctx.children.OpenBracket[0].startOffset 365 | } 366 | 367 | export const size = (arr: unknown) => { 368 | if (isString(arr) || isArray(arr)) { 369 | return arr.length 370 | } 371 | 372 | if (isMap(arr)) { 373 | return Object.keys(arr).length 374 | } 375 | 376 | throw new CelEvaluationError(`invalid_argument: ${arr}`) 377 | } 378 | 379 | /** 380 | * Macro definition for the CEL has() function that checks if a path exists in an object. 381 | * 382 | * @param path - The path to check for existence 383 | * @returns boolean - True if the path exists (is not undefined), false otherwise 384 | * 385 | * @example 386 | * has(obj.field) // returns true if field exists on obj 387 | */ 388 | export const has = (path: unknown): boolean => { 389 | // If the path itself is undefined, it means the field/index doesn't exist 390 | return path !== undefined 391 | } 392 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { CelParseError } from './errors/CelParseError.js' 2 | export { CelEvaluationError } from './errors/CelEvaluationError.js' 3 | export { CelTypeError } from './errors/CelTypeError.js' 4 | 5 | export { Failure, Success, ParseResult, evaluate, parse } from './lib.js' 6 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import { CELLexer } from './tokens.js' 2 | import { CelParser } from './parser.js' 3 | import { CelVisitor } from './visitor.js' 4 | import { CstNode } from 'chevrotain' 5 | import { CelParseError } from './errors/CelParseError.js' 6 | 7 | export { CelParseError } from './errors/CelParseError.js' 8 | export { CelEvaluationError } from './errors/CelEvaluationError.js' 9 | export { CelTypeError } from './errors/CelTypeError.js' 10 | 11 | const parserInstance = new CelParser() 12 | 13 | export type Success = { 14 | isSuccess: true 15 | cst: CstNode 16 | } 17 | 18 | export type Failure = { 19 | isSuccess: false 20 | errors: string[] 21 | } 22 | 23 | export type ParseResult = Success | Failure 24 | 25 | export function parse(expression: string): ParseResult { 26 | const lexResult = CELLexer.tokenize(expression) 27 | parserInstance.input = lexResult.tokens 28 | const cst = parserInstance.expr() 29 | 30 | if (parserInstance.errors.length > 0) { 31 | return { 32 | isSuccess: false, 33 | errors: parserInstance.errors.map((e) => e.message), 34 | } 35 | } 36 | 37 | return { isSuccess: true, cst } 38 | } 39 | 40 | export function evaluate( 41 | expression: string | CstNode, 42 | context?: Record, 43 | functions?: Record, 44 | ) { 45 | const result = 46 | typeof expression === 'string' 47 | ? parse(expression) 48 | : { isSuccess: true, cst: expression } 49 | const toAstVisitorInstance = new CelVisitor(context, functions) 50 | 51 | if (!result.isSuccess) { 52 | throw new CelParseError( 53 | 'Given string is not a valid CEL expression: ' + result.errors.join(', '), 54 | ) 55 | } 56 | 57 | return toAstVisitorInstance.visit(result.cst) as unknown 58 | } 59 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { CstParser } from 'chevrotain' 2 | import { 3 | HexUnsignedInteger, 4 | HexInteger, 5 | UnsignedInteger, 6 | Integer, 7 | allTokens, 8 | AdditionOperator, 9 | MultiplicationOperator, 10 | Identifier, 11 | BooleanLiteral, 12 | Null, 13 | OpenParenthesis, 14 | CloseParenthesis, 15 | StringLiteral, 16 | Float, 17 | LogicalAndOperator, 18 | LogicalOrOperator, 19 | ComparisonOperator, 20 | UnaryOperator, 21 | Dot, 22 | CloseBracket, 23 | OpenBracket, 24 | Comma, 25 | OpenCurlyBracket, 26 | CloseCurlyBracket, 27 | Colon, 28 | QuestionMark, 29 | } from './tokens.js' 30 | 31 | export class CelParser extends CstParser { 32 | constructor() { 33 | super(allTokens) 34 | this.performSelfAnalysis() 35 | } 36 | 37 | public expr = this.RULE('expr', () => { 38 | this.SUBRULE(this.conditionalOr, { LABEL: 'conditionalOr' }) 39 | this.OPTION(() => { 40 | this.CONSUME(QuestionMark) 41 | this.SUBRULE(this.expr, { LABEL: 'lhs' }) 42 | this.CONSUME(Colon) 43 | this.SUBRULE2(this.expr, { LABEL: 'rhs' }) 44 | }) 45 | }) 46 | 47 | private conditionalAnd = this.RULE('conditionalAnd', () => { 48 | this.SUBRULE(this.relation, { LABEL: 'lhs' }) 49 | this.MANY(() => { 50 | this.CONSUME(LogicalAndOperator) 51 | this.SUBRULE2(this.relation, { LABEL: 'rhs' }) 52 | }) 53 | }) 54 | 55 | private conditionalOr = this.RULE('conditionalOr', () => { 56 | this.SUBRULE(this.conditionalAnd, { LABEL: 'lhs' }) 57 | this.MANY(() => { 58 | this.CONSUME(LogicalOrOperator) 59 | this.SUBRULE2(this.conditionalAnd, { LABEL: 'rhs' }) 60 | }) 61 | }) 62 | 63 | private relation = this.RULE('relation', () => { 64 | this.SUBRULE(this.addition, { LABEL: 'lhs' }) 65 | this.OPTION(() => { 66 | this.CONSUME(ComparisonOperator) 67 | this.SUBRULE2(this.addition, { LABEL: 'rhs' }) 68 | }) 69 | }) 70 | 71 | private addition = this.RULE('addition', () => { 72 | this.SUBRULE(this.multiplication, { LABEL: 'lhs' }) 73 | this.MANY(() => { 74 | this.CONSUME(AdditionOperator) 75 | this.SUBRULE2(this.multiplication, { LABEL: 'rhs' }) 76 | }) 77 | }) 78 | 79 | private multiplication = this.RULE('multiplication', () => { 80 | this.SUBRULE(this.unaryExpression, { LABEL: 'lhs' }) 81 | this.MANY(() => { 82 | this.CONSUME(MultiplicationOperator) 83 | this.SUBRULE2(this.unaryExpression, { LABEL: 'rhs' }) 84 | }) 85 | }) 86 | 87 | private unaryExpression = this.RULE('unaryExpression', () => { 88 | this.MANY(() => { 89 | this.CONSUME(UnaryOperator) 90 | }) 91 | this.SUBRULE(this.atomicExpression) 92 | }) 93 | 94 | private parenthesisExpression = this.RULE('parenthesisExpression', () => { 95 | this.CONSUME(OpenParenthesis, { LABEL: 'open' }) 96 | this.SUBRULE(this.expr) 97 | this.CONSUME(CloseParenthesis, { LABEL: 'close' }) 98 | }) 99 | 100 | private listExpression = this.RULE('listExpression', () => { 101 | this.CONSUME(OpenBracket) 102 | this.OPTION(() => { 103 | this.SUBRULE(this.expr, { LABEL: 'lhs' }) 104 | this.MANY(() => { 105 | this.CONSUME(Comma) 106 | this.SUBRULE2(this.expr, { LABEL: 'rhs' }) 107 | }) 108 | }) 109 | this.CONSUME(CloseBracket) 110 | this.OPTION2(() => { 111 | this.SUBRULE(this.indexExpression, { LABEL: 'Index' }) 112 | }) 113 | }) 114 | 115 | private mapExpression = this.RULE('mapExpression', () => { 116 | this.CONSUME(OpenCurlyBracket) 117 | this.MANY(() => { 118 | this.SUBRULE(this.mapKeyValues, { LABEL: 'keyValues' }) 119 | }) 120 | this.CONSUME(CloseCurlyBracket) 121 | this.MANY2(() => { 122 | this.OR([ 123 | { ALT: () => this.SUBRULE(this.identifierDotExpression) }, 124 | { 125 | ALT: () => 126 | this.SUBRULE(this.indexExpression, { 127 | LABEL: 'identifierIndexExpression', 128 | }), 129 | }, 130 | ]) 131 | }) 132 | }) 133 | 134 | private mapKeyValues = this.RULE('mapKeyValues', () => { 135 | this.SUBRULE(this.expr, { LABEL: 'key' }) 136 | this.CONSUME(Colon) 137 | this.SUBRULE2(this.expr, { LABEL: 'value' }) 138 | this.OPTION(() => { 139 | this.CONSUME(Comma) 140 | }) 141 | }) 142 | 143 | private macrosExpression = this.RULE('macrosExpression', () => { 144 | this.CONSUME(Identifier) 145 | this.CONSUME(OpenParenthesis) 146 | this.OPTION(() => { 147 | this.SUBRULE(this.expr, { LABEL: 'arg' }) 148 | this.MANY(() => { 149 | this.CONSUME(Comma) 150 | this.SUBRULE2(this.expr, { LABEL: 'args' }) 151 | }) 152 | }) 153 | this.CONSUME(CloseParenthesis) 154 | }) 155 | 156 | private identifierExpression = this.RULE('identifierExpression', () => { 157 | this.CONSUME(Identifier) 158 | this.MANY(() => { 159 | this.OR([ 160 | { ALT: () => this.SUBRULE(this.identifierDotExpression) }, 161 | { 162 | ALT: () => 163 | this.SUBRULE(this.indexExpression, { 164 | LABEL: 'identifierIndexExpression', 165 | }), 166 | }, 167 | ]) 168 | }) 169 | }) 170 | 171 | private identifierDotExpression = this.RULE('identifierDotExpression', () => { 172 | this.CONSUME(Dot) 173 | this.CONSUME(Identifier) 174 | }) 175 | 176 | private indexExpression = this.RULE('indexExpression', () => { 177 | this.CONSUME(OpenBracket) 178 | this.SUBRULE(this.expr) 179 | this.CONSUME(CloseBracket) 180 | }) 181 | 182 | private atomicExpression = this.RULE('atomicExpression', () => { 183 | this.OR([ 184 | { ALT: () => this.SUBRULE(this.parenthesisExpression) }, 185 | { ALT: () => this.CONSUME(BooleanLiteral) }, 186 | { ALT: () => this.CONSUME(Null) }, 187 | { ALT: () => this.CONSUME(StringLiteral) }, 188 | { ALT: () => this.CONSUME(Float) }, 189 | { ALT: () => this.CONSUME(HexUnsignedInteger) }, 190 | { ALT: () => this.CONSUME(HexInteger) }, 191 | { ALT: () => this.CONSUME(UnsignedInteger) }, 192 | { ALT: () => this.CONSUME(Integer) }, 193 | { ALT: () => this.SUBRULE(this.listExpression) }, 194 | { ALT: () => this.SUBRULE(this.mapExpression) }, 195 | { ALT: () => this.SUBRULE(this.macrosExpression) }, 196 | { ALT: () => this.SUBRULE(this.identifierExpression) }, 197 | ]) 198 | }) 199 | } 200 | -------------------------------------------------------------------------------- /src/spec/addition.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { evaluate } from '..' 4 | import { CelTypeError } from '../errors/CelTypeError' 5 | import { Operations } from '../helper' 6 | 7 | describe('addition', () => { 8 | it('should evaluate addition', () => { 9 | const expr = '1 + 1' 10 | 11 | const result = evaluate(expr) 12 | 13 | expect(result).toBe(2) 14 | }) 15 | 16 | it('should evaluate subtraction', () => { 17 | const expr = '1 - 1' 18 | 19 | const result = evaluate(expr) 20 | 21 | expect(result).toBe(0) 22 | }) 23 | 24 | it('should evaluate addition with multiple terms', () => { 25 | const expr = '1 + 1 + 1' 26 | 27 | const result = evaluate(expr) 28 | 29 | expect(result).toBe(3) 30 | }) 31 | 32 | it('should evaluate addition with multiple terms with different signs', () => { 33 | const expr = '1 + 1 - 1' 34 | 35 | const result = evaluate(expr) 36 | 37 | expect(result).toBe(1) 38 | }) 39 | 40 | it('should evaluate float addition', () => { 41 | const expr = '0.333 + 0.333' 42 | 43 | const result = evaluate(expr) 44 | 45 | expect(result).toBe(0.666) 46 | }) 47 | 48 | it('should concatenate strings', () => { 49 | const expr = '"a" + "b"' 50 | 51 | const result = evaluate(expr) 52 | 53 | expect(result).toBe('ab') 54 | }) 55 | 56 | describe('should throw when', () => { 57 | it('is a boolean', () => { 58 | const expr = 'true + 1' 59 | 60 | const result = () => evaluate(expr) 61 | 62 | expect(result).toThrow(new CelTypeError(Operations.addition, true, 1)) 63 | }) 64 | 65 | it('is a null', () => { 66 | const expr = 'null + 1' 67 | 68 | const result = () => evaluate(expr) 69 | 70 | expect(result).toThrow(new CelTypeError(Operations.addition, null, 1)) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /src/spec/atomic-expression.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { evaluate } from '..' 4 | import { CelParseError } from '../errors/CelParseError' 5 | 6 | describe('atomic expressions', () => { 7 | it('should evaluate a number', () => { 8 | const expr = '1' 9 | 10 | const result = evaluate(expr) 11 | 12 | expect(result).toBe(1) 13 | }) 14 | 15 | it('should evaluate a hexadecimal number', () => { 16 | const expr = '0xA' 17 | 18 | const result = evaluate(expr) 19 | 20 | expect(result).toBe(10) 21 | }) 22 | 23 | it('should evaluate a true boolean literal', () => { 24 | const expr = 'true' 25 | 26 | const result = evaluate(expr) 27 | 28 | expect(result).toBe(true) 29 | }) 30 | 31 | it('should evaluate a false boolean literal', () => { 32 | const expr = 'false' 33 | 34 | const result = evaluate(expr) 35 | 36 | expect(result).toBe(false) 37 | }) 38 | 39 | it('should evaluate null literal', () => { 40 | const expr = 'null' 41 | 42 | const result = evaluate(expr) 43 | 44 | expect(result).toBeNull() 45 | }) 46 | 47 | it('should evaluate a double-quoted string literal', () => { 48 | const expr = '"foo"' 49 | 50 | const result = evaluate(expr) 51 | 52 | expect(result).toBe('foo') 53 | }) 54 | 55 | it('should evaluate a single-quoted string literal', () => { 56 | const expr = "'foo'" 57 | 58 | const result = evaluate(expr) 59 | 60 | expect(result).toBe('foo') 61 | }) 62 | 63 | it('should not parse a double-quoted string with a newline', () => { 64 | const expr = `"fo 65 | o"` 66 | 67 | const result = () => evaluate(expr) 68 | 69 | expect(result).toThrow( 70 | new CelParseError( 71 | 'Given string is not a valid CEL expression: Redundant input, expecting EOF but found: o', 72 | ), 73 | ) 74 | }) 75 | 76 | it('should not parse a single-quoted string with a newline', () => { 77 | const expr = `'fo 78 | o'` 79 | 80 | const result = () => evaluate(expr) 81 | 82 | expect(result).toThrow( 83 | new CelParseError( 84 | 'Given string is not a valid CEL expression: Redundant input, expecting EOF but found: o', 85 | ), 86 | ) 87 | }) 88 | 89 | it('should evaluate a float', () => { 90 | const expr = '1.2' 91 | 92 | const result = evaluate(expr) 93 | 94 | expect(result).toBe(1.2) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /src/spec/comments.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { evaluate } from '../index.js' 3 | 4 | describe('Comments', () => { 5 | it('should ignore single-line comments', () => { 6 | expect(evaluate('1 + 2 // This is a comment')).toBe(3) 7 | }) 8 | 9 | it('should ignore comments at the beginning of the line', () => { 10 | expect(evaluate('// This is a comment\n1 + 2')).toBe(3) 11 | }) 12 | 13 | it('should allow comments between and after operators', () => { 14 | expect( 15 | evaluate('1 +// First comment\n// Second comment\n2 // Last comment'), 16 | ).toBe(3) 17 | }) 18 | 19 | it('multi-line comments', () => { 20 | expect( 21 | evaluate(` 22 | "foo" + // some comment 23 | "bar" 24 | `), 25 | ).toBe('foobar') 26 | }) 27 | 28 | it('should not parse // inside a string literal as a comment', () => { 29 | const result = evaluate('"This contains // but is not a comment"') 30 | expect(result).toBe('This contains // but is not a comment') 31 | }) 32 | 33 | it('should support complex expressions with comments', () => { 34 | expect( 35 | evaluate(`true ? "yes" // some comment\n : \n// other comment\n "no"`), 36 | ).toBe('yes') 37 | expect( 38 | evaluate(`false ? "yes" // some comment\n : \n// other comment\n "no"`), 39 | ).toBe('no') 40 | }) 41 | 42 | it('should support comments with special characters', () => { 43 | const result = evaluate( 44 | '1 + 2 // Special chars: !@#$%^&*()_+-=[]{}|;:\'",.<>/?`~😃', 45 | ) 46 | expect(result).toBe(3) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/spec/comparisons.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { CelTypeError, evaluate } from '..' 4 | import { Operations } from '../helper' 5 | 6 | describe('comparisons', () => { 7 | it('should evaluate greater than operator', () => { 8 | const expr = '2 > 1' 9 | 10 | const result = evaluate(expr) 11 | 12 | expect(result).toBe(true) 13 | }) 14 | 15 | it('should evaluate less than operator', () => { 16 | const expr = '2 < 1' 17 | 18 | const result = evaluate(expr) 19 | 20 | expect(result).toBe(false) 21 | }) 22 | 23 | it('should evaluate greater than or equal operator', () => { 24 | const expr = '1 >= 1' 25 | const result = evaluate(expr) 26 | 27 | expect(result).toBe(true) 28 | }) 29 | 30 | it('should evaluate less than or equal operator', () => { 31 | const expr = '1 <= 1' 32 | 33 | const result = evaluate(expr) 34 | 35 | expect(result).toBe(true) 36 | }) 37 | 38 | it('should evaluate equal operator', () => { 39 | const expr = '1 == 1' 40 | 41 | const result = evaluate(expr) 42 | 43 | expect(result).toBe(true) 44 | }) 45 | 46 | it('should evaluate not equal operator', () => { 47 | const expr = '1 != 1' 48 | 49 | const result = evaluate(expr) 50 | 51 | expect(result).toBe(false) 52 | }) 53 | 54 | describe('in', () => { 55 | it('should return false for element in empty list', () => { 56 | const expr = '1 in []' 57 | 58 | const result = evaluate(expr) 59 | 60 | expect(result).toBe(false) 61 | }) 62 | 63 | it('should return true for element the only element on the list', () => { 64 | const expr = '1 in [1]' 65 | 66 | const result = evaluate(expr) 67 | 68 | expect(result).toBe(true) 69 | }) 70 | 71 | it('should return true for element the first element of the list', () => { 72 | const expr = '"first" in ["first", "second", "third"]' 73 | 74 | const result = evaluate(expr) 75 | 76 | expect(result).toBe(true) 77 | }) 78 | 79 | it('should thrown an error if used on something else than list', () => { 80 | const expr = '"a" in "asd"' 81 | 82 | const result = () => evaluate(expr) 83 | 84 | expect(result).toThrow( 85 | new CelTypeError(Operations.in, 'string', 'string'), 86 | ) 87 | }) 88 | 89 | it.each(['"install"', '"inin"', '"stalin"'])( 90 | 'should not be recognized in string', 91 | (aString) => { 92 | const expr = aString 93 | 94 | const result = evaluate(expr) 95 | 96 | const expected = aString.slice(1, -1) // remove quotes 97 | 98 | expect(result).toBe(expected) 99 | }, 100 | ) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/spec/conditional-ternary.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { evaluate } from '../index.js' 3 | 4 | describe('Ternary Operator', () => { 5 | it('should handle simple ternary expressions', () => { 6 | expect(evaluate('true ? 1 : 2')).toBe(1) 7 | expect(evaluate('false ? 1 : 2')).toBe(2) 8 | }) 9 | 10 | it('should handle complex conditions in ternary expressions', () => { 11 | expect(evaluate('1 < 2 ? "yes" : "no"')).toBe('yes') 12 | expect(evaluate('2 < 1 ? "yes" : "no"')).toBe('no') 13 | expect(evaluate('1 + 1 == 2 ? "correct" : "incorrect"')).toBe('correct') 14 | }) 15 | 16 | it('should handle nested ternary expressions - true case', () => { 17 | expect(evaluate('true ? (true ? 1 : 2) : 3')).toBe(1) 18 | expect(evaluate('true ? (false ? 1 : 2) : 3')).toBe(2) 19 | }) 20 | 21 | it('should handle nested ternary expressions - false case', () => { 22 | expect(evaluate('false ? 1 : (true ? 2 : 3)')).toBe(2) 23 | expect(evaluate('false ? 1 : (false ? 2 : 3)')).toBe(3) 24 | }) 25 | 26 | it('should handle complex expressions in all parts of the ternary', () => { 27 | expect(evaluate('1 + 1 == 2 ? 3 * 2 : 5 * 2')).toBe(6) 28 | expect(evaluate('1 + 1 != 2 ? 3 * 2 : 5 * 2')).toBe(10) 29 | }) 30 | 31 | it('should work with variables', () => { 32 | expect( 33 | evaluate('user.admin ? "Admin" : "User"', { user: { admin: true } }), 34 | ).toBe('Admin') 35 | expect( 36 | evaluate('user.admin ? "Admin" : "User"', { user: { admin: false } }), 37 | ).toBe('User') 38 | }) 39 | 40 | it('should support logical operators in condition', () => { 41 | expect(evaluate('true && true ? "yes" : "no"')).toBe('yes') 42 | expect(evaluate('true && false ? "yes" : "no"')).toBe('no') 43 | expect(evaluate('false || true ? "yes" : "no"')).toBe('yes') 44 | expect(evaluate('false || false ? "yes" : "no"')).toBe('no') 45 | }) 46 | 47 | it('should handle null conditions properly', () => { 48 | expect(evaluate('null ? "true" : "false"')).toBe('false') 49 | expect(evaluate('!null ? "true" : "false"')).toBe('true') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/spec/hex-integer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | import { evaluate } from '..' 3 | 4 | describe('hexadecimal integers', () => { 5 | it('should evaluate a simple hex integer', () => { 6 | expect(evaluate('0xA')).toBe(10) 7 | }) 8 | 9 | it('should evaluate a hex integer with lowercase', () => { 10 | expect(evaluate('0xabc')).toBe(2748) 11 | }) 12 | 13 | it('should handle hex integers in comparison operations', () => { 14 | expect(evaluate('0xA > 0x5')).toBe(true) 15 | }) 16 | 17 | it('should handle hex integers in lists', () => { 18 | expect(evaluate('[0xA, 0x14, 0x1E][1]')).toBe(20) // 0x14 = 20 19 | }) 20 | 21 | it('should evaluate hex unsigned integers with uppercase suffix', () => { 22 | expect(evaluate('0xAU')).toBe(10) 23 | }) 24 | 25 | it('should handle hex unsigned integers in arithmetic operations', () => { 26 | expect(evaluate('0xAu + 10')).toBe(20) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/spec/identifiers.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | import { evaluate } from '..' 3 | 4 | describe('identifiers', () => { 5 | describe('dot notation', () => { 6 | it('should evaluate single identifier', () => { 7 | const expr = 'a' 8 | const context = { a: 2 } 9 | 10 | const result = evaluate(expr, context) 11 | 12 | expect(result).toBe(2) 13 | }) 14 | 15 | it('should evaluate nested identifiers', () => { 16 | const expr = 'a.b.c' 17 | const context = { a: { b: { c: 2 } } } 18 | 19 | const result = evaluate(expr, context) 20 | 21 | expect(result).toBe(2) 22 | }) 23 | }) 24 | 25 | describe('index notation', () => { 26 | it('should evaluate single identifier', () => { 27 | const expr = 'a["b"]' 28 | const context = { a: { b: 2 } } 29 | 30 | const result = evaluate(expr, context) 31 | 32 | expect(result).toBe(2) 33 | }) 34 | 35 | it('should evaluate nested identifiers', () => { 36 | const expr = 'a["b"]["c"]' 37 | const context = { a: { b: { c: 2 } } } 38 | 39 | const result = evaluate(expr, context) 40 | 41 | expect(result).toBe(2) 42 | }) 43 | }) 44 | 45 | it('should evaluate identifiers - mixed', () => { 46 | const expr = 'a.b["c"].d' 47 | 48 | const context = { a: { b: { c: { d: 2 } } } } 49 | 50 | const result = evaluate(expr, context) 51 | 52 | expect(result).toBe(2) 53 | }) 54 | 55 | it('should evaluate identifiers - multiple usage of the same identifiers', () => { 56 | const expr = 'a.b["c"].d + a.b["c"].d' 57 | 58 | const context = { a: { b: { c: { d: 2 } } } } 59 | 60 | const result = evaluate(expr, context) 61 | 62 | expect(result).toBe(4) 63 | }) 64 | 65 | it('should return object if identifier is object', () => { 66 | const expr = 'a' 67 | const context = { a: { b: 2 } } 68 | 69 | const result = evaluate(expr, context) 70 | 71 | expect(result).toStrictEqual({ b: 2 }) 72 | }) 73 | 74 | it('should throw if access to identifier but w/o context', () => { 75 | const expr = 'a' 76 | 77 | const result = () => evaluate(expr) 78 | 79 | expect(result).toThrow(`Identifier "a" not found, no context passed`) 80 | }) 81 | 82 | it('should throw if identifier is not in context', () => { 83 | const expr = 'a' 84 | 85 | const result = () => evaluate(expr, { b: 2 }) 86 | 87 | expect(result).toThrow(`Identifier "a" not found in context: {"b":2}`) 88 | }) 89 | 90 | describe('reserved identifiers', () => { 91 | it('should throw if reserved identifier is used', () => { 92 | const expr = 'as' 93 | 94 | const result = () => evaluate(expr) 95 | 96 | expect(result).toThrow( 97 | `Detected reserved identifier. This is not allowed`, 98 | ) 99 | }) 100 | 101 | it('should throw if reserved is used as a statment', () => { 102 | const expr = 'as + 1' 103 | 104 | const result = () => evaluate(expr) 105 | 106 | expect(result).toThrow( 107 | `Detected reserved identifier. This is not allowed`, 108 | ) 109 | }) 110 | 111 | it('should not throw if reserved is at the start of the identifier', () => { 112 | const expr = 'as.b' 113 | 114 | const result = evaluate(expr, { as: { b: 2 } }) 115 | 116 | expect(result).toBe(2) 117 | }) 118 | 119 | it('should not throw if reserved is at the start of the identifier', () => { 120 | const expr = 'b.as' 121 | 122 | const result = evaluate(expr, { b: { as: 2 } }) 123 | 124 | expect(result).toBe(2) 125 | }) 126 | 127 | it('should not throw if reserved is start of an identifire string', () => { 128 | const expr = 'asx.b' 129 | 130 | const result = evaluate(expr, { asx: { b: 2 } }) 131 | 132 | expect(result).toBe(2) 133 | }) 134 | 135 | it('should not throw if reserved is in the middle of an identifire string', () => { 136 | const expr = 'xasx.b' 137 | 138 | const result = evaluate(expr, { xasx: { b: 2 } }) 139 | 140 | expect(result).toBe(2) 141 | }) 142 | 143 | it('should not throw if reserved is at the end of an identifire string', () => { 144 | const expr = 'xas.b' 145 | 146 | const result = evaluate(expr, { xas: { b: 2 } }) 147 | 148 | expect(result).toBe(2) 149 | }) 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /src/spec/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { Success, evaluate, parse } from '..' 4 | import { CelParseError } from '../errors/CelParseError' 5 | 6 | describe('index.ts', () => { 7 | describe('parse', () => { 8 | it('should return isSuccess true and cst if given string is valid CEL string', () => { 9 | const expr = '1' 10 | 11 | const result = parse(expr) 12 | 13 | expect(result).toStrictEqual({ 14 | isSuccess: true, 15 | cst: expect.any(Object), 16 | }) 17 | }) 18 | 19 | it('should return isSuccess false and errors if given string is not valid CEL string', () => { 20 | const expr = '1 +' 21 | 22 | const result = parse(expr) 23 | 24 | expect(result).toStrictEqual({ 25 | isSuccess: false, 26 | errors: expect.any(Array), 27 | }) 28 | }) 29 | }) 30 | 31 | describe('evaluate', () => { 32 | it('should throw an error if given string is not valid CEL expression', () => { 33 | const expr = '1 + ' 34 | 35 | const result = () => evaluate(expr) 36 | 37 | expect(result).toThrow(CelParseError) 38 | expect(result).toThrow('Given string is not a valid CEL expression: ') 39 | }) 40 | }) 41 | 42 | it('should be able to reuse parse results in evaluate', () => { 43 | const expr = '1' 44 | 45 | const result = parse(expr) 46 | 47 | expect(() => evaluate((result as Success).cst)).not.toThrow() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/spec/lists.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { CelEvaluationError, CelTypeError, evaluate } from '..' 4 | import { Operations } from '../helper' 5 | 6 | describe('lists expressions', () => { 7 | describe('literals', () => { 8 | it('should create a empty list', () => { 9 | const expr = '[]' 10 | 11 | const result = evaluate(expr) 12 | 13 | expect(result).toStrictEqual([]) 14 | }) 15 | 16 | it('should create a one element list', () => { 17 | const expr = '[1]' 18 | 19 | const result = evaluate(expr) 20 | 21 | expect(result).toStrictEqual([1]) 22 | }) 23 | 24 | it('should create a many element list', () => { 25 | const expr = '[1, 2, 3]' 26 | 27 | const result = evaluate(expr) 28 | 29 | expect(result).toStrictEqual([1, 2, 3]) 30 | }) 31 | 32 | // Shall we throw an error if lists have different types? 33 | // The original implementation does that if we put literals 34 | // but no in case of context usage. So for now we will not throw an error 35 | it.todo('should throw an error if lists have different types', () => { 36 | const expr = '[1, true]' 37 | 38 | const result = () => evaluate(expr) 39 | 40 | expect(result).toThrow(new CelTypeError(Operations.logicalAnd, true, 1)) 41 | }) 42 | }) 43 | 44 | describe('lists', () => { 45 | it('should create a one element list', () => { 46 | const expr = '[[1]]' 47 | 48 | const result = evaluate(expr) 49 | 50 | expect(result).toStrictEqual([[1]]) 51 | }) 52 | 53 | it('should create a many element list', () => { 54 | const expr = '[[1], [2], [3]]' 55 | 56 | const result = evaluate(expr) 57 | 58 | expect(result).toStrictEqual([[1], [2], [3]]) 59 | }) 60 | }) 61 | 62 | describe('index', () => { 63 | it('should access list by index', () => { 64 | const expr = 'a[1]' 65 | 66 | const context = { a: [1, 2, 3] } 67 | 68 | const result = evaluate(expr, context) 69 | 70 | expect(result).toBe(2) 71 | }) 72 | 73 | it('should access list by index if literal used', () => { 74 | const expr = '[1, 2, 3][1]' 75 | 76 | const context = { a: [1, 2, 3] } 77 | 78 | const result = evaluate(expr, context) 79 | 80 | expect(result).toBe(2) 81 | }) 82 | 83 | it('should access list on zero index', () => { 84 | const expr = '[7, 8, 9][0]' 85 | 86 | const result = evaluate(expr) 87 | 88 | expect(result).toBe(7) 89 | }) 90 | 91 | it('should access first element if index 0.0', () => { 92 | const expr = '[7, 8, 9][0.0]' 93 | 94 | const result = evaluate(expr) 95 | 96 | expect(result).toBe(7) 97 | }) 98 | 99 | it('should throw error on index 0.1', () => { 100 | const expr = '[7, 8, 9][0.1]' 101 | 102 | const result = () => evaluate(expr) 103 | 104 | expect(result).toThrow(new CelEvaluationError('invalid_argument: 0.1')) 105 | }) 106 | 107 | it('should access list a singleton', () => { 108 | const expr = '["foo"][0]' 109 | 110 | const result = evaluate(expr) 111 | 112 | expect(result).toBe('foo') 113 | }) 114 | 115 | it('should access list on the last index', () => { 116 | const expr = '[7, 8, 9][2]' 117 | 118 | const result = evaluate(expr) 119 | 120 | expect(result).toBe(9) 121 | }) 122 | 123 | it('should access the list on middle values', () => { 124 | const expr = '[0, 1, 1, 2, 3, 5, 8, 13][4]' 125 | 126 | const result = evaluate(expr) 127 | 128 | expect(result).toBe(3) 129 | }) 130 | 131 | it('should throw an error if index out of bounds', () => { 132 | const expr = '[1][5]' 133 | 134 | const result = () => evaluate(expr) 135 | 136 | expect(result).toThrow(new CelEvaluationError(`Index out of bounds: 5`)) 137 | }) 138 | }) 139 | 140 | describe('concatenation', () => { 141 | it('should concatenate two lists', () => { 142 | const expr = '[1, 2] + [3, 4]' 143 | 144 | const result = evaluate(expr) 145 | 146 | expect(result).toStrictEqual([1, 2, 3, 4]) 147 | }) 148 | 149 | it('should concatenate two lists with the same element', () => { 150 | const expr = '[2] + [2]' 151 | 152 | const result = evaluate(expr) 153 | 154 | expect(result).toStrictEqual([2, 2]) 155 | }) 156 | 157 | it('should return empty list if both elements are empty', () => { 158 | const expr = '[] + []' 159 | 160 | const result = evaluate(expr) 161 | 162 | expect(result).toStrictEqual([]) 163 | }) 164 | 165 | it('should return correct list if left side is empty', () => { 166 | const expr = '[] + [1, 2]' 167 | 168 | const result = evaluate(expr) 169 | 170 | expect(result).toStrictEqual([1, 2]) 171 | }) 172 | 173 | it('should return correct list if right side is empty', () => { 174 | const expr = '[1, 2] + []' 175 | 176 | const result = evaluate(expr) 177 | 178 | expect(result).toStrictEqual([1, 2]) 179 | }) 180 | 181 | it('should throw an error if lists have different types', () => { 182 | const expr = '[1] + [true]' 183 | 184 | const result = () => evaluate(expr) 185 | 186 | expect(result).toThrow(new CelTypeError(Operations.addition, 1, true)) 187 | }) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /src/spec/logical-operators.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { evaluate } from '..' 4 | import { CelTypeError } from '../errors/CelTypeError' 5 | import { Operations } from '../helper' 6 | 7 | describe('logical operators', () => { 8 | describe('AND', () => { 9 | it('should return true if second expressions are true', () => { 10 | const expr = 'true && true' 11 | 12 | const result = evaluate(expr) 13 | 14 | expect(result).toBe(true) 15 | }) 16 | 17 | it('should return false if second expression is false', () => { 18 | const expr = 'true && false' 19 | 20 | const result = evaluate(expr) 21 | 22 | expect(result).toBe(false) 23 | }) 24 | 25 | it('should return true if all expressions are true', () => { 26 | const expr = 'true && true && true' 27 | 28 | const result = evaluate(expr) 29 | 30 | expect(result).toBe(true) 31 | }) 32 | 33 | it('should return false if at least one expressions is false', () => { 34 | const expr = 'true && false && true' 35 | 36 | const result = evaluate(expr) 37 | 38 | expect(result).toBe(false) 39 | }) 40 | 41 | it('should throw an error if one of types is not boolean', () => { 42 | const expr = 'true && 1' 43 | 44 | const result = () => evaluate(expr) 45 | 46 | expect(result).toThrow(new CelTypeError(Operations.logicalAnd, true, 1)) 47 | }) 48 | }) 49 | 50 | describe('OR', () => { 51 | it('should return true if at least one expression is true', () => { 52 | const expr = 'true || false' 53 | 54 | const result = evaluate(expr) 55 | 56 | expect(result).toBe(true) 57 | }) 58 | 59 | it('should return false if all expressions are false', () => { 60 | const expr = 'false || false' 61 | 62 | const result = evaluate(expr) 63 | 64 | expect(result).toBe(false) 65 | }) 66 | 67 | it('should return true if at least expression is true', () => { 68 | const expr = 'false || true || false' 69 | 70 | const result = evaluate(expr) 71 | 72 | expect(result).toBe(true) 73 | }) 74 | }) 75 | 76 | it('should be able to combine AND and OR', () => { 77 | const expr = 'true && true || false' 78 | 79 | const result = evaluate(expr) 80 | 81 | expect(result).toBe(true) 82 | }) 83 | 84 | it('should throw an error if one of types is not boolean', () => { 85 | const expr = 'true || 1' 86 | 87 | const result = () => evaluate(expr) 88 | 89 | expect(result).toThrow(new CelTypeError(Operations.logicalOr, true, 1)) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/spec/macros.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { CelTypeError, evaluate } from '..' 4 | import { Operations } from '../helper' 5 | 6 | describe('lists expressions', () => { 7 | describe('has', () => { 8 | it('should return true when nested property exists', () => { 9 | const expr = 'has(object.property)' 10 | 11 | const result = evaluate(expr, { object: { property: true } }) 12 | 13 | expect(result).toBe(true) 14 | }) 15 | 16 | it('should return false when property does not exists', () => { 17 | const expr = 'has(object.nonExisting)' 18 | 19 | const result = evaluate(expr, { object: { property: true } }) 20 | 21 | expect(result).toBe(false) 22 | }) 23 | 24 | it('should return false when property does not exists, combined with property usage', () => { 25 | const expr = 'has(object.nonExisting) && object.nonExisting' 26 | 27 | const result = evaluate(expr, { object: { property: true } }) 28 | 29 | expect(result).toBe(false) 30 | }) 31 | 32 | it('should throw when no arguments are passed', () => { 33 | const expr = 'has()' 34 | const context = { object: { property: true } } 35 | 36 | expect(() => evaluate(expr, context)).toThrow( 37 | 'has() requires exactly one argument', 38 | ) 39 | }) 40 | 41 | it('should throw when argument is not an object', () => { 42 | const context = { object: { property: true } } 43 | const errorMessages = 'has() requires a field selection' 44 | 45 | expect(() => evaluate('has(object)', context)).toThrow(errorMessages) 46 | 47 | expect(() => evaluate('has(object[0])', context)).toThrow(errorMessages) 48 | 49 | expect(() => evaluate('has(object[property])', context)).toThrow( 50 | errorMessages, 51 | ) 52 | }) 53 | 54 | describe('should throw when argument is an atomic expresion of type', () => { 55 | const errorMessages = 'has() does not support atomic expressions' 56 | const context = { object: { property: true } } 57 | 58 | it('string', () => { 59 | expect(() => evaluate('has("")', context)).toThrow(errorMessages) 60 | 61 | expect(() => evaluate('has("string")', context)).toThrow(errorMessages) 62 | }) 63 | 64 | it('array', () => { 65 | expect(() => evaluate('has([])', context)).toThrow(errorMessages) 66 | 67 | expect(() => evaluate('has([1, 2, 3])', context)).toThrow(errorMessages) 68 | }) 69 | 70 | it('boolean', () => { 71 | expect(() => evaluate('has(true)', context)).toThrow(errorMessages) 72 | 73 | expect(() => evaluate('has(false)', context)).toThrow(errorMessages) 74 | }) 75 | 76 | it('number', () => { 77 | expect(() => evaluate('has(42)', context)).toThrow(errorMessages) 78 | 79 | expect(() => evaluate('has(0)', context)).toThrow(errorMessages) 80 | 81 | expect(() => evaluate('has(0.3)', context)).toThrow(errorMessages) 82 | }) 83 | }) 84 | }) 85 | 86 | describe('size', () => { 87 | describe('list', () => { 88 | it('should return 0 for empty list', () => { 89 | const expr = 'size([])' 90 | 91 | const result = evaluate(expr) 92 | 93 | expect(result).toBe(0) 94 | }) 95 | 96 | it('should return 1 for one element list', () => { 97 | const expr = 'size([1])' 98 | 99 | const result = evaluate(expr) 100 | 101 | expect(result).toBe(1) 102 | }) 103 | 104 | it('should return 3 for three element list', () => { 105 | const expr = 'size([1, 2, 3])' 106 | 107 | const result = evaluate(expr) 108 | 109 | expect(result).toBe(3) 110 | }) 111 | }) 112 | 113 | describe('map', () => { 114 | it('should return 0 for empty map', () => { 115 | const expr = 'size({})' 116 | 117 | const result = evaluate(expr) 118 | 119 | expect(result).toBe(0) 120 | }) 121 | 122 | it('should return 1 for one element map', () => { 123 | const expr = 'size({"a": 1})' 124 | 125 | const result = evaluate(expr) 126 | 127 | expect(result).toBe(1) 128 | }) 129 | 130 | it('should return 3 for three element map', () => { 131 | const expr = 'size({"a": 1, "b": 2, "c": 3})' 132 | 133 | const result = evaluate(expr) 134 | 135 | expect(result).toBe(3) 136 | }) 137 | }) 138 | 139 | describe('string', () => { 140 | it('should return 0 for empty string', () => { 141 | const expr = 'size("")' 142 | 143 | const result = evaluate(expr) 144 | 145 | expect(result).toBe(0) 146 | }) 147 | 148 | it('should return length of string', () => { 149 | const expr = 'size("abc")' 150 | 151 | const result = evaluate(expr) 152 | 153 | expect(result).toBe(3) 154 | }) 155 | }) 156 | 157 | it.todo('should thrown an error if operator is not string or list', () => { 158 | const expr = 'size(123)' 159 | 160 | const result = () => evaluate(expr) 161 | 162 | expect(result).toThrow(new CelTypeError(Operations.addition, 123, 123)) 163 | }) 164 | }) 165 | }) 166 | 167 | describe('custom functions', () => { 168 | describe('single argument', () => { 169 | it('should execute a single argument custom function', () => { 170 | const expr = 'foo(bar)' 171 | 172 | const foo = (arg: unknown) => { 173 | return `foo:${arg}` 174 | } 175 | 176 | const result = evaluate(expr, { bar: 'bar' }, { foo }) 177 | 178 | expect(result).toBe('foo:bar') 179 | }) 180 | }) 181 | 182 | describe('multi argument', () => { 183 | it('should execute a two argument custom function', () => { 184 | const expr = 'foo(bar, 42)' 185 | 186 | const foo = (thing: unknown, intensity: unknown) => { 187 | return `foo:${thing} ${intensity}` 188 | } 189 | 190 | const result = evaluate(expr, { bar: 'bar' }, { foo }) 191 | 192 | expect(result).toBe('foo:bar 42') 193 | }) 194 | }) 195 | 196 | describe('interaction with default functions', () => { 197 | it('should preserve default functions when custom functions specified', () => { 198 | const expr = 'foo(bar, size("ubernete"), true)' 199 | 200 | const foo = (thing, intensity, enable) => { 201 | return `foo:${thing} ${intensity} ${enable}` 202 | } 203 | 204 | const result = evaluate(expr, { bar: 'bar' }, { foo: foo }) 205 | 206 | expect(result).toBe('foo:bar 8 true') 207 | }) 208 | 209 | it('should allow overriding default functions', () => { 210 | const expr = 'foo(bar, size("ubernete"), true)' 211 | 212 | const foo = (thing, intensity, enable) => { 213 | return `foo:${thing} ${intensity} ${enable}` 214 | } 215 | 216 | const result = evaluate( 217 | expr, 218 | { bar: 'bar' }, 219 | { foo: foo, size: () => 'strange' }, 220 | ) 221 | 222 | expect(result).toBe('foo:bar strange true') 223 | }) 224 | }) 225 | 226 | describe('unknown functions', () => { 227 | it('should throw when an unknown function is called', () => { 228 | const expr = 'foo(bar)' 229 | 230 | const result = () => evaluate(expr) 231 | 232 | expect(result).toThrow('Macros foo not recognized') 233 | }) 234 | 235 | it('should not treat context values as first-class functions', () => { 236 | const expr = 'foo(bar)' 237 | 238 | const result = () => evaluate(expr, { foo: 'foo', bar: 'bar' }) 239 | 240 | expect(result).toThrow('Macros foo not recognized') 241 | }) 242 | }) 243 | }) 244 | -------------------------------------------------------------------------------- /src/spec/maps.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | import { CelEvaluationError, evaluate } from '..' 3 | 4 | describe('maps expressions', () => { 5 | describe('literal', () => { 6 | it('should create a empty map', () => { 7 | const expr = '{}' 8 | 9 | const result = evaluate(expr) 10 | 11 | expect(result).toStrictEqual({}) 12 | }) 13 | 14 | it('should create a one element map', () => { 15 | const expr = '{"a": 1}' 16 | 17 | const result = evaluate(expr) 18 | 19 | expect(result).toStrictEqual({ a: 1 }) 20 | }) 21 | 22 | it('should create a many element map', () => { 23 | const expr = '{"a": 1, "b": 2, "c": 3}' 24 | 25 | const result = evaluate(expr) 26 | 27 | expect(result).toStrictEqual({ a: 1, b: 2, c: 3 }) 28 | }) 29 | 30 | it('should throw an error if maps have different types', () => { 31 | const expr = '{"a": 1, "b": true}' 32 | 33 | const result = () => evaluate(expr) 34 | 35 | expect(result).toThrow(new CelEvaluationError('invalid_argument: true')) 36 | }) 37 | }) 38 | 39 | describe('index', () => { 40 | describe('dot expression', () => { 41 | it('should get the value of a key', () => { 42 | const expr = '{"a": 1}.a' 43 | 44 | const result = evaluate(expr) 45 | 46 | expect(result).toBe(1) 47 | }) 48 | 49 | it('should throw an error if the key does not exist', () => { 50 | const expr = '{"a": 1}.b' 51 | 52 | const result = () => evaluate(expr) 53 | 54 | expect(result).toThrow('Identifier "b" not found, no context passed') 55 | }) 56 | 57 | it.todo('should throw an error if the key is not a string', () => { 58 | const expr = '{"a": 1}.1' 59 | 60 | const result = () => evaluate(expr) 61 | 62 | expect(result).toThrow('invalid_argument: 1') 63 | }) 64 | }) 65 | describe('index expression', () => { 66 | it('should get the value of a key', () => { 67 | const expr = '{"a": 1}["a"]' 68 | 69 | const result = evaluate(expr) 70 | 71 | expect(result).toBe(1) 72 | }) 73 | 74 | it('should throw an error if the key does not exist', () => { 75 | const expr = '{"a": 1}["b"]' 76 | 77 | const result = () => evaluate(expr) 78 | 79 | expect(result).toThrow('Identifier "b" not found, no context passed') 80 | }) 81 | 82 | it('should throw an error if the key is not a string', () => { 83 | const expr = '{"a": 1}[1]' 84 | 85 | const result = () => evaluate(expr) 86 | 87 | expect(result).toThrow('Identifier "1" not found, no context passed') 88 | }) 89 | }) 90 | }) 91 | 92 | describe('equal', () => { 93 | it('should compare two equal maps', () => { 94 | const expr = '{"c": 1, "a": 1, "b": 2} == {"a": 1, "b": 2, "c": 1}' 95 | 96 | const result = evaluate(expr) 97 | 98 | expect(result).toBe(true) 99 | }) 100 | it('should compare two different maps', () => { 101 | const expr = '{"a": 1, "b": 2} == {"a": 1, "b": 2, "c": 1}' 102 | 103 | const result = evaluate(expr) 104 | 105 | expect(result).toBe(false) 106 | }) 107 | }) 108 | describe('not equal', () => { 109 | it('should compare two equal maps', () => { 110 | const expr = '{"c": 1, "a": 1, "b": 2} != {"a": 1, "b": 2, "c": 1}' 111 | 112 | const result = evaluate(expr) 113 | 114 | expect(result).toBe(false) 115 | }) 116 | it('should compare two different maps', () => { 117 | const expr = '{"a": 1, "b": 2} != {"a": 1, "b": 2, "c": 1}' 118 | 119 | const result = evaluate(expr) 120 | 121 | expect(result).toBe(true) 122 | }) 123 | }) 124 | describe('in', () => { 125 | it('should find a key in the map', () => { 126 | const expr = '"c" in {"c": 1, "a": 1, "b": 2}' 127 | 128 | const result = evaluate(expr) 129 | 130 | expect(result).toBe(true) 131 | }) 132 | it('should not find a key in the map', () => { 133 | const expr = '"z" in {"c": 1, "a": 1, "b": 2}' 134 | 135 | const result = evaluate(expr) 136 | 137 | expect(result).toBe(false) 138 | }) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /src/spec/miscellaneous.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { evaluate } from '..' 4 | 5 | describe('miscellaneous', () => { 6 | it('order of arithmetic operations', () => { 7 | const expr = '1 + 2 * 3 + 1' 8 | 9 | const result = evaluate(expr) 10 | 11 | expect(result).toBe(8) 12 | }) 13 | 14 | describe('parenthesis', () => { 15 | it('should prioritize parenthesis expression', () => { 16 | const expr = '(1 + 2) * 3 + 1' 17 | 18 | const result = evaluate(expr) 19 | 20 | expect(result).toBe(10) 21 | }) 22 | 23 | it('should allow multiple expressions', () => { 24 | const expr = '(1 + 2) * (3 + 1)' 25 | 26 | const result = evaluate(expr) 27 | 28 | expect(result).toBe(12) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/spec/multiplication.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { evaluate } from '..' 4 | import { CelTypeError } from '../errors/CelTypeError' 5 | import { Operations } from '../helper' 6 | import { CelEvaluationError } from '../errors/CelEvaluationError' 7 | 8 | describe('multiplication', () => { 9 | it('should evaluate multiplication', () => { 10 | const expr = '2 * 3' 11 | 12 | const result = evaluate(expr) 13 | 14 | expect(result).toBe(6) 15 | }) 16 | 17 | it('should evaluate division', () => { 18 | const expr = '6 / 3' 19 | 20 | const result = evaluate(expr) 21 | 22 | expect(result).toBe(2) 23 | }) 24 | 25 | it('should evaluate modulo', () => { 26 | const expr = '6 % 4' 27 | 28 | const result = evaluate(expr) 29 | 30 | expect(result).toBe(2) 31 | }) 32 | 33 | it('should evaluate multiplication with multiple terms', () => { 34 | const expr = '2 * 3 * 4' 35 | 36 | const result = evaluate(expr) 37 | 38 | expect(result).toBe(24) 39 | }) 40 | 41 | it('should evaluate multiplication with multiple terms with different signs', () => { 42 | const expr = '2 * 3 / 3' 43 | 44 | const result = evaluate(expr) 45 | 46 | expect(result).toBe(2) 47 | }) 48 | 49 | describe('should throw when', () => { 50 | it('is a boolean', () => { 51 | const expr = 'true * 1' 52 | 53 | const result = () => evaluate(expr) 54 | 55 | expect(result).toThrow( 56 | new CelTypeError(Operations.multiplication, true, 1), 57 | ) 58 | }) 59 | 60 | it('is a null', () => { 61 | const expr = 'null / 1' 62 | 63 | const result = () => evaluate(expr) 64 | 65 | expect(result).toThrow(new CelTypeError(Operations.division, null, 1)) 66 | }) 67 | 68 | it('is dividing by 0', () => { 69 | const expr = '1 / 0' 70 | 71 | const result = () => evaluate(expr) 72 | 73 | expect(result).toThrow(new CelEvaluationError('Division by zero')) 74 | }) 75 | 76 | it('is modulo by 0', () => { 77 | const expr = '1 % 0' 78 | 79 | const result = () => evaluate(expr) 80 | 81 | expect(result).toThrow(new CelEvaluationError('Modulus by zero')) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/spec/reserver-identifiers.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | 3 | import { evaluate } from '..' 4 | import { reservedIdentifiers } from '../tokens.js' 5 | 6 | describe('reserved identifiers', () => { 7 | it.each(reservedIdentifiers)( 8 | 'should throw if reserved identifier "%s" is used', 9 | (identifier) => { 10 | const expr = `${identifier} < 1` 11 | 12 | const result = () => evaluate(expr) 13 | 14 | expect(result).toThrow( 15 | `Detected reserved identifier. This is not allowed`, 16 | ) 17 | }, 18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /src/spec/unary-operators.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { evaluate } from '../index.js' 3 | import { CelTypeError } from '../errors/CelTypeError.js' 4 | 5 | describe('Unary Operators', () => { 6 | describe('Logical negation (!)', () => { 7 | it('should negate boolean values correctly', () => { 8 | expect(evaluate('!true')).toBe(false) 9 | expect(evaluate('!false')).toBe(true) 10 | expect(evaluate('!!true')).toBe(true) 11 | expect(evaluate('!!false')).toBe(false) 12 | expect(evaluate('!!!true')).toBe(false) 13 | }) 14 | 15 | it('should handle null values correctly', () => { 16 | expect(evaluate('!null')).toBe(true) 17 | expect(evaluate('!!null')).toBe(false) 18 | expect(evaluate('!!!null')).toBe(true) 19 | }) 20 | 21 | it('should throw error when used with non-boolean, non-null values', () => { 22 | expect(() => evaluate('!"string"')).toThrow( 23 | new CelTypeError('logical negation', 'string', null), 24 | ) 25 | 26 | expect(() => evaluate('!123')).toThrow( 27 | new CelTypeError('logical negation', 123, null), 28 | ) 29 | 30 | expect(() => evaluate('![]')).toThrow( 31 | new CelTypeError('logical negation', [], null), 32 | ) 33 | 34 | expect(() => evaluate('!{}')).toThrow( 35 | new CelTypeError('logical negation', {}, null), 36 | ) 37 | }) 38 | }) 39 | 40 | describe('Arithmetic negation (-)', () => { 41 | it('should negate numeric values correctly', () => { 42 | expect(evaluate('-5')).toBe(-5) 43 | expect(evaluate('--5')).toBe(5) 44 | expect(evaluate('---5')).toBe(-5) 45 | expect(evaluate('-0')).toBe(0) 46 | expect(evaluate('-3.14')).toBe(-3.14) 47 | }) 48 | 49 | it('should throw error when used with non-numeric values', () => { 50 | expect(() => evaluate('-"string"')).toThrow( 51 | new CelTypeError('arithmetic negation', 'string', null), 52 | ) 53 | 54 | expect(() => evaluate('-true')).toThrow( 55 | new CelTypeError('arithmetic negation', true, null), 56 | ) 57 | 58 | expect(() => evaluate('-null')).toThrow( 59 | new CelTypeError('arithmetic negation', null, null), 60 | ) 61 | 62 | expect(() => evaluate('-[]')).toThrow( 63 | new CelTypeError('arithmetic negation', [], null), 64 | ) 65 | 66 | expect(() => evaluate('-{}')).toThrow( 67 | new CelTypeError('arithmetic negation', {}, null), 68 | ) 69 | }) 70 | }) 71 | 72 | describe('Integration with other operators', () => { 73 | it('should work with comparison operators', () => { 74 | expect(evaluate('!true == false')).toBe(true) 75 | expect(evaluate('!(5 > 3) == false')).toBe(true) 76 | expect(evaluate('-5 < 0')).toBe(true) 77 | }) 78 | 79 | it('should work with conditional operators', () => { 80 | expect(evaluate('!true ? "yes" : "no"')).toBe('no') 81 | expect(evaluate('!false ? "yes" : "no"')).toBe('yes') 82 | expect(evaluate('!null ? "yes" : "no"')).toBe('yes') 83 | }) 84 | 85 | it('should respect operator precedence', () => { 86 | expect(evaluate('!true || true')).toBe(true) // (!true) || true 87 | expect(evaluate('!(true || true)')).toBe(false) // !(true || true) 88 | expect(evaluate('-5 + 3')).toBe(-2) // (-5) + 3 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/spec/unsigned-integer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'vitest' 2 | import { evaluate } from '..' 3 | 4 | describe('unsigned integers', () => { 5 | it('should evaluate a simple unsigned integer', () => { 6 | expect(evaluate('123u')).toBe(123) 7 | }) 8 | 9 | it('should evaluate a uppercase U unsigned integer', () => { 10 | expect(evaluate('456U')).toBe(456) 11 | }) 12 | 13 | it('should evaluate zero as unsigned integer', () => { 14 | expect(evaluate('0u')).toBe(0) 15 | }) 16 | 17 | it('should preserve the unsigned integer type in calculations', () => { 18 | expect(evaluate('10u + 5')).toBe(15) 19 | }) 20 | 21 | it('should handle unsigned integers in comparisons', () => { 22 | expect(evaluate('100u > 50')).toBe(true) 23 | }) 24 | 25 | it('should evaluate a hexadecimal unsigned integer with uppercase', () => { 26 | expect(evaluate('0xABCDU')).toBe(43981) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { createToken, Lexer } from 'chevrotain' 2 | 3 | export const Comment = createToken({ 4 | name: 'Comment', 5 | pattern: /\/\/[^\n]*/, 6 | group: Lexer.SKIPPED, 7 | }) 8 | 9 | export const WhiteSpace = createToken({ 10 | name: 'WhiteSpace', 11 | pattern: /\s+/, 12 | group: Lexer.SKIPPED, 13 | }) 14 | 15 | export const CloseParenthesis = createToken({ 16 | name: 'CloseParenthesis', 17 | pattern: /\)/, 18 | }) 19 | 20 | export const OpenParenthesis = createToken({ 21 | name: 'OpenParenthesis', 22 | pattern: /\(/, 23 | }) 24 | 25 | export const OpenBracket = createToken({ name: 'OpenBracket', pattern: /\[/ }) 26 | 27 | export const CloseBracket = createToken({ 28 | name: 'CloseBracket', 29 | pattern: /\]/, 30 | }) 31 | 32 | export const OpenCurlyBracket = createToken({ 33 | name: 'OpenCurlyBracket', 34 | pattern: /{/, 35 | }) 36 | 37 | export const CloseCurlyBracket = createToken({ 38 | name: 'CloseCurlyBracket', 39 | pattern: /}/, 40 | }) 41 | 42 | export const Dot = createToken({ name: 'Dot', pattern: /\./ }) 43 | 44 | export const Comma = createToken({ name: 'Comma', pattern: /,/ }) 45 | 46 | export const Colon = createToken({ name: 'Colon', pattern: /:/ }) 47 | 48 | export const QuestionMark = createToken({ name: 'QuestionMark', pattern: /\?/ }) 49 | 50 | export const Float = createToken({ 51 | name: 'Float', 52 | pattern: /-?\d+\.\d+/, 53 | }) 54 | 55 | export const HexInteger = createToken({ 56 | name: 'HexInteger', 57 | pattern: /0x[0-9a-fA-F]+/, 58 | }) 59 | 60 | export const Integer = createToken({ name: 'Integer', pattern: /0|[1-9]\d*/ }) 61 | 62 | export const HexUnsignedInteger = createToken({ 63 | name: 'HexUnsignedInteger', 64 | pattern: /0x[0-9a-fA-F]+[uU]/, 65 | }) 66 | 67 | export const UnsignedInteger = createToken({ 68 | name: 'UnsignedInteger', 69 | pattern: /(0|[1-9]\d*)[uU]/, 70 | }) 71 | 72 | export const BooleanLiteral = createToken({ 73 | name: 'BooleanLiteral', 74 | pattern: /true|false/, 75 | }) 76 | 77 | export const True = createToken({ 78 | name: 'True', 79 | pattern: /true/, 80 | categories: BooleanLiteral, 81 | }) 82 | 83 | export const False = createToken({ 84 | name: 'False', 85 | pattern: /false/, 86 | categories: BooleanLiteral, 87 | }) 88 | 89 | export const Null = createToken({ name: 'Null', pattern: /null/ }) 90 | 91 | export const StringLiteral = createToken({ 92 | name: 'StringLiteral', 93 | pattern: /(?:"(?:[^"\n\\]|\\.)*")|(?:'(?:[^'\n\\]|\\.)*')/, 94 | }) 95 | 96 | export const reservedIdentifiers = [ 97 | 'as', 98 | 'break', 99 | 'const', 100 | 'continue', 101 | 'else', 102 | 'for', 103 | 'function', 104 | 'if', 105 | 'import', 106 | 'let', 107 | 'loop', 108 | 'package', 109 | 'namespace', 110 | 'return', 111 | 'var', 112 | 'void', 113 | 'while', 114 | ] 115 | 116 | export const LogicalOrOperator = createToken({ 117 | name: 'LogicalOrOperator', 118 | pattern: /\|\|/, 119 | }) 120 | 121 | export const LogicalAndOperator = createToken({ 122 | name: 'LogicalAndOperator', 123 | pattern: /&&/, 124 | }) 125 | 126 | export const UnaryOperator = createToken({ 127 | name: 'UnaryOperator', 128 | pattern: Lexer.NA, 129 | }) 130 | 131 | export const LogicalNotOperator = createToken({ 132 | name: 'LogicalNotOperator', 133 | pattern: /!/, 134 | categories: UnaryOperator, 135 | }) 136 | 137 | export const ComparisonOperator = createToken({ 138 | name: 'ComparisonOperator', 139 | pattern: Lexer.NA, 140 | }) 141 | 142 | export const Equals = createToken({ 143 | name: 'Equals', 144 | pattern: /==/, 145 | categories: ComparisonOperator, 146 | }) 147 | 148 | export const NotEquals = createToken({ 149 | name: 'NotEquals', 150 | pattern: /!=/, 151 | categories: ComparisonOperator, 152 | }) 153 | 154 | export const GreaterOrEqualThan = createToken({ 155 | name: 'GreaterOrEqualThan', 156 | pattern: />=/, 157 | categories: ComparisonOperator, 158 | }) 159 | 160 | export const LessOrEqualThan = createToken({ 161 | name: 'LessOrEqualThan', 162 | pattern: /<=/, 163 | categories: ComparisonOperator, 164 | }) 165 | 166 | export const GreaterThan = createToken({ 167 | name: 'GreaterThan', 168 | pattern: />/, 169 | categories: ComparisonOperator, 170 | }) 171 | 172 | export const LessThan = createToken({ 173 | name: 'LessThan', 174 | pattern: / 55 | { 56 | constructor( 57 | context?: Record, 58 | functions?: Record, 59 | ) { 60 | super() 61 | this.context = context || {} 62 | 63 | this.functions = { 64 | ...defaultFunctions, 65 | ...(functions || {}), 66 | } 67 | 68 | this.validateVisitor() 69 | } 70 | 71 | private context: Record 72 | 73 | /** 74 | * Tracks the current mode of the visitor to handle special cases. 75 | */ 76 | private mode: Mode = Mode.normal 77 | 78 | private functions: Record 79 | 80 | /** 81 | * Evaluates the expression including conditional ternary expressions in the form: condition ? trueExpr : falseExpr 82 | * 83 | * @param ctx - The expression context containing the condition and optional ternary branches 84 | * @returns The result of evaluating the expression 85 | */ 86 | public expr(ctx: ExprCstChildren): unknown { 87 | const condition = this.visit(ctx.conditionalOr[0]) 88 | 89 | // If no ternary operator is present, just return the condition 90 | if (!ctx.QuestionMark) return condition 91 | 92 | // Evaluate the appropriate branch based on the condition (logical true/false) 93 | if (condition) { 94 | return this.visit(ctx.lhs![0]) 95 | } else { 96 | return this.visit(ctx.rhs![0]) 97 | } 98 | } 99 | 100 | /** 101 | * Handles the special 'has' macro which checks for the existence of a field. 102 | * 103 | * @param ctx - The macro expression context containing the argument to check 104 | * @returns boolean indicating if the field exists 105 | * @throws CelEvaluationError if argument is missing or invalid 106 | */ 107 | private handleHasMacro(ctx: MacrosExpressionCstChildren): boolean { 108 | if (!ctx.arg) { 109 | throw new CelEvaluationError('has() requires exactly one argument') 110 | } 111 | 112 | this.mode = Mode.has 113 | try { 114 | const result = this.visit(ctx.arg) 115 | return this.functions.has(result) 116 | } catch (error) { 117 | // Only convert to false if it's not a validation error 118 | if (error instanceof CelEvaluationError) { 119 | throw error 120 | } 121 | return false 122 | } finally { 123 | this.mode = Mode.normal 124 | } 125 | } 126 | 127 | /** 128 | * Handles execution of generic macro functions by evaluating and passing their arguments. 129 | * 130 | * @param fn - The macro function to execute 131 | * @param ctx - The macro expression context containing the arguments 132 | * @returns The result of executing the macro function with the evaluated arguments 133 | */ 134 | private handleGenericMacro( 135 | fn: CallableFunction, 136 | ctx: MacrosExpressionCstChildren, 137 | ): unknown { 138 | return fn( 139 | ...[ 140 | ...(ctx.arg ? [this.visit(ctx.arg)] : []), 141 | ...(ctx.args ? ctx.args.map((arg) => this.visit(arg)) : []), 142 | ], 143 | ) 144 | } 145 | 146 | conditionalOr(ctx: ConditionalOrCstChildren): boolean { 147 | let left = this.visit(ctx.lhs) 148 | 149 | if (ctx.rhs) { 150 | ctx.rhs.forEach((rhsOperand) => { 151 | const right = this.visit(rhsOperand) 152 | const operator = ctx.LogicalOrOperator![0] 153 | 154 | left = getResult(operator, left, right) 155 | }) 156 | } 157 | 158 | return left 159 | } 160 | 161 | /** 162 | * Evaluates a logical AND expression by visiting left and right hand operands. 163 | * 164 | * @param ctx - The conditional AND context containing left and right operands 165 | * @returns The boolean result of evaluating the AND expression 166 | * 167 | * This method implements short-circuit evaluation - if the left operand is false, 168 | * it returns false immediately without evaluating the right operand. This is required 169 | * for proper handling of the has() macro. 170 | * 171 | * For multiple right-hand operands, it evaluates them sequentially, combining results 172 | * with logical AND operations. 173 | */ 174 | conditionalAnd(ctx: ConditionalAndCstChildren): boolean { 175 | let left = this.visit(ctx.lhs) 176 | 177 | // Short circuit if left is false. Required to quick fail for has() macro. 178 | if (left === false) { 179 | return false 180 | } 181 | 182 | if (ctx.rhs) { 183 | ctx.rhs.forEach((rhsOperand) => { 184 | const right = this.visit(rhsOperand) 185 | const operator = ctx.LogicalAndOperator![0] 186 | 187 | left = getResult(operator, left, right) 188 | }) 189 | } 190 | 191 | return left 192 | } 193 | 194 | relation(ctx: RelationCstChildren): boolean { 195 | const left = this.visit(ctx.lhs) 196 | 197 | if (ctx.rhs) { 198 | const right = this.visit(ctx.rhs) 199 | const operator = ctx.ComparisonOperator![0] 200 | 201 | // maybe we can make the function more type safe by mapping input w/ output types 202 | return getResult(operator, left, right) as boolean 203 | } 204 | 205 | return left 206 | } 207 | 208 | addition(ctx: AdditionCstChildren): unknown { 209 | let left = this.visit(ctx.lhs) 210 | 211 | if (ctx.rhs) { 212 | ctx.rhs.forEach((rhsOperand, idx) => { 213 | const right = this.visit(rhsOperand) 214 | const operator = ctx.AdditionOperator![idx] 215 | 216 | left = getResult(operator, left, right) 217 | }) 218 | } 219 | 220 | return left 221 | } 222 | 223 | multiplication(ctx: MultiplicationCstChildren) { 224 | let left = this.visit(ctx.lhs) 225 | 226 | if (ctx.rhs) { 227 | ctx.rhs.forEach((rhsOperand, idx) => { 228 | const right = this.visit(rhsOperand) 229 | const operator = ctx.MultiplicationOperator![idx] 230 | 231 | left = getResult(operator, left, right) 232 | }) 233 | } 234 | 235 | return left 236 | } 237 | 238 | unaryExpression(ctx: UnaryExpressionCstChildren): unknown { 239 | if (ctx.UnaryOperator) { 240 | const operator = ctx.UnaryOperator 241 | const operand = this.visit(ctx.atomicExpression) 242 | 243 | return getUnaryResult(operator, operand) 244 | } 245 | 246 | return this.visit(ctx.atomicExpression) 247 | } 248 | 249 | parenthesisExpression(ctx: ParenthesisExpressionCstChildren) { 250 | return this.visit(ctx.expr) 251 | } 252 | 253 | listExpression(ctx: ListExpressionCstChildren) { 254 | const result = [] 255 | if (!ctx.lhs) { 256 | return [] 257 | } 258 | 259 | const left = this.visit(ctx.lhs) 260 | 261 | result.push(left) 262 | if (ctx.rhs) { 263 | for (const rhsOperand of ctx.rhs) { 264 | const right = this.visit(rhsOperand) 265 | result.push(right) 266 | } 267 | } 268 | 269 | if (!ctx.Index) { 270 | return result 271 | } 272 | 273 | const index = this.visit(ctx.Index) 274 | 275 | const indexType = getCelType(index) 276 | if (indexType != CelType.int && indexType != CelType.uint) { 277 | throw new CelEvaluationError(`invalid_argument: ${index}`) 278 | } 279 | 280 | if (index < 0 || index >= result.length) { 281 | throw new CelEvaluationError(`Index out of bounds: ${index}`) 282 | } 283 | 284 | return result[index] 285 | } 286 | 287 | mapExpression(ctx: MapExpressionCstChildren) { 288 | const mapExpression: Record = {} 289 | if (!ctx.keyValues) { 290 | return {} 291 | } 292 | let valueType = '' 293 | for (const keyValuePair of ctx.keyValues) { 294 | const [key, value] = this.visit(keyValuePair) 295 | if (valueType === '') { 296 | valueType = getCelType(value) 297 | } 298 | if (getCelType(key) != CelType.string) { 299 | throw new CelEvaluationError(`invalid_argument: ${key}`) 300 | } 301 | if (valueType !== getCelType(value)) { 302 | throw new CelEvaluationError(`invalid_argument: ${value}`) 303 | } 304 | mapExpression[key] = value 305 | } 306 | 307 | if (!ctx.identifierDotExpression && !ctx.identifierIndexExpression) { 308 | return mapExpression 309 | } 310 | 311 | return this.getIndexSection(ctx, mapExpression) 312 | } 313 | 314 | private getIndexSection( 315 | ctx: MapExpressionCstChildren | IdentifierExpressionCstChildren, 316 | mapExpression: unknown, 317 | ) { 318 | const expressions = [ 319 | ...(ctx.identifierDotExpression || []), 320 | ...(ctx.identifierIndexExpression || []), 321 | ].sort((a, b) => (getPosition(a) > getPosition(b) ? 1 : -1)) 322 | 323 | return expressions.reduce((acc: unknown, expression) => { 324 | if (expression.name === 'identifierDotExpression') { 325 | return this.getIdentifier(acc, expression.children.Identifier[0].image) 326 | } 327 | 328 | const index = this.visit(expression.children.expr[0]) 329 | return this.getIdentifier(acc, index) 330 | }, mapExpression) 331 | } 332 | 333 | mapKeyValues(children: MapKeyValuesCstChildren): [string, unknown] { 334 | const key = this.visit(children.key) 335 | const value = this.visit(children.value) 336 | return [key, value] 337 | } 338 | 339 | /** 340 | * Evaluates a macros expression by executing the corresponding macro function. 341 | * 342 | * @param ctx - The macro expression context containing the macro identifier and arguments 343 | * @returns The result of executing the macro function 344 | * @throws Error if the macro function is not recognized 345 | * 346 | * This method handles two types of macros: 347 | * 1. The special 'has' macro which checks for field existence 348 | * 2. Generic macros that take evaluated arguments 349 | */ 350 | macrosExpression(ctx: MacrosExpressionCstChildren): unknown { 351 | const [macrosIdentifier] = ctx.Identifier 352 | const fn = this.functions[macrosIdentifier.image] 353 | 354 | if (!fn) { 355 | throw new Error(`Macros ${macrosIdentifier.image} not recognized`) 356 | } 357 | 358 | // Handle special case for `has` macro 359 | if (macrosIdentifier.image === 'has') { 360 | return this.handleHasMacro(ctx) 361 | } 362 | 363 | return this.handleGenericMacro(fn, ctx) 364 | } 365 | 366 | /** 367 | * Evaluates an atomic expression node in the AST. 368 | * 369 | * @param ctx - The atomic expression context containing the expression type and value 370 | * @returns The evaluated value of the atomic expression 371 | * @throws CelEvaluationError if invalid atomic expression is used in has() macro 372 | * @throws Error if reserved identifier is used or expression type not recognized 373 | * 374 | * Handles the following atomic expression types: 375 | * - Null literals 376 | * - Parenthesized expressions 377 | * - String literals 378 | * - Boolean literals 379 | * - Float literals 380 | * - Integer literals 381 | * - Identifier expressions 382 | * - List expressions 383 | * - Map expressions 384 | * - Macro expressions 385 | */ 386 | atomicExpression(ctx: AtomicExpressionCstChildren) { 387 | if (ctx.Null) { 388 | return null 389 | } 390 | 391 | // Check if we are in a has() macro, and if so, throw an error if we are not in a field selection 392 | if (this.mode === Mode.has && !ctx.identifierExpression) { 393 | throw new CelEvaluationError('has() does not support atomic expressions') 394 | } 395 | 396 | if (ctx.parenthesisExpression) { 397 | return this.visit(ctx.parenthesisExpression) 398 | } 399 | 400 | if (ctx.StringLiteral) { 401 | return ctx.StringLiteral[0].image.slice(1, -1) 402 | } 403 | 404 | if (ctx.BooleanLiteral) { 405 | return ctx.BooleanLiteral[0].image === 'true' 406 | } 407 | 408 | if (ctx.Float) { 409 | return parseFloat(ctx.Float[0].image) 410 | } 411 | 412 | if (ctx.Integer) { 413 | return parseInt(ctx.Integer[0].image, 10) 414 | } 415 | 416 | if (ctx.UnsignedInteger) { 417 | return parseInt(ctx.UnsignedInteger[0].image.slice(0, -1), 10) 418 | } 419 | 420 | if (ctx.HexInteger) { 421 | return parseInt(ctx.HexInteger[0].image.slice(2), 16) 422 | } 423 | 424 | if (ctx.HexUnsignedInteger) { 425 | return parseInt(ctx.HexUnsignedInteger[0].image.slice(2, -1), 16) 426 | } 427 | 428 | if (ctx.identifierExpression) { 429 | return this.visit(ctx.identifierExpression) 430 | } 431 | 432 | if (ctx.listExpression) { 433 | return this.visit(ctx.listExpression) 434 | } 435 | 436 | if (ctx.mapExpression) { 437 | return this.visit(ctx.mapExpression) 438 | } 439 | 440 | if (ctx.macrosExpression) { 441 | return this.visit(ctx.macrosExpression) 442 | } 443 | 444 | throw new Error('Atomic expression not recognized') 445 | } 446 | 447 | identifierExpression(ctx: IdentifierExpressionCstChildren): unknown { 448 | // Validate that we have a dot expression when in a has() macro 449 | if (this.mode === Mode.has && !ctx.identifierDotExpression?.length) { 450 | throw new CelEvaluationError('has() requires a field selection') 451 | } 452 | 453 | const identifierName = ctx.Identifier[0].image 454 | // If this is a standalone identifier and is reserved, throw 455 | if ( 456 | !ctx.identifierDotExpression && 457 | !ctx.identifierIndexExpression && 458 | reservedIdentifiers.includes(identifierName) 459 | ) { 460 | throw new Error('Detected reserved identifier. This is not allowed') 461 | } 462 | const data = this.context 463 | const result = this.getIdentifier(data, identifierName) 464 | 465 | if (!ctx.identifierDotExpression && !ctx.identifierIndexExpression) { 466 | return result 467 | } 468 | 469 | return this.getIndexSection(ctx, result) 470 | } 471 | 472 | identifierDotExpression( 473 | ctx: IdentifierDotExpressionCstChildren, 474 | param: unknown, 475 | ): unknown { 476 | const identifierName = ctx.Identifier[0].image 477 | return this.getIdentifier(param, identifierName) 478 | } 479 | 480 | indexExpression(ctx: IndexExpressionCstChildren): unknown { 481 | return this.visit(ctx.expr) 482 | } 483 | 484 | getIdentifier(searchContext: unknown, identifier: string): unknown { 485 | if (typeof searchContext !== 'object' || searchContext === null) { 486 | throw new Error( 487 | `Cannot obtain "${identifier}" from non-object context: ${searchContext}`, 488 | ) 489 | } 490 | 491 | const value = (searchContext as Record)[identifier] 492 | 493 | if (value === undefined) { 494 | const context = JSON.stringify(this?.context) 495 | 496 | if (context === '{}') { 497 | throw new Error( 498 | `Identifier "${identifier}" not found, no context passed`, 499 | ) 500 | } 501 | throw new Error( 502 | `Identifier "${identifier}" not found in context: ${context}`, 503 | ) 504 | } 505 | 506 | return value 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /ts-signatures-generator.ts: -------------------------------------------------------------------------------- 1 | import { CelParser } from './src/parser.ts' 2 | import { writeFileSync } from 'fs' 3 | import { resolve, dirname } from 'path' 4 | import { generateCstDts } from 'chevrotain' 5 | import { fileURLToPath } from 'url' 6 | 7 | const parser = new CelParser() 8 | 9 | export const productions = parser.getGAstProductions() 10 | 11 | const __dirname = dirname(fileURLToPath(import.meta.url)) 12 | 13 | const dtsString = generateCstDts(productions) 14 | const dtsPath = resolve(__dirname, './src', 'cst-definitions.d.ts') 15 | writeFileSync(dtsPath, dtsString) 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2022", 4 | "target": "ES2022", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "sourceMap": false, 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./src", 14 | "paths": { 15 | "*": ["*", "src/*"] 16 | } 17 | }, 18 | "include": ["src/index.ts"], 19 | "exclude": ["node_modules", "dist", "**/*.spec.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | // w/o this file VSC vitest extension crushes ... 2 | const config = {} 3 | export default config 4 | --------------------------------------------------------------------------------