From 93d206047df10c9cf5c91f08aa30c797eaa87e58 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 29 Apr 2025 12:19:08 -0400 Subject: [PATCH 1/7] chore: Add package per OS/arch --- libs/darwin-arm64/README.md | 3 +++ libs/darwin-arm64/package.json | 18 ++++++++++++++++++ libs/darwin-x64/README.md | 3 +++ libs/darwin-x64/package.json | 18 ++++++++++++++++++ libs/linux-arm64-gnu/README.md | 3 +++ libs/linux-arm64-gnu/package.json | 21 +++++++++++++++++++++ libs/linux-x64-gnu/README.md | 3 +++ libs/linux-x64-gnu/package.json | 21 +++++++++++++++++++++ libs/win32-x64-msvc/README.md | 3 +++ libs/win32-x64-msvc/package.json | 18 ++++++++++++++++++ 10 files changed, 111 insertions(+) create mode 100644 libs/darwin-arm64/README.md create mode 100644 libs/darwin-arm64/package.json create mode 100644 libs/darwin-x64/README.md create mode 100644 libs/darwin-x64/package.json create mode 100644 libs/linux-arm64-gnu/README.md create mode 100644 libs/linux-arm64-gnu/package.json create mode 100644 libs/linux-x64-gnu/README.md create mode 100644 libs/linux-x64-gnu/package.json create mode 100644 libs/win32-x64-msvc/README.md create mode 100644 libs/win32-x64-msvc/package.json diff --git a/libs/darwin-arm64/README.md b/libs/darwin-arm64/README.md new file mode 100644 index 0000000..d5e2106 --- /dev/null +++ b/libs/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# `@kevinmichaelchen/cel-typescript-darwin-arm64` + +This is the **aarch64-apple-darwin** binary for `@kevinmichaelchen/cel-typescript` diff --git a/libs/darwin-arm64/package.json b/libs/darwin-arm64/package.json new file mode 100644 index 0000000..a7145a7 --- /dev/null +++ b/libs/darwin-arm64/package.json @@ -0,0 +1,18 @@ +{ + "name": "@kevinmichaelchen/cel-typescript-darwin-arm64", + "version": "0.0.0", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "main": "cel-typescript.darwin-arm64.node", + "files": [ + "cel-typescript.darwin-arm64.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/libs/darwin-x64/README.md b/libs/darwin-x64/README.md new file mode 100644 index 0000000..33739f3 --- /dev/null +++ b/libs/darwin-x64/README.md @@ -0,0 +1,3 @@ +# `@kevinmichaelchen/cel-typescript-darwin-x64` + +This is the **x86_64-apple-darwin** binary for `@kevinmichaelchen/cel-typescript` diff --git a/libs/darwin-x64/package.json b/libs/darwin-x64/package.json new file mode 100644 index 0000000..ec066c1 --- /dev/null +++ b/libs/darwin-x64/package.json @@ -0,0 +1,18 @@ +{ + "name": "@kevinmichaelchen/cel-typescript-darwin-x64", + "version": "0.0.0", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "main": "cel-typescript.darwin-x64.node", + "files": [ + "cel-typescript.darwin-x64.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/libs/linux-arm64-gnu/README.md b/libs/linux-arm64-gnu/README.md new file mode 100644 index 0000000..717cf17 --- /dev/null +++ b/libs/linux-arm64-gnu/README.md @@ -0,0 +1,3 @@ +# `@kevinmichaelchen/cel-typescript-linux-arm64-gnu` + +This is the **aarch64-unknown-linux-gnu** binary for `@kevinmichaelchen/cel-typescript` diff --git a/libs/linux-arm64-gnu/package.json b/libs/linux-arm64-gnu/package.json new file mode 100644 index 0000000..f470001 --- /dev/null +++ b/libs/linux-arm64-gnu/package.json @@ -0,0 +1,21 @@ +{ + "name": "@kevinmichaelchen/cel-typescript-linux-arm64-gnu", + "version": "0.0.0", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "cel-typescript.linux-arm64-gnu.node", + "files": [ + "cel-typescript.linux-arm64-gnu.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "glibc" + ] +} \ No newline at end of file diff --git a/libs/linux-x64-gnu/README.md b/libs/linux-x64-gnu/README.md new file mode 100644 index 0000000..19e654e --- /dev/null +++ b/libs/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `@kevinmichaelchen/cel-typescript-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `@kevinmichaelchen/cel-typescript` diff --git a/libs/linux-x64-gnu/package.json b/libs/linux-x64-gnu/package.json new file mode 100644 index 0000000..eb5bf25 --- /dev/null +++ b/libs/linux-x64-gnu/package.json @@ -0,0 +1,21 @@ +{ + "name": "@kevinmichaelchen/cel-typescript-linux-x64-gnu", + "version": "0.0.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "cel-typescript.linux-x64-gnu.node", + "files": [ + "cel-typescript.linux-x64-gnu.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "glibc" + ] +} \ No newline at end of file diff --git a/libs/win32-x64-msvc/README.md b/libs/win32-x64-msvc/README.md new file mode 100644 index 0000000..8b9e770 --- /dev/null +++ b/libs/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `@kevinmichaelchen/cel-typescript-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `@kevinmichaelchen/cel-typescript` diff --git a/libs/win32-x64-msvc/package.json b/libs/win32-x64-msvc/package.json new file mode 100644 index 0000000..863df76 --- /dev/null +++ b/libs/win32-x64-msvc/package.json @@ -0,0 +1,18 @@ +{ + "name": "@kevinmichaelchen/cel-typescript-win32-x64-msvc", + "version": "0.0.0", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "cel-typescript.win32-x64-msvc.node", + "files": [ + "cel-typescript.win32-x64-msvc.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file From 3a4aea57a5e9ab19fbda7cd20a2d6d45a6a9b06b Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 29 Apr 2025 12:24:36 -0400 Subject: [PATCH 2/7] chore: stuff --- biome.json | 2 +- esm/wrapper.js | 4 ---- project.json | 5 +++++ scripts/build.sh | 4 ++-- vitest.config.ts | 4 +--- 5 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 esm/wrapper.js diff --git a/biome.json b/biome.json index 8240f9c..50427bc 100644 --- a/biome.json +++ b/biome.json @@ -12,7 +12,7 @@ "indentStyle": "space", "indentWidth": 2, "lineWidth": 80, - "ignore": ["index.js"] + "ignore": ["index.js", "index.d.ts"] }, "linter": { "enabled": true, diff --git a/esm/wrapper.js b/esm/wrapper.js deleted file mode 100644 index 664bedd..0000000 --- a/esm/wrapper.js +++ /dev/null @@ -1,4 +0,0 @@ -import * as native from "../index.js"; -export const CelProgram = native.CelProgram; -export const evaluate = native.evaluate; -export default native; diff --git a/project.json b/project.json index 04d8380..6d2f501 100644 --- a/project.json +++ b/project.json @@ -7,6 +7,11 @@ "options": { "command": "bash scripts/build.sh {args.target}" }, + "inputs": [ + "{workspaceRoot}/package.json", + "{workspaceRoot}/scripts/build.sh", + "{projectRoot}/src/lib.rs" + ], "outputs": [ "{workspaceRoot}/cel-typescript.darwin-arm64.node", "{workspaceRoot}/cel-typescript.darwin-x64.node", diff --git a/scripts/build.sh b/scripts/build.sh index a779dea..481d61b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,7 +1,7 @@ #!/bin/bash if [ "$USE_ZIG" = "true" ]; then - npx @napi-rs/cli build --platform --target "$1" --release --zig + npx @napi-rs/cli build --platform --target "$1" --js src/native.cjs --release --zig else - npx @napi-rs/cli build --platform --target "$1" --release + npx @napi-rs/cli build --platform --target "$1" --js src/native.cjs --release fi diff --git a/vitest.config.ts b/vitest.config.ts index 6041fbc..a8f45ba 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,9 +7,7 @@ export default defineConfig({ environment: "node", }, resolve: { - alias: { - "#native": "../cel-typescript.darwin-arm64.node", - }, + alias: {}, }, assetsInclude: ["**/*.node"], }); From 331488fff82eff246fa669256dee94ed24648bd0 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 29 Apr 2025 12:32:30 -0400 Subject: [PATCH 3/7] chore: package.json --- package.json | 57 +++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 9d66f9c..58e91b6 100644 --- a/package.json +++ b/package.json @@ -3,39 +3,14 @@ "version": "0.0.11", "type": "module", "description": "TypeScript bindings for the Common Expression Language (CEL) using cel-rust", - "repository": { - "type": "git", - "url": "git+https://github.com/kevinmichaelchen/cel-typescript.git" - }, - "author": "Kevin Chen", - "license": "MIT", - "bugs": { - "url": "https://github.com/kevinmichaelchen/cel-typescript/issues" - }, - "homepage": "https://github.com/kevinmichaelchen/cel-typescript#readme", - "files": ["dist/**/*", "*.node", "index.js", "index.d.ts"], - "keywords": [ - "cel", - "common-expression-language", - "expression-language", - "policy", - "rust", - "napi-rs" - ], - "engines": { - "node": ">=18.0.0" - }, + "files": ["dist/src/**/*", "*.node", "index.js", "index.d.ts"], + "types": "./dist/src/index.d.ts", "exports": { ".": { "import": "./dist/src/index.js", "types": "./dist/src/index.d.ts" - }, - "./native": { - "import": "./esm/wrapper.js", - "types": "./index.d.ts" } }, - "types": "./dist/src/index.d.ts", "napi": { "name": "cel-typescript", "triples": { @@ -60,6 +35,13 @@ "test": "nx test", "clean": "nx clean" }, + "optionalDependencies": { + "@kevinmichaelchen/cel-typescript-darwin-arm64": "^0.0.0", + "@kevinmichaelchen/cel-typescript-darwin-x64": "^0.0.0", + "@kevinmichaelchen/cel-typescript-linux-arm64-gnu": "^0.0.0", + "@kevinmichaelchen/cel-typescript-linux-x64-gnu": "^0.0.0", + "@kevinmichaelchen/cel-typescript-win32-x64-msvc": "^0.0.0" + }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@napi-rs/cli": "^2.18.4", @@ -73,5 +55,26 @@ "ts-node": "^10.9.2", "typescript": "^5.0.0", "vitest": "^3.1.2" + }, + "author": "Kevin Chen", + "repository": { + "type": "git", + "url": "git+https://github.com/kevinmichaelchen/cel-typescript.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/kevinmichaelchen/cel-typescript/issues" + }, + "homepage": "https://github.com/kevinmichaelchen/cel-typescript#readme", + "keywords": [ + "cel", + "common-expression-language", + "expression-language", + "policy", + "rust", + "napi-rs" + ], + "engines": { + "node": ">=18.0.0" } } From 519d8b068d29ea4e1630db021adeaa7ff5324ce5 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 29 Apr 2025 12:32:47 -0400 Subject: [PATCH 4/7] chore: Formatting --- libs/darwin-arm64/package.json | 14 ++++---------- libs/darwin-x64/package.json | 14 ++++---------- libs/linux-arm64-gnu/package.json | 18 +++++------------- libs/linux-x64-gnu/package.json | 18 +++++------------- libs/win32-x64-msvc/package.json | 14 ++++---------- 5 files changed, 22 insertions(+), 56 deletions(-) diff --git a/libs/darwin-arm64/package.json b/libs/darwin-arm64/package.json index a7145a7..b6e50d6 100644 --- a/libs/darwin-arm64/package.json +++ b/libs/darwin-arm64/package.json @@ -1,18 +1,12 @@ { "name": "@kevinmichaelchen/cel-typescript-darwin-arm64", "version": "0.0.0", - "os": [ - "darwin" - ], - "cpu": [ - "arm64" - ], + "os": ["darwin"], + "cpu": ["arm64"], "main": "cel-typescript.darwin-arm64.node", - "files": [ - "cel-typescript.darwin-arm64.node" - ], + "files": ["cel-typescript.darwin-arm64.node"], "license": "MIT", "engines": { "node": ">= 10" } -} \ No newline at end of file +} diff --git a/libs/darwin-x64/package.json b/libs/darwin-x64/package.json index ec066c1..bc10935 100644 --- a/libs/darwin-x64/package.json +++ b/libs/darwin-x64/package.json @@ -1,18 +1,12 @@ { "name": "@kevinmichaelchen/cel-typescript-darwin-x64", "version": "0.0.0", - "os": [ - "darwin" - ], - "cpu": [ - "x64" - ], + "os": ["darwin"], + "cpu": ["x64"], "main": "cel-typescript.darwin-x64.node", - "files": [ - "cel-typescript.darwin-x64.node" - ], + "files": ["cel-typescript.darwin-x64.node"], "license": "MIT", "engines": { "node": ">= 10" } -} \ No newline at end of file +} diff --git a/libs/linux-arm64-gnu/package.json b/libs/linux-arm64-gnu/package.json index f470001..0069e55 100644 --- a/libs/linux-arm64-gnu/package.json +++ b/libs/linux-arm64-gnu/package.json @@ -1,21 +1,13 @@ { "name": "@kevinmichaelchen/cel-typescript-linux-arm64-gnu", "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "arm64" - ], + "os": ["linux"], + "cpu": ["arm64"], "main": "cel-typescript.linux-arm64-gnu.node", - "files": [ - "cel-typescript.linux-arm64-gnu.node" - ], + "files": ["cel-typescript.linux-arm64-gnu.node"], "license": "MIT", "engines": { "node": ">= 10" }, - "libc": [ - "glibc" - ] -} \ No newline at end of file + "libc": ["glibc"] +} diff --git a/libs/linux-x64-gnu/package.json b/libs/linux-x64-gnu/package.json index eb5bf25..f7a8fa1 100644 --- a/libs/linux-x64-gnu/package.json +++ b/libs/linux-x64-gnu/package.json @@ -1,21 +1,13 @@ { "name": "@kevinmichaelchen/cel-typescript-linux-x64-gnu", "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], + "os": ["linux"], + "cpu": ["x64"], "main": "cel-typescript.linux-x64-gnu.node", - "files": [ - "cel-typescript.linux-x64-gnu.node" - ], + "files": ["cel-typescript.linux-x64-gnu.node"], "license": "MIT", "engines": { "node": ">= 10" }, - "libc": [ - "glibc" - ] -} \ No newline at end of file + "libc": ["glibc"] +} diff --git a/libs/win32-x64-msvc/package.json b/libs/win32-x64-msvc/package.json index 863df76..e17cd39 100644 --- a/libs/win32-x64-msvc/package.json +++ b/libs/win32-x64-msvc/package.json @@ -1,18 +1,12 @@ { "name": "@kevinmichaelchen/cel-typescript-win32-x64-msvc", "version": "0.0.0", - "os": [ - "win32" - ], - "cpu": [ - "x64" - ], + "os": ["win32"], + "cpu": ["x64"], "main": "cel-typescript.win32-x64-msvc.node", - "files": [ - "cel-typescript.win32-x64-msvc.node" - ], + "files": ["cel-typescript.win32-x64-msvc.node"], "license": "MIT", "engines": { "node": ">= 10" } -} \ No newline at end of file +} From 83ea1a295aef562ac76c36b5673852da900e0517 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 29 Apr 2025 12:48:37 -0400 Subject: [PATCH 5/7] chore: new loader file --- biome.json | 4 +- package.json | 4 +- scripts/build.sh | 30 +++++++++- index.js => src/native.cjs | 108 +++++++++++++++++----------------- index.d.ts => src/native.d.ts | 0 5 files changed, 86 insertions(+), 60 deletions(-) rename index.js => src/native.cjs (52%) rename index.d.ts => src/native.d.ts (100%) diff --git a/biome.json b/biome.json index 50427bc..b632370 100644 --- a/biome.json +++ b/biome.json @@ -5,14 +5,14 @@ }, "files": { "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"], - "ignore": [".nx", "index.js", "index.d.ts", "dist/**/*"] + "ignore": [".nx", "dist/**/*", "src/native.cjs", "src/native.d.ts"] }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 80, - "ignore": ["index.js", "index.d.ts"] + "ignore": ["src/native.cjs", "src/native.d.ts"] }, "linter": { "enabled": true, diff --git a/package.json b/package.json index 58e91b6..4e41dd6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.11", "type": "module", "description": "TypeScript bindings for the Common Expression Language (CEL) using cel-rust", - "files": ["dist/src/**/*", "*.node", "index.js", "index.d.ts"], + "files": ["dist/src/**/*"], "types": "./dist/src/index.d.ts", "exports": { ".": { @@ -12,7 +12,7 @@ } }, "napi": { - "name": "cel-typescript", + "name": "@kevinmichaelchen/cel-typescript", "triples": { "defaults": false, "additional": [ diff --git a/scripts/build.sh b/scripts/build.sh index 481d61b..f57b076 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,7 +1,33 @@ #!/bin/bash +# See https://napi.rs/docs/cli/build for details + if [ "$USE_ZIG" = "true" ]; then - npx @napi-rs/cli build --platform --target "$1" --js src/native.cjs --release --zig + npx @napi-rs/cli build \ + # Add platform triple to the .node file (e.g., [name].linux-x64-gnu.node) + --platform \ + + # Target triple (e.g., x86_64-apple-darwin) + --target "$1" \ + + # The filename and path of the JavaScript binding file + --js src/native.cjs \ + + # TypeScript declaration file + --dts src/native.d.ts \ + + # Bypass to cargo build --release + --release \ + + # @napi-rs/cli will use zig as cc / cxx and linker to build your program. + --zig + else - npx @napi-rs/cli build --platform --target "$1" --js src/native.cjs --release + npx @napi-rs/cli build \ + --platform \ + --target "$1" \ + --js src/native.cjs \ + --dts src/native.d.ts \ + --release + fi diff --git a/index.js b/src/native.cjs similarity index 52% rename from index.js rename to src/native.cjs index 2ef3bc4..76e6c6a 100644 --- a/index.js +++ b/src/native.cjs @@ -32,24 +32,24 @@ switch (platform) { case 'android': switch (arch) { case 'arm64': - localFileExisted = existsSync(join(__dirname, 'cel-typescript.android-arm64.node')) + localFileExisted = existsSync(join(__dirname, '@kevinmichaelchen/cel-typescript.android-arm64.node')) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.android-arm64.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.android-arm64.node') } else { - nativeBinding = require('cel-typescript-android-arm64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-android-arm64') } } catch (e) { loadError = e } break case 'arm': - localFileExisted = existsSync(join(__dirname, 'cel-typescript.android-arm-eabi.node')) + localFileExisted = existsSync(join(__dirname, '@kevinmichaelchen/cel-typescript.android-arm-eabi.node')) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.android-arm-eabi.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.android-arm-eabi.node') } else { - nativeBinding = require('cel-typescript-android-arm-eabi') + nativeBinding = require('@kevinmichaelchen/cel-typescript-android-arm-eabi') } } catch (e) { loadError = e @@ -63,13 +63,13 @@ switch (platform) { switch (arch) { case 'x64': localFileExisted = existsSync( - join(__dirname, 'cel-typescript.win32-x64-msvc.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.win32-x64-msvc.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.win32-x64-msvc.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.win32-x64-msvc.node') } else { - nativeBinding = require('cel-typescript-win32-x64-msvc') + nativeBinding = require('@kevinmichaelchen/cel-typescript-win32-x64-msvc') } } catch (e) { loadError = e @@ -77,13 +77,13 @@ switch (platform) { break case 'ia32': localFileExisted = existsSync( - join(__dirname, 'cel-typescript.win32-ia32-msvc.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.win32-ia32-msvc.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.win32-ia32-msvc.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.win32-ia32-msvc.node') } else { - nativeBinding = require('cel-typescript-win32-ia32-msvc') + nativeBinding = require('@kevinmichaelchen/cel-typescript-win32-ia32-msvc') } } catch (e) { loadError = e @@ -91,13 +91,13 @@ switch (platform) { break case 'arm64': localFileExisted = existsSync( - join(__dirname, 'cel-typescript.win32-arm64-msvc.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.win32-arm64-msvc.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.win32-arm64-msvc.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.win32-arm64-msvc.node') } else { - nativeBinding = require('cel-typescript-win32-arm64-msvc') + nativeBinding = require('@kevinmichaelchen/cel-typescript-win32-arm64-msvc') } } catch (e) { loadError = e @@ -108,23 +108,23 @@ switch (platform) { } break case 'darwin': - localFileExisted = existsSync(join(__dirname, 'cel-typescript.darwin-universal.node')) + localFileExisted = existsSync(join(__dirname, '@kevinmichaelchen/cel-typescript.darwin-universal.node')) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.darwin-universal.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.darwin-universal.node') } else { - nativeBinding = require('cel-typescript-darwin-universal') + nativeBinding = require('@kevinmichaelchen/cel-typescript-darwin-universal') } break } catch {} switch (arch) { case 'x64': - localFileExisted = existsSync(join(__dirname, 'cel-typescript.darwin-x64.node')) + localFileExisted = existsSync(join(__dirname, '@kevinmichaelchen/cel-typescript.darwin-x64.node')) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.darwin-x64.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.darwin-x64.node') } else { - nativeBinding = require('cel-typescript-darwin-x64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-darwin-x64') } } catch (e) { loadError = e @@ -132,13 +132,13 @@ switch (platform) { break case 'arm64': localFileExisted = existsSync( - join(__dirname, 'cel-typescript.darwin-arm64.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.darwin-arm64.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.darwin-arm64.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.darwin-arm64.node') } else { - nativeBinding = require('cel-typescript-darwin-arm64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-darwin-arm64') } } catch (e) { loadError = e @@ -152,12 +152,12 @@ switch (platform) { if (arch !== 'x64') { throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) } - localFileExisted = existsSync(join(__dirname, 'cel-typescript.freebsd-x64.node')) + localFileExisted = existsSync(join(__dirname, '@kevinmichaelchen/cel-typescript.freebsd-x64.node')) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.freebsd-x64.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.freebsd-x64.node') } else { - nativeBinding = require('cel-typescript-freebsd-x64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-freebsd-x64') } } catch (e) { loadError = e @@ -168,26 +168,26 @@ switch (platform) { case 'x64': if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-x64-musl.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-x64-musl.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-x64-musl.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-x64-musl.node') } else { - nativeBinding = require('cel-typescript-linux-x64-musl') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-x64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-x64-gnu.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-x64-gnu.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-x64-gnu.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-x64-gnu.node') } else { - nativeBinding = require('cel-typescript-linux-x64-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-x64-gnu') } } catch (e) { loadError = e @@ -197,26 +197,26 @@ switch (platform) { case 'arm64': if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-arm64-musl.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-arm64-musl.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-arm64-musl.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm64-musl.node') } else { - nativeBinding = require('cel-typescript-linux-arm64-musl') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-arm64-gnu.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-arm64-gnu.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-arm64-gnu.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm64-gnu.node') } else { - nativeBinding = require('cel-typescript-linux-arm64-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm64-gnu') } } catch (e) { loadError = e @@ -226,26 +226,26 @@ switch (platform) { case 'arm': if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-arm-musleabihf.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-arm-musleabihf.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-arm-musleabihf.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm-musleabihf.node') } else { - nativeBinding = require('cel-typescript-linux-arm-musleabihf') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm-musleabihf') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-arm-gnueabihf.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-arm-gnueabihf.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-arm-gnueabihf.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm-gnueabihf.node') } else { - nativeBinding = require('cel-typescript-linux-arm-gnueabihf') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm-gnueabihf') } } catch (e) { loadError = e @@ -255,26 +255,26 @@ switch (platform) { case 'riscv64': if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-riscv64-musl.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-riscv64-musl.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-riscv64-musl.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-riscv64-musl.node') } else { - nativeBinding = require('cel-typescript-linux-riscv64-musl') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-riscv64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-riscv64-gnu.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-riscv64-gnu.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-riscv64-gnu.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-riscv64-gnu.node') } else { - nativeBinding = require('cel-typescript-linux-riscv64-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-riscv64-gnu') } } catch (e) { loadError = e @@ -283,13 +283,13 @@ switch (platform) { break case 's390x': localFileExisted = existsSync( - join(__dirname, 'cel-typescript.linux-s390x-gnu.node') + join(__dirname, '@kevinmichaelchen/cel-typescript.linux-s390x-gnu.node') ) try { if (localFileExisted) { - nativeBinding = require('./cel-typescript.linux-s390x-gnu.node') + nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-s390x-gnu.node') } else { - nativeBinding = require('cel-typescript-linux-s390x-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-s390x-gnu') } } catch (e) { loadError = e diff --git a/index.d.ts b/src/native.d.ts similarity index 100% rename from index.d.ts rename to src/native.d.ts From b60fb2c45ad8625994bf73eaf9762c9efa489306 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 29 Apr 2025 13:06:42 -0400 Subject: [PATCH 6/7] chore: move stuff to libs/core --- cel-rust | 1 - Cargo.toml => libs/core/Cargo.toml | 0 build.rs => libs/core/build.rs | 0 .../cel-rust/.github/workflows/release.yml | 51 + libs/core/cel-rust/.github/workflows/rust.yml | 34 + libs/core/cel-rust/.gitignore | 2 + libs/core/cel-rust/Cargo.toml | 8 + libs/core/cel-rust/LICENSE | 19 + libs/core/cel-rust/README.md | 47 + libs/core/cel-rust/example/Cargo.toml | 60 + libs/core/cel-rust/example/src/axum.rs | 151 ++ libs/core/cel-rust/example/src/functions.rs | 65 + libs/core/cel-rust/example/src/json.rs | 12 + libs/core/cel-rust/example/src/serde.rs | 22 + libs/core/cel-rust/example/src/simple.rs | 8 + libs/core/cel-rust/example/src/threads.rs | 23 + libs/core/cel-rust/example/src/variables.rs | 10 + libs/core/cel-rust/interpreter/CHANGELOG.md | 21 + libs/core/cel-rust/interpreter/Cargo.toml | 37 + libs/core/cel-rust/interpreter/README.md | 54 + .../cel-rust/interpreter/benches/runtime.rs | 70 + libs/core/cel-rust/interpreter/src/context.rs | 207 +++ .../core/cel-rust/interpreter/src/duration.rs | 281 ++++ .../cel-rust/interpreter/src/functions.rs | 1010 ++++++++++++ libs/core/cel-rust/interpreter/src/json.rs | 117 ++ libs/core/cel-rust/interpreter/src/lib.rs | 269 ++++ libs/core/cel-rust/interpreter/src/macros.rs | 98 ++ libs/core/cel-rust/interpreter/src/magic.rs | 403 +++++ libs/core/cel-rust/interpreter/src/objects.rs | 994 ++++++++++++ .../cel-rust/interpreter/src/resolvers.rs | 64 + libs/core/cel-rust/interpreter/src/ser.rs | 1372 +++++++++++++++++ libs/core/cel-rust/parser/CHANGELOG.md | 17 + libs/core/cel-rust/parser/Cargo.toml | 24 + libs/core/cel-rust/parser/README.md | 14 + libs/core/cel-rust/parser/benches/runtime.rs | 35 + libs/core/cel-rust/parser/build.rs | 5 + libs/core/cel-rust/parser/src/ast.rs | 223 +++ libs/core/cel-rust/parser/src/cel.lalrpop | 172 +++ libs/core/cel-rust/parser/src/error.rs | 140 ++ libs/core/cel-rust/parser/src/lib.rs | 549 +++++++ libs/core/cel-rust/parser/src/parse.rs | 537 +++++++ libs/core/cel-rust/release.toml | 2 + libs/core/package.json | 18 + {src => libs/core/src}/index.ts | 10 +- {src => libs/core/src}/lib.rs | 0 {src => libs/core/src}/native.cjs | 0 {src => libs/core/src}/native.d.ts | 0 package.json | 9 +- scripts/build.sh | 8 +- 49 files changed, 7256 insertions(+), 17 deletions(-) delete mode 160000 cel-rust rename Cargo.toml => libs/core/Cargo.toml (100%) rename build.rs => libs/core/build.rs (100%) create mode 100644 libs/core/cel-rust/.github/workflows/release.yml create mode 100644 libs/core/cel-rust/.github/workflows/rust.yml create mode 100644 libs/core/cel-rust/.gitignore create mode 100644 libs/core/cel-rust/Cargo.toml create mode 100644 libs/core/cel-rust/LICENSE create mode 100644 libs/core/cel-rust/README.md create mode 100644 libs/core/cel-rust/example/Cargo.toml create mode 100644 libs/core/cel-rust/example/src/axum.rs create mode 100644 libs/core/cel-rust/example/src/functions.rs create mode 100644 libs/core/cel-rust/example/src/json.rs create mode 100644 libs/core/cel-rust/example/src/serde.rs create mode 100644 libs/core/cel-rust/example/src/simple.rs create mode 100644 libs/core/cel-rust/example/src/threads.rs create mode 100644 libs/core/cel-rust/example/src/variables.rs create mode 100644 libs/core/cel-rust/interpreter/CHANGELOG.md create mode 100644 libs/core/cel-rust/interpreter/Cargo.toml create mode 100644 libs/core/cel-rust/interpreter/README.md create mode 100644 libs/core/cel-rust/interpreter/benches/runtime.rs create mode 100644 libs/core/cel-rust/interpreter/src/context.rs create mode 100644 libs/core/cel-rust/interpreter/src/duration.rs create mode 100644 libs/core/cel-rust/interpreter/src/functions.rs create mode 100644 libs/core/cel-rust/interpreter/src/json.rs create mode 100644 libs/core/cel-rust/interpreter/src/lib.rs create mode 100644 libs/core/cel-rust/interpreter/src/macros.rs create mode 100644 libs/core/cel-rust/interpreter/src/magic.rs create mode 100644 libs/core/cel-rust/interpreter/src/objects.rs create mode 100644 libs/core/cel-rust/interpreter/src/resolvers.rs create mode 100644 libs/core/cel-rust/interpreter/src/ser.rs create mode 100644 libs/core/cel-rust/parser/CHANGELOG.md create mode 100644 libs/core/cel-rust/parser/Cargo.toml create mode 100644 libs/core/cel-rust/parser/README.md create mode 100644 libs/core/cel-rust/parser/benches/runtime.rs create mode 100644 libs/core/cel-rust/parser/build.rs create mode 100644 libs/core/cel-rust/parser/src/ast.rs create mode 100644 libs/core/cel-rust/parser/src/cel.lalrpop create mode 100644 libs/core/cel-rust/parser/src/error.rs create mode 100644 libs/core/cel-rust/parser/src/lib.rs create mode 100644 libs/core/cel-rust/parser/src/parse.rs create mode 100644 libs/core/cel-rust/release.toml create mode 100644 libs/core/package.json rename {src => libs/core/src}/index.ts (91%) rename {src => libs/core/src}/lib.rs (100%) rename {src => libs/core/src}/native.cjs (100%) rename {src => libs/core/src}/native.d.ts (100%) diff --git a/cel-rust b/cel-rust deleted file mode 160000 index d84558a..0000000 --- a/cel-rust +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d84558acf2eb76e1145b9a106e05b2189911a176 diff --git a/Cargo.toml b/libs/core/Cargo.toml similarity index 100% rename from Cargo.toml rename to libs/core/Cargo.toml diff --git a/build.rs b/libs/core/build.rs similarity index 100% rename from build.rs rename to libs/core/build.rs diff --git a/libs/core/cel-rust/.github/workflows/release.yml b/libs/core/cel-rust/.github/workflows/release.yml new file mode 100644 index 0000000..28d4a2f --- /dev/null +++ b/libs/core/cel-rust/.github/workflows/release.yml @@ -0,0 +1,51 @@ +name: Release-plz + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: + - master + +jobs: + + # Release unpublished packages. + release-plz-release: + name: Release-plz release + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: MarcoIeni/release-plz-action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # Create a PR with the new versions and changelog, preparing the next release. + release-plz-pr: + name: Release-plz PR + runs-on: ubuntu-latest + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: MarcoIeni/release-plz-action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/libs/core/cel-rust/.github/workflows/rust.yml b/libs/core/cel-rust/.github/workflows/rust.yml new file mode 100644 index 0000000..2fdd333 --- /dev/null +++ b/libs/core/cel-rust/.github/workflows/rust.yml @@ -0,0 +1,34 @@ +name: Rust + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + - name: Format + run: cargo fmt --all -- --check + - name: Build minimal + run: cargo build --verbose --no-default-features + - name: Build with default features + run: cargo build --verbose + - name: Build with all features + run: cargo build --verbose --all-features + - name: Clippy + run: cargo clippy --all-features --all-targets -- -D warnings + - name: Run tests + run: | + cargo test --verbose + cargo test --verbose --features json + cargo test --verbose --features regex + cargo test --verbose --features chrono diff --git a/libs/core/cel-rust/.gitignore b/libs/core/cel-rust/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/libs/core/cel-rust/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/libs/core/cel-rust/Cargo.toml b/libs/core/cel-rust/Cargo.toml new file mode 100644 index 0000000..3d41446 --- /dev/null +++ b/libs/core/cel-rust/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = ["parser", "interpreter", "example"] +resolver = "2" + +[profile.bench] +lto = true +codegen-units = 1 +opt-level = 3 diff --git a/libs/core/cel-rust/LICENSE b/libs/core/cel-rust/LICENSE new file mode 100644 index 0000000..b675d1c --- /dev/null +++ b/libs/core/cel-rust/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Tom Forbes and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libs/core/cel-rust/README.md b/libs/core/cel-rust/README.md new file mode 100644 index 0000000..57dd619 --- /dev/null +++ b/libs/core/cel-rust/README.md @@ -0,0 +1,47 @@ +# Common Expression Language (Rust) + +[![Rust](https://github.com/clarkmcc/cel-rust/actions/workflows/rust.yml/badge.svg)](https://github.com/clarkmcc/cel-rust/actions/workflows/rust.yml) + +The [Common Expression Language (CEL)](https://github.com/google/cel-spec) is a non-Turing complete language designed +for simplicity, speed, safety, and +portability. CEL's C-like syntax looks nearly identical to equivalent expressions in C++, Go, Java, and TypeScript. CEL +is ideal for lightweight expression evaluation when a fully sandboxed scripting language is too resource intensive. + +```java +// Check whether a resource name starts with a group name. +resource.name.startsWith("/groups/" + auth.claims.group) +``` + +```go +// Determine whether the request is in the permitted time window. +request.time - resource.age < duration("24h") +``` + +```typescript +// Check whether all resource names in a list match a given filter. +auth.claims.email_verified && resources.all(r, r.startsWith(auth.claims.email)) +``` + +## Getting Started + +This project includes a CEL-parser and an interpreter which means that it can be used to evaluate CEL-expressions. The +library aims to be very simple to use, while still being fast, safe, and customizable. + +```rust +fn main() { + let program = Program::compile("add(2, 3) == 5").unwrap(); + let mut context = Context::default(); + context.add_function("add", |a: i64, b: i64| a + b); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); +} +``` + +### Examples + +Check out these other examples to learn how to use this library: + +- [Simple](./example/src/simple.rs) - A simple example of how to use the library. +- [Variables](./example/src/variables.rs) - Passing variables and using them in your program. +- [Functions](./example/src/functions.rs) - Defining and using custom functions in your program. +- [Concurrent Execution](./example/src/threads.rs) - Executing the same program concurrently. diff --git a/libs/core/cel-rust/example/Cargo.toml b/libs/core/cel-rust/example/Cargo.toml new file mode 100644 index 0000000..5751eee --- /dev/null +++ b/libs/core/cel-rust/example/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" + +[features] +axum = ["dep:axum", "dep:tokio", "dep:thiserror"] +json = ["dep:serde_json", "cel-interpreter/json"] +chrono = ["dep:chrono", "cel-interpreter/chrono"] + +[dependencies] +cel-interpreter = { path = "../interpreter", default-features = false } + +chrono = { version = "0.4", optional = true } + +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", optional = true } + +axum = { version = "0.7.5", default-features = false, features = [ + "http1", + "json", + "tokio", +], optional = true } +tokio = { version = "1.38.0", default-features = false, features = [ + "macros", + "net", + "rt-multi-thread", +], optional = true } +thiserror = { version = "1.0", optional = true } + +[[bin]] +name = "example-simple" +path = "src/simple.rs" + +[[bin]] +name = "example-variables" +path = "src/variables.rs" + +[[bin]] +name = "example-functions" +path = "src/functions.rs" +required-features = ["chrono"] + +[[bin]] +name = "example-threads" +path = "src/threads.rs" + +[[bin]] +name = "example-serde" +path = "src/serde.rs" + +[[bin]] +name = "example-axum" +path = "src/axum.rs" +required-features = ["axum"] + +[[bin]] +name = "example-json" +path = "src/json.rs" +required-features = ["json"] diff --git a/libs/core/cel-rust/example/src/axum.rs b/libs/core/cel-rust/example/src/axum.rs new file mode 100644 index 0000000..1f2fa78 --- /dev/null +++ b/libs/core/cel-rust/example/src/axum.rs @@ -0,0 +1,151 @@ +//! This is a pretty straightforward TODO app using the Axum web framework. +//! You can add TODOs to your list but only if they meet certain criteria. +//! +//! To run it: +//! +//! ```shell +//! cargo run +//! ``` +//! +//! Add a TODO (HTTP 202): +//! +//! ```shell +//! curl -w "%{http_code}" -XPOST -H "content-type: application/json" \ +//! http://localhost:8080/todos -d '{"kind":"work","text":"Learn more Rust"}' +//! ``` +//! +//! Fetch the current TODOs: +//! +//! ```shell +//! curl http://localhost:8080/todos +//! ``` +//! +//! Add another TODO (HTTP 400): +//! +//! ```shell +//! curl -w "%{http_code}" -XPOST -H "content-type: application/json" \ +//! http://localhost:8080/todos -d '{"kind":"home","text":"Learn more Rust"}' +//! ``` + +use std::sync::{Arc, Mutex}; + +use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use cel_interpreter::{Context, Program, Value}; +use serde::{Deserialize, Serialize}; + +// Policies dictating which text TODOs may contain +const WORK_TODO_POLICY: &str = "!text.contains('TV')"; // Don't watch TV at work! +const HOME_TODO_POLICY: &str = "!text.contains('Rust')"; // Don't hack on Rust at home! + +// TODOs carry some text and have a kind +#[derive(Clone, Deserialize, Serialize)] +struct Todo { + text: String, + kind: TodoKind, +} + +// TODOs are either for work or for home +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +enum TodoKind { + Work, + Home, +} + +// POST a new TODO to the list +async fn add_todo( + State(AppContext { todos, decider }): State, + Json(todo): Json, +) -> impl IntoResponse { + // Use the policy decider to see if the TODO is allowed + match decider.todo_is_allowed(&todo) { + Ok(is_allowed) => { + // If not, throw HTTP 400 + if !is_allowed { + return StatusCode::BAD_REQUEST; + } + + // If allowed, add it to the TODOs and return HTTP 202 + let mut todos = todos.lock().unwrap(); + todos.push(todo); + StatusCode::ACCEPTED + } + // If there's an error return an HTTP 500 + Err(_) => StatusCode::INTERNAL_SERVER_ERROR, + } +} + +// GET the current list of TODOs +async fn list_todos(State(AppContext { todos, .. }): State) -> impl IntoResponse { + Json(todos.lock().unwrap().clone()) +} + +// The policy engine for our TODOs app +struct PolicyDecider(Context<'static>); + +impl PolicyDecider { + // Start with a wrapper around the default Context + fn new() -> Self { + Self(Context::default()) + } + + // Determine whether a given TODO is allowed + fn todo_is_allowed(&self, todo: &Todo) -> Result { + // Create a new mutable context out of the root context + let mut ctx = self.0.new_inner_scope(); + // Add the TODO's text as a variable so that it can be part of the expression + ctx.add_variable_from_value("text", todo.text.clone()); + + // Which policy to enforce depends on the kind of TODO + let policy = match todo.kind { + TodoKind::Home => HOME_TODO_POLICY, + TodoKind::Work => WORK_TODO_POLICY, + }; + + // Compile the program + let program = Program::compile(policy)?; + + // Execute the program and either return a Boolean or the TODO is + // considered invalid + match program.execute(&ctx)? { + Value::Bool(b) => Ok(b), + _ => Err(TodosError::Invalid), + } + } +} + +// Custom error type +#[derive(Debug, thiserror::Error)] +enum TodosError { + #[error("CEL execution error: {0}")] + Execution(#[from] cel_interpreter::ExecutionError), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("CEL parse error: {0}")] + Parse(#[from] cel_interpreter::ParseError), + #[error("invalid TODO")] + Invalid, +} + +// The state attached to the HTTP router +#[derive(Clone)] +struct AppContext { + todos: Arc>>, + decider: Arc, +} + +#[tokio::main] +async fn main() -> Result<(), TodosError> { + let ctx = AppContext { + todos: Arc::new(Mutex::new(Vec::new())), + decider: Arc::new(PolicyDecider::new()), + }; + + let app = Router::new() + .route("/todos", get(list_todos).post(add_todo)) + .with_state(ctx); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?; + + Ok(axum::serve(listener, app).await?) +} diff --git a/libs/core/cel-rust/example/src/functions.rs b/libs/core/cel-rust/example/src/functions.rs new file mode 100644 index 0000000..497af9e --- /dev/null +++ b/libs/core/cel-rust/example/src/functions.rs @@ -0,0 +1,65 @@ +#![allow(clippy::too_many_arguments)] +use cel_interpreter::extractors::This; +use cel_interpreter::{Context, ExecutionError, FunctionContext, Program, ResolveResult, Value}; +use chrono::{DateTime, Duration, FixedOffset}; +use std::sync::Arc; + +fn main() { + let program = Program::compile("add(2, 3) == 5 && ''.isEmpty() && fail()").unwrap(); + let mut context = Context::default(); + + // Add functions using closures + context.add_function("add", |a: i64, b: i64| a + b); + + // Add methods to a string type + context.add_function("isEmpty", is_empty); + + // Use the function context to return error messages + context.add_function("fail", fail); + + // See all the different value types you can accept in your functions + context.add_function("primitives", primitives); + + // Run the program + let result = program.execute(&context); + assert!(matches!(result, Err(ExecutionError::FunctionError { .. }))); +} + +/// A method on a string type. When added to the CEL context, this function +/// can be called by running. We use the [`This`] extractor give us a reference +/// to the string that this method was called on. +/// +/// ```skip +/// "foo".isEmpty() +/// ``` +fn is_empty(This(s): This>) -> bool { + s.is_empty() +} + +/// A function that gives us access to the [`FunctionContext`]. All functions have +/// can accept this context as their first argument. The context is helpful for +/// several reasons: +/// 1. Creating shadowed variables using cloned contexts (see the `filter` and `map` +/// functions for example). +/// 2. Functions that are fallible can return an error message using ftx.error(...) +/// which attaches some helpful context for the error. +fn fail(ftx: &FunctionContext) -> ResolveResult { + ftx.error("This function always fails").into() +} + +/// A function that illustrates all the different types of values that are supported +/// as arguments to a function, as well as the fact that any of these types can also +/// be returned from a function. +fn primitives( + _a: i64, + _b: u64, + _c: f64, + _d: bool, + _e: Arc, + _f: Arc>, + _g: Duration, + _h: DateTime, + _i: Arc>, +) -> Duration { + Duration::zero() +} diff --git a/libs/core/cel-rust/example/src/json.rs b/libs/core/cel-rust/example/src/json.rs new file mode 100644 index 0000000..5d233ea --- /dev/null +++ b/libs/core/cel-rust/example/src/json.rs @@ -0,0 +1,12 @@ +use cel_interpreter::{Context, Program}; + +fn main() { + // Create a CEL program that returns a JSON object + let program = Program::compile("{'foo': true}").unwrap(); + let value = program.execute(&Context::default()).unwrap(); + + // Convert the return type to JSON and cast to object + let json = value.json().unwrap(); + let object = json.as_object().unwrap(); + assert_eq!(Some(&serde_json::Value::Bool(true)), object.get("foo")); +} diff --git a/libs/core/cel-rust/example/src/serde.rs b/libs/core/cel-rust/example/src/serde.rs new file mode 100644 index 0000000..eee286c --- /dev/null +++ b/libs/core/cel-rust/example/src/serde.rs @@ -0,0 +1,22 @@ +use cel_interpreter::{Context, Program}; +use serde::Serialize; + +// An example struct that derives Serialize +#[derive(Serialize)] +struct MyStruct { + a: i32, + b: i32, +} + +fn main() { + let program = Program::compile("foo.a == foo.b").unwrap(); + let mut context = Context::default(); + + // MyStruct will be implicitly serialized into the CEL appropriate types + context + .add_variable("foo", MyStruct { a: 1, b: 1 }) + .unwrap(); + + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); +} diff --git a/libs/core/cel-rust/example/src/simple.rs b/libs/core/cel-rust/example/src/simple.rs new file mode 100644 index 0000000..1dae526 --- /dev/null +++ b/libs/core/cel-rust/example/src/simple.rs @@ -0,0 +1,8 @@ +use cel_interpreter::{Context, Program}; + +fn main() { + let program = Program::compile("1 == 1").unwrap(); + let context = Context::default(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); +} diff --git a/libs/core/cel-rust/example/src/threads.rs b/libs/core/cel-rust/example/src/threads.rs new file mode 100644 index 0000000..080e9ed --- /dev/null +++ b/libs/core/cel-rust/example/src/threads.rs @@ -0,0 +1,23 @@ +use cel_interpreter::{Context, Program}; +use std::thread::scope; + +fn main() { + let program = Program::compile("a + b").unwrap(); + + scope(|scope| { + scope.spawn(|| { + let mut context = Context::default(); + context.add_variable("a", 1).unwrap(); + context.add_variable("b", 2).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, 3.into()); + }); + scope.spawn(|| { + let mut context = Context::default(); + context.add_variable("a", 2).unwrap(); + context.add_variable("b", 4).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, 6.into()); + }); + }); +} diff --git a/libs/core/cel-rust/example/src/variables.rs b/libs/core/cel-rust/example/src/variables.rs new file mode 100644 index 0000000..9cd6ad9 --- /dev/null +++ b/libs/core/cel-rust/example/src/variables.rs @@ -0,0 +1,10 @@ +use cel_interpreter::{Context, Program}; + +fn main() { + let program = Program::compile("foo * 2").unwrap(); + let mut context = Context::default(); + context.add_variable("foo", 10).unwrap(); + + let value = program.execute(&context).unwrap(); + assert_eq!(value, 20.into()); +} diff --git a/libs/core/cel-rust/interpreter/CHANGELOG.md b/libs/core/cel-rust/interpreter/CHANGELOG.md new file mode 100644 index 0000000..5f4318e --- /dev/null +++ b/libs/core/cel-rust/interpreter/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.9.0](https://github.com/clarkmcc/cel-rust/compare/cel-interpreter-v0.8.1...cel-interpreter-v0.9.0) - 2024-10-30 + +### Other + +- Support `.map` over map ([#105](https://github.com/clarkmcc/cel-rust/pull/105)) +- Detailed parse error ([#102](https://github.com/clarkmcc/cel-rust/pull/102)) +- Fix `clippy::too_long_first_doc_paragraph` lints. ([#101](https://github.com/clarkmcc/cel-rust/pull/101)) +- Support empty/default contexts, put chrono/regex behind features ([#97](https://github.com/clarkmcc/cel-rust/pull/97)) +- Fix `clippy::empty_line_after_doc_comments` lints ([#98](https://github.com/clarkmcc/cel-rust/pull/98)) +- Allow `.size()` method on types ([#88](https://github.com/clarkmcc/cel-rust/pull/88)) +- Conformance test fixes ([#79](https://github.com/clarkmcc/cel-rust/pull/79)) +- Convert CEL values to JSON ([#77](https://github.com/clarkmcc/cel-rust/pull/77)) diff --git a/libs/core/cel-rust/interpreter/Cargo.toml b/libs/core/cel-rust/interpreter/Cargo.toml new file mode 100644 index 0000000..ba65b53 --- /dev/null +++ b/libs/core/cel-rust/interpreter/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "cel-interpreter" +description = "An interpreter for the Common Expression Language (CEL)" +repository = "https://github.com/clarkmcc/cel-rust" +version = "0.9.0" +authors = ["Tom Forbes ", "Clark McCauley "] +edition = "2021" +license = "MIT" +categories = ["compilers"] + +[dependencies] +cel-parser = { path = "../parser", version = "0.8.0" } + +nom = "7.1.3" + +chrono = { version = "0.4", default-features = false, features = ["alloc", "serde"], optional = true } +regex = { version = "1.10.5", optional = true } +serde = "1.0" +serde_json = { version = "1.0", optional = true } +base64 = { version = "0.22.1", optional = true } + +thiserror = "1.0" +paste = "1.0" + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } +serde_bytes = "0.11.14" + +[[bench]] +name = "runtime" +harness = false + +[features] +default = ["regex", "chrono"] +json = ["dep:serde_json", "dep:base64"] +regex = ["dep:regex"] +chrono = ["dep:chrono"] diff --git a/libs/core/cel-rust/interpreter/README.md b/libs/core/cel-rust/interpreter/README.md new file mode 100644 index 0000000..9dcbb46 --- /dev/null +++ b/libs/core/cel-rust/interpreter/README.md @@ -0,0 +1,54 @@ +# CEL Interpreter + +[![Rust](https://github.com/clarkmcc/cel-rust/actions/workflows/rust.yml/badge.svg)](https://github.com/clarkmcc/cel-rust/actions/workflows/rust.yml) + +The [Common Expression Language (CEL)](https://github.com/google/cel-spec) is a non-Turing complete language designed +for simplicity, speed, safety, and +portability. CEL's C-like syntax looks nearly identical to equivalent expressions in C++, Go, Java, and TypeScript. CEL +is ideal for lightweight expression evaluation when a fully sandboxed scripting language is too resource intensive. + +```java +// Check whether a resource name starts with a group name. +resource.name.startsWith("/groups/" + auth.claims.group) +``` + +```go +// Determine whether the request is in the permitted time window. +request.time - resource.age < duration("24h") +``` + +```typescript +// Check whether all resource names in a list match a given filter. +auth.claims.email_verified && resources.all(r, r.startsWith(auth.claims.email)) +``` + +## Getting Started + +This project includes a parser and an interpreter which means that it can be used to evaluate CEL-expressions. The +library aims to be very simple to use, while still being fast, safe, and customizable. + +```rust +use cel_interpreter::{Context, Program}; + +fn main() { + // Compile a CEL program + let program = Program::compile("add(2, 3)").unwrap(); + + // Add any variables or functions that the program will need + let mut context = Context::default(); + context.add_function("add", |a: i64, b: i64| a + b); + + // Run the program + let value = program.execute(&context).unwrap(); + assert_eq!(value, 5.into()); +} +``` + +### Examples + +Check out these other examples to learn how to use this library: + +- [Simple](https://github.com/clarkmcc/cel-rust/blob/master/example/src/simple.rs) - A simple example of how to use the library. +- [Variables](https://github.com/clarkmcc/cel-rust/blob/master/example/src/variables.rs) - Passing variables and using them in your program. +- [Functions](https://github.com/clarkmcc/cel-rust/blob/master/example/src/functions.rs) - Defining and using custom functions in your program. +- [Concurrent Execution](https://github.com/clarkmcc/cel-rust/blob/master/example/src/threads.rs) - Executing the same program concurrently. diff --git a/libs/core/cel-rust/interpreter/benches/runtime.rs b/libs/core/cel-rust/interpreter/benches/runtime.rs new file mode 100644 index 0000000..8289fb6 --- /dev/null +++ b/libs/core/cel-rust/interpreter/benches/runtime.rs @@ -0,0 +1,70 @@ +use cel_interpreter::context::Context; +use cel_interpreter::Program; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::collections::HashMap; + +pub fn criterion_benchmark(c: &mut Criterion) { + let expressions = vec![ + ("ternary_1", "(1 || 2) ? 1 : 2"), + ("ternary_2", "(1 ? 2 : 3) ? 1 : 2"), + ("or_1", "1 || 2"), + ("and_1", "1 && 2"), + ("and_2", "1 && (false ? 2 : 3)"), + ("number", "1"), + ("construct_list", "[1,2,3]"), + ("construct_list_1", "[1]"), + ("construct_list_2", "[1, 2]"), + ("add_list", "[1,2,3] + [4, 5, 6]"), + ("list_element", "[1,2,3][1]"), + ("construct_dict", "{1: 2, '3': '4'}"), + ("add_string", "'abc' + 'def'"), + ("list", "[1,2,3, Now, ]"), + ("mapexpr", "{1 + a: 3}"), + ("map_merge", "{'a': 1} + {'a': 2, 'b': 3}"), + ("size_list", "[1].size()"), + ("size_list_1", "size([1])"), + ("size_str", "'a'.size()"), + ("size_str_2", "size('a')"), + ("size_map", "{1:2}.size()"), + ("size_map_2", "size({1:2})"), + ("map has", "has(foo.bar.baz)"), + ("map macro", "[1, 2, 3].map(x, x * 2)"), + ("filter macro", "[1, 2, 3].filter(x, x > 2)"), + ("all macro", "[1, 2, 3].all(x, x > 0)"), + ("all map macro", "{0: 0, 1:1, 2:2}.all(x, x >= 0)"), + ("max", "max(1, 2, 3)"), + ("max negative", "max(-1, 0, 1)"), + ("max float", "max(-1.0, 0.0, 1.0)"), + ("duration", "duration('1s')"), + ("timestamp", "timestamp('2023-05-28T00:00:00Z')"), // ("complex", "Account{user_id: 123}.user_id == 123"), + ]; + // https://gist.github.com/rhnvrm/db4567fcd87b2cb8e997999e1366d406 + + for (name, expr) in black_box(&expressions) { + c.bench_function(name, |b| { + let program = Program::compile(expr).expect("Parsing failed"); + let mut ctx = Context::default(); + ctx.add_variable_from_value("foo", HashMap::from([("bar", 1)])); + b.iter(|| program.execute(&ctx)) + }); + } +} + +pub fn map_macro_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("map list"); + let sizes = vec![1, 10, 100, 1000, 10000, 100000]; + + for size in sizes { + group.bench_function(format!("map_{}", size).as_str(), |b| { + let list = (0..size).collect::>(); + let program = Program::compile("list.map(x, x * 2)").unwrap(); + let mut ctx = Context::default(); + ctx.add_variable_from_value("list", list); + b.iter(|| program.execute(&ctx).unwrap()) + }); + } + group.finish(); +} + +criterion_group!(benches, criterion_benchmark, map_macro_benchmark); +criterion_main!(benches); diff --git a/libs/core/cel-rust/interpreter/src/context.rs b/libs/core/cel-rust/interpreter/src/context.rs new file mode 100644 index 0000000..96c3bf3 --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/context.rs @@ -0,0 +1,207 @@ +use crate::magic::{Function, FunctionRegistry, Handler}; +use crate::objects::{TryIntoValue, Value}; +use crate::{functions, ExecutionError}; +use cel_parser::Expression; +use std::collections::HashMap; + +/// Context is a collection of variables and functions that can be used +/// by the interpreter to resolve expressions. +/// +/// The context can be either a parent context, or a child context. A +/// parent context is created by default and contains all of the built-in +/// functions. A child context can be created by calling `.clone()`. The +/// child context has it's own variables (which can be added to), but it +/// will also reference the parent context. This allows for variables to +/// be overridden within the child context while still being able to +/// resolve variables in the child's parents. You can have theoretically +/// have an infinite number of child contexts that reference each-other. +/// +/// So why is this important? Well some CEL-macros such as the `.map` macro +/// declare intermediate user-specified identifiers that should only be +/// available within the macro, and should not override variables in the +/// parent context. The `.map` macro can clone the parent context, add the +/// intermediate identifier to the child context, and then evaluate the +/// map expression. +/// +/// Intermediate variable stored in child context +/// โ†“ +/// [1, 2, 3].map(x, x * 2) == [2, 4, 6] +/// โ†‘ +/// Only in scope for the duration of the map expression +/// +pub enum Context<'a> { + Root { + functions: FunctionRegistry, + variables: HashMap, + }, + Child { + parent: &'a Context<'a>, + variables: HashMap, + }, +} + +impl Context<'_> { + pub fn add_variable( + &mut self, + name: S, + value: V, + ) -> Result<(), ::Error> + where + S: Into, + V: TryIntoValue, + { + match self { + Context::Root { variables, .. } => { + variables.insert(name.into(), value.try_into_value()?); + } + Context::Child { variables, .. } => { + variables.insert(name.into(), value.try_into_value()?); + } + } + Ok(()) + } + + pub fn add_variable_from_value(&mut self, name: S, value: V) + where + S: Into, + V: Into, + { + match self { + Context::Root { variables, .. } => { + variables.insert(name.into(), value.into()); + } + Context::Child { variables, .. } => { + variables.insert(name.into(), value.into()); + } + } + } + + pub fn get_variable(&self, name: S) -> Result + where + S: Into, + { + let name = name.into(); + match self { + Context::Child { variables, parent } => variables + .get(&name) + .cloned() + .or_else(|| parent.get_variable(&name).ok()) + .ok_or_else(|| ExecutionError::UndeclaredReference(name.into())), + Context::Root { variables, .. } => variables + .get(&name) + .cloned() + .ok_or_else(|| ExecutionError::UndeclaredReference(name.into())), + } + } + + pub(crate) fn has_function(&self, name: S) -> bool + where + S: Into, + { + let name = name.into(); + match self { + Context::Root { functions, .. } => functions.has(&name), + Context::Child { parent, .. } => parent.has_function(name), + } + } + + pub(crate) fn get_function(&self, name: S) -> Option> + where + S: Into, + { + let name = name.into(); + match self { + Context::Root { functions, .. } => functions.get(&name), + Context::Child { parent, .. } => parent.get_function(name), + } + } + + pub fn add_function(&mut self, name: &str, value: F) + where + F: Handler + 'static + Send + Sync, + { + if let Context::Root { functions, .. } = self { + functions.add(name, value); + }; + } + + pub fn resolve(&self, expr: &Expression) -> Result { + Value::resolve(expr, self) + } + + pub fn resolve_all(&self, exprs: &[Expression]) -> Result { + Value::resolve_all(exprs, self) + } + + pub fn new_inner_scope(&self) -> Context { + Context::Child { + parent: self, + variables: Default::default(), + } + } + + /// Constructs a new empty context with no variables or functions. + /// + /// If you're looking for a context that has all the standard methods, functions + /// and macros already added to the context, use [`Context::default`] instead. + /// + /// # Example + /// ``` + /// use cel_interpreter::Context; + /// let mut context = Context::empty(); + /// context.add_function("add", |a: i64, b: i64| a + b); + /// ``` + pub fn empty() -> Self { + Context::Root { + variables: Default::default(), + functions: Default::default(), + } + } +} + +impl Default for Context<'_> { + fn default() -> Self { + let mut ctx = Context::Root { + variables: Default::default(), + functions: Default::default(), + }; + + ctx.add_function("contains", functions::contains); + ctx.add_function("size", functions::size); + ctx.add_function("has", functions::has); + ctx.add_function("map", functions::map); + ctx.add_function("filter", functions::filter); + ctx.add_function("all", functions::all); + ctx.add_function("max", functions::max); + ctx.add_function("startsWith", functions::starts_with); + ctx.add_function("endsWith", functions::ends_with); + ctx.add_function("string", functions::string); + ctx.add_function("bytes", functions::bytes); + ctx.add_function("double", functions::double); + ctx.add_function("exists", functions::exists); + ctx.add_function("exists_one", functions::exists_one); + ctx.add_function("int", functions::int); + ctx.add_function("uint", functions::uint); + + #[cfg(feature = "regex")] + ctx.add_function("matches", functions::matches); + + #[cfg(feature = "chrono")] + { + ctx.add_function("duration", functions::duration); + ctx.add_function("timestamp", functions::timestamp); + ctx.add_function("getFullYear", functions::time::timestamp_year); + ctx.add_function("getMonth", functions::time::timestamp_month); + ctx.add_function("getDayOfYear", functions::time::timestamp_year_day); + ctx.add_function("getDayOfMonth", functions::time::timestamp_month_day); + ctx.add_function("getDate", functions::time::timestamp_date); + ctx.add_function("getDayOfWeek", functions::time::timestamp_weekday); + ctx.add_function("getHours", functions::time::timestamp_hours); + ctx.add_function("getMinutes", functions::time::timestamp_minutes); + ctx.add_function("getSeconds", functions::time::timestamp_seconds); + ctx.add_function("getMilliseconds", functions::time::timestamp_millis); + } + + ctx + } +} diff --git a/libs/core/cel-rust/interpreter/src/duration.rs b/libs/core/cel-rust/interpreter/src/duration.rs new file mode 100644 index 0000000..31b53a2 --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/duration.rs @@ -0,0 +1,281 @@ +use chrono::Duration; +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::character::complete::char; +use nom::combinator::{map, opt}; +use nom::multi::many1; +use nom::number::complete::double; +use nom::IResult; + +// Constants representing time units in nanoseconds +const SECOND: u64 = 1_000_000_000; +const MILLISECOND: u64 = 1_000_000; +const MICROSECOND: u64 = 1_000; + +/// Parses a duration string into a [`Duration`]. Duration strings support the +/// following grammar: +/// +/// DurationString -> Sign? Number Unit String? +/// Sign -> '-' +/// Number -> Digit+ ('.' Digit+)? +/// Digit -> '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' +/// Unit -> 'h' | 'm' | 's' | 'ms' | 'us' | 'ns' +/// String -> DurationString +/// +/// # Examples +/// - `1h` parses as 1 hour +/// - `1.5h` parses as 1 hour and 30 minutes +/// - `1h30m` parses as 1 hour and 30 minutes +/// - `1h30m1s` parses as 1 hour, 30 minutes, and 1 second +/// - `1ms` parses as 1 millisecond +/// - `1.5ms` parses as 1 millisecond and 500 microseconds +/// - `1ns` parses as 1 nanosecond +/// - `1.5ns` parses as 1 nanosecond (sub-nanosecond durations not supported) +pub fn parse_duration(i: &str) -> IResult<&str, Duration> { + let (i, neg) = opt(parse_negative)(i)?; + if i == "0" { + return Ok((i, Duration::zero())); + } + let (i, duration) = many1(parse_number_unit)(i) + .map(|(i, d)| (i, d.iter().fold(Duration::zero(), |acc, next| acc + *next)))?; + Ok((i, duration * if neg.is_some() { -1 } else { 1 })) +} + +enum Unit { + Nanosecond, + Microsecond, + Millisecond, + Second, + Minute, + Hour, +} + +impl Unit { + fn nanos(&self) -> i64 { + match self { + Unit::Nanosecond => 1, + Unit::Microsecond => 1_000, + Unit::Millisecond => 1_000_000, + Unit::Second => 1_000_000_000, + Unit::Minute => 60 * 1_000_000_000, + Unit::Hour => 60 * 60 * 1_000_000_000, + } + } +} + +fn parse_number_unit(i: &str) -> IResult<&str, Duration> { + let (i, num) = double(i)?; + let (i, unit) = parse_unit(i)?; + let duration = to_duration(num, unit); + Ok((i, duration)) +} + +fn parse_negative(i: &str) -> IResult<&str, ()> { + let (i, _): (&str, char) = char('-')(i)?; + Ok((i, ())) +} + +fn parse_unit(i: &str) -> IResult<&str, Unit> { + alt(( + map(tag("ms"), |_| Unit::Millisecond), + map(tag("us"), |_| Unit::Microsecond), + map(tag("ns"), |_| Unit::Nanosecond), + map(char('h'), |_| Unit::Hour), + map(char('m'), |_| Unit::Minute), + map(char('s'), |_| Unit::Second), + ))(i) +} + +fn to_duration(num: f64, unit: Unit) -> Duration { + Duration::nanoseconds((num * unit.nanos() as f64).trunc() as i64) +} + +/// Formats a [`Duration`] into a string. String returns a string representing the +/// duration in the form "72h3m0.5s". Leading zero units are omitted. As a special +/// case, durations less than one second format use a smaller unit (milli-, micro-, +/// or nanoseconds) to ensure that the leading digit is non-zero. The zero duration +/// formats as 0s. +/// +/// This is a direct port of the Go version of the time.Duration(0).String() function. +pub fn format_duration(d: &Duration) -> String { + let buf = &mut [0u8; 32]; + let mut w = buf.len(); + + let mut neg = false; + let mut u = d + .num_nanoseconds() + .map(|n| { + if n < 0 { + neg = true; + } + n as u64 + }) + .unwrap_or_else(|| { + let s = d.num_seconds(); + if s < 0 { + neg = true; + } + s as u64 * SECOND + }); + + if u < SECOND { + // Special case: if duration is smaller than a second, + // use smaller units, like 1.2ms + let mut _prec = 0; + w -= 1; + buf[w] = b's'; + w -= 1; + + if u == 0 { + return "0s".to_string(); + } else if u < MICROSECOND { + _prec = 0; + buf[w] = b'n'; + } else if u < MILLISECOND { + _prec = 3; + // U+00B5 'ยต' micro sign == 0xC2 0xB5 + buf[w] = 0xB5; + w -= 1; + buf[w] = 0xC2; + } else { + _prec = 6; + buf[w] = b'm'; + } + (w, u) = format_float(&mut buf[..w], u, _prec); + w = format_int(&mut buf[..w], u); + } else { + w -= 1; + buf[w] = b's'; + (w, u) = format_float(&mut buf[..w], u, 9); + + // u is now integer number of seconds + w = format_int(&mut buf[..w], u % 60); + u /= 60; + + // u is now integer number of minutes + if u > 0 { + w -= 1; + buf[w] = b'm'; + w = format_int(&mut buf[..w], u % 60); + u /= 60; + + // u is now integer number of hours + if u > 0 { + w -= 1; + buf[w] = b'h'; + w = format_int(&mut buf[..w], u); + } + } + } + + if neg { + w -= 1; + buf[w] = b'-'; + } + String::from_utf8_lossy(&buf[w..]).into_owned() +} + +fn format_float(buf: &mut [u8], mut v: u64, prec: usize) -> (usize, u64) { + let mut w = buf.len(); + let mut print = false; + for _ in 0..prec { + let digit = v % 10; + print = print || digit != 0; + if print { + w -= 1; + buf[w] = digit as u8 + b'0'; + } + v /= 10; + } + if print { + w -= 1; + buf[w] = b'.'; + } + (w, v) +} + +fn format_int(buf: &mut [u8], mut v: u64) -> usize { + let mut w = buf.len(); + if v == 0 { + w -= 1; + buf[w] = b'0'; + } else { + while v > 0 { + w -= 1; + buf[w] = (v % 10) as u8 + b'0'; + v /= 10; + } + } + w +} + +#[cfg(test)] +mod tests { + use crate::duration::{format_duration, parse_duration}; + use chrono::Duration; + + fn assert_duration(input: &str, expected: Duration) { + let (_, duration) = parse_duration(input).unwrap(); + assert_eq!(duration, expected, "{}", input); + } + + fn assert_print_duration(input: Duration, expected: &str) { + let actual = format_duration(&input); + assert_eq!(actual, expected, "{}", input); + } + + macro_rules! assert_durations { + ($($str:expr => $duration:expr),*$(,)?) => { + #[test] + fn test_durations() { + $( + assert_duration($str, $duration); + )* + } + }; + } + + macro_rules! assert_duration_format { + ($($duration:expr => $str:expr),*$(,)?) => { + #[test] + fn test_format_durations() { + $( + assert_print_duration($duration, $str); + )* + } + }; + } + + assert_durations! { + "1s" => Duration::seconds(1), + "-1s" => Duration::seconds(-1), + "1.1s" => Duration::seconds(1) + Duration::milliseconds(100), + "1.5m" => Duration::minutes(1) + Duration::seconds(30), + "1m1s" => Duration::minutes(1) + Duration::seconds(1), + "1h1m1s" => Duration::hours(1) + Duration::minutes(1) + Duration::seconds(1), + "1ms" => Duration::milliseconds(1), + "1us" => Duration::microseconds(1), + "1ns" => Duration::nanoseconds(1), + "1.1ns" => Duration::nanoseconds(1), + "1.123us" => Duration::microseconds(1) + Duration::nanoseconds(123), + "0s" => Duration::zero(), + "0h0m0s" => Duration::zero(), + "0h0m1s" => Duration::seconds(1), + "0" => Duration::zero(), + "-0" => Duration::zero(), + } + + assert_duration_format! { + Duration::zero() => "0s", + Duration::nanoseconds(1) => "1ns", + Duration::nanoseconds(1100) => "1.1ยตs", + Duration::microseconds(2200) => "2.2ms", + Duration::milliseconds(3300) => "3.3s", + Duration::minutes(4) + Duration::seconds(5) => "4m5s", + Duration::minutes(4) + Duration::milliseconds(5001) => "4m5.001s", + Duration::hours(5) + Duration::minutes(6) + Duration::milliseconds(7001) => "5h6m7.001s", + Duration::minutes(8) + Duration::nanoseconds(1) => "8m0.000000001s", + Duration::nanoseconds(i64::MAX) => "2562047h47m16.854775807s", + Duration::nanoseconds(i64::MIN) => "-2562047h47m16.854775808s", + } +} diff --git a/libs/core/cel-rust/interpreter/src/functions.rs b/libs/core/cel-rust/interpreter/src/functions.rs new file mode 100644 index 0000000..22696b1 --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/functions.rs @@ -0,0 +1,1010 @@ +use crate::context::Context; +use crate::magic::{Arguments, Identifier, This}; +use crate::objects::{Value, ValueType}; +use crate::resolvers::{Argument, Resolver}; +use crate::ExecutionError; +use cel_parser::Expression; +use std::cmp::Ordering; +use std::convert::TryInto; +use std::sync::Arc; + +type Result = std::result::Result; + +/// `FunctionContext` is a context object passed to functions when they are called. +/// +/// It contains references to the target object (if the function is called as +/// a method), the program context ([`Context`]) which gives functions access +/// to variables, and the arguments to the function call. +#[derive(Clone)] +pub struct FunctionContext<'context> { + pub name: Arc, + pub this: Option, + pub ptx: &'context Context<'context>, + pub args: Vec, + pub arg_idx: usize, +} + +impl<'context> FunctionContext<'context> { + pub fn new( + name: Arc, + this: Option, + ptx: &'context Context<'context>, + args: Vec, + ) -> Self { + Self { + name, + this, + ptx, + args, + arg_idx: 0, + } + } + + /// Resolves the given expression using the program's [`Context`]. + pub fn resolve(&self, resolver: R) -> Result + where + R: Resolver, + { + resolver.resolve(self) + } + + /// Returns an execution error for the currently execution function. + pub fn error(&self, message: M) -> ExecutionError { + ExecutionError::function_error(self.name.as_str(), message) + } +} + +/// Calculates the size of either the target, or the provided args depending on how +/// the function is called. +/// +/// If called as a method, the target will be used. If called as a function, the +/// first argument will be used. +/// +/// The following [`Value`] variants are supported: +/// * [`Value::List`] +/// * [`Value::Map`] +/// * [`Value::String`] +/// * [`Value::Bytes`] +/// +/// # Examples +/// ```skip +/// size([1, 2, 3]) == 3 +/// ``` +/// ```skip +/// 'foobar'.size() == 6 +/// ``` +pub fn size(ftx: &FunctionContext, This(this): This) -> Result { + let size = match this { + Value::List(l) => l.len(), + Value::Map(m) => m.map.len(), + Value::String(s) => s.len(), + Value::Bytes(b) => b.len(), + value => return Err(ftx.error(format!("cannot determine the size of {:?}", value))), + }; + Ok(size as i64) +} + +/// Returns true if the target contains the provided argument. The actual behavior +/// depends mainly on the type of the target. +/// +/// The following [`Value`] variants are supported: +/// * [`Value::List`] - Returns true if the list contains the provided value. +/// * [`Value::Map`] - Returns true if the map contains the provided key. +/// * [`Value::String`] - Returns true if the string contains the provided substring. +/// * [`Value::Bytes`] - Returns true if the bytes contain the provided byte. +/// +/// # Example +/// +/// ## List +/// ```cel +/// [1, 2, 3].contains(1) == true +/// ``` +/// +/// ## Map +/// ```cel +/// {"a": 1, "b": 2, "c": 3}.contains("a") == true +/// ``` +/// +/// ## String +/// ```cel +/// "abc".contains("b") == true +/// ``` +/// +/// ## Bytes +/// ```cel +/// b"abc".contains(b"c") == true +/// ``` +pub fn contains(This(this): This, arg: Value) -> Result { + Ok(match this { + Value::List(v) => v.contains(&arg), + Value::Map(v) => v + .map + .contains_key(&arg.try_into().map_err(ExecutionError::UnsupportedKeyType)?), + Value::String(s) => { + if let Value::String(arg) = arg { + s.contains(arg.as_str()) + } else { + false + } + } + Value::Bytes(b) => { + if let Value::Bytes(arg) = arg { + let s = arg.as_slice(); + b.windows(arg.len()).any(|w| w == s) + } else { + false + } + } + _ => false, + } + .into()) +} + +// Performs a type conversion on the target. The following conversions are currently +// supported: +// * `string` - Returns a copy of the target string. +// * `timestamp` - Returns the timestamp in RFC3339 format. +// * `duration` - Returns the duration in a string formatted like "72h3m0.5s". +// * `int` - Returns the integer value of the target. +// * `uint` - Returns the unsigned integer value of the target. +// * `float` - Returns the float value of the target. +// * `bytes` - Converts bytes to string using from_utf8_lossy. +pub fn string(ftx: &FunctionContext, This(this): This) -> Result { + Ok(match this { + Value::String(v) => Value::String(v.clone()), + #[cfg(feature = "chrono")] + Value::Timestamp(t) => Value::String(t.to_rfc3339().into()), + #[cfg(feature = "chrono")] + Value::Duration(v) => Value::String(crate::duration::format_duration(&v).into()), + Value::Int(v) => Value::String(v.to_string().into()), + Value::UInt(v) => Value::String(v.to_string().into()), + Value::Float(v) => Value::String(v.to_string().into()), + Value::Bytes(v) => Value::String(Arc::new(String::from_utf8_lossy(v.as_slice()).into())), + v => return Err(ftx.error(format!("cannot convert {:?} to string", v))), + }) +} + +pub fn bytes(value: Arc) -> Result { + Ok(Value::Bytes(value.as_bytes().to_vec().into())) +} + +// Performs a type conversion on the target. +pub fn double(ftx: &FunctionContext, This(this): This) -> Result { + Ok(match this { + Value::String(v) => v + .parse::() + .map(Value::Float) + .map_err(|e| ftx.error(format!("string parse error: {e}")))?, + Value::Float(v) => Value::Float(v), + Value::Int(v) => Value::Float(v as f64), + Value::UInt(v) => Value::Float(v as f64), + v => return Err(ftx.error(format!("cannot convert {:?} to double", v))), + }) +} + +// Performs a type conversion on the target. +pub fn uint(ftx: &FunctionContext, This(this): This) -> Result { + Ok(match this { + Value::String(v) => v + .parse::() + .map(Value::UInt) + .map_err(|e| ftx.error(format!("string parse error: {e}")))?, + Value::Float(v) => { + if v > u64::MAX as f64 || v < u64::MIN as f64 { + return Err(ftx.error("unsigned integer overflow")); + } + Value::UInt(v as u64) + } + Value::Int(v) => Value::UInt( + v.try_into() + .map_err(|_| ftx.error("unsigned integer overflow"))?, + ), + Value::UInt(v) => Value::UInt(v), + v => return Err(ftx.error(format!("cannot convert {:?} to uint", v))), + }) +} + +// Performs a type conversion on the target. +pub fn int(ftx: &FunctionContext, This(this): This) -> Result { + Ok(match this { + Value::String(v) => v + .parse::() + .map(Value::Int) + .map_err(|e| ftx.error(format!("string parse error: {e}")))?, + Value::Float(v) => { + if v > i64::MAX as f64 || v < i64::MIN as f64 { + return Err(ftx.error("integer overflow")); + } + Value::Int(v as i64) + } + Value::Int(v) => Value::Int(v), + Value::UInt(v) => Value::Int(v.try_into().map_err(|_| ftx.error("integer overflow"))?), + v => return Err(ftx.error(format!("cannot convert {:?} to int", v))), + }) +} + +/// Returns true if a string starts with another string. +/// +/// # Example +/// ```cel +/// "abc".startsWith("a") == true +/// ``` +pub fn starts_with(This(this): This>, prefix: Arc) -> bool { + this.starts_with(prefix.as_str()) +} + +/// Returns true if a string ends with another string. +/// +/// # Example +/// ```cel +/// "abc".endsWith("c") == true +/// ``` +pub fn ends_with(This(this): This>, suffix: Arc) -> bool { + this.ends_with(suffix.as_str()) +} + +/// Returns true if a string matches the regular expression. +/// +/// # Example +/// ```cel +/// "abc".matches("^[a-z]*$") == true +/// ``` +#[cfg(feature = "regex")] +pub fn matches( + ftx: &FunctionContext, + This(this): This>, + regex: Arc, +) -> Result { + match regex::Regex::new(®ex) { + Ok(re) => Ok(re.is_match(&this)), + Err(err) => Err(ftx.error(format!("'{regex}' not a valid regex:\n{err}"))), + } +} + +/// Returns true if the provided argument can be resolved. +/// +/// This function is useful for checking if a property exists on a type before +/// attempting to resolve it. Resolving a property that does not exist will +/// result in a [`ExecutionError::NoSuchKey`] error. +/// +/// Operates similar to the `has` macro describe in the Go CEL implementation +/// spec: . +/// +/// # Examples +/// ```cel +/// has(foo.bar.baz) +/// ``` +pub fn has(ftx: &FunctionContext) -> Result { + // We determine if a type has a property by attempting to resolve it. + // If we get a NoSuchKey error, then we know the property does not exist + match ftx.resolve(Argument(0)) { + Ok(_) => Value::Bool(true), + Err(err) => match err { + ExecutionError::NoSuchKey(_) => Value::Bool(false), + _ => return Err(err), + }, + } + .into() +} + +/// Maps the provided list to a new list by applying an expression to each +/// input item. +/// +/// This function is intended to be used like the CEL-go `map` macro: +/// +/// +/// # Examples +/// ```cel +/// [1, 2, 3].map(x, x * 2) == [2, 4, 6] +/// ``` +pub fn map( + ftx: &FunctionContext, + This(this): This, + ident: Identifier, + expr: Expression, +) -> Result { + match this { + Value::List(items) => { + let mut values = Vec::with_capacity(items.len()); + let mut ptx = ftx.ptx.new_inner_scope(); + for item in items.iter() { + ptx.add_variable_from_value(ident.clone(), item.clone()); + let value = ptx.resolve(&expr)?; + values.push(value); + } + Value::List(Arc::new(values)) + } + Value::Map(map) => { + let mut values = Vec::with_capacity(map.map.len()); + let mut ptx = ftx.ptx.new_inner_scope(); + for (key, _) in map.map.iter() { + ptx.add_variable_from_value(ident.clone(), key.clone()); + let value = ptx.resolve(&expr)?; + values.push(value); + } + Value::List(Arc::new(values)) + } + _ => return Err(this.error_expected_type(ValueType::List)), + } + .into() +} + +/// Filters the provided list by applying an expression to each input item +/// and including the input item in the resulting list, only if the expression +/// returned true. +/// +/// This function is intended to be used like the CEL-go `filter` macro: +/// +/// +/// # Example +/// ```cel +/// [1, 2, 3].filter(x, x > 1) == [2, 3] +/// ``` +pub fn filter( + ftx: &FunctionContext, + This(this): This, + ident: Identifier, + expr: Expression, +) -> Result { + match this { + Value::List(items) => { + let mut values = Vec::with_capacity(items.len()); + let mut ptx = ftx.ptx.new_inner_scope(); + for item in items.iter() { + ptx.add_variable_from_value(ident.clone(), item.clone()); + if let Value::Bool(true) = ptx.resolve(&expr)? { + values.push(item.clone()); + } + } + Value::List(Arc::new(values)) + } + _ => return Err(this.error_expected_type(ValueType::List)), + } + .into() +} + +/// Returns a boolean value indicating whether every value in the provided +/// list or map met the predicate defined by the provided expression. If +/// called on a map, the predicate is applied to the map keys. +/// +/// This function is intended to be used like the CEL-go `all` macro: +/// +/// +/// # Example +/// ```cel +/// [1, 2, 3].all(x, x > 0) == true +/// [{1:true, 2:true, 3:false}].all(x, x > 0) == true +/// ``` +pub fn all( + ftx: &FunctionContext, + This(this): This, + ident: Identifier, + expr: Expression, +) -> Result { + match this { + Value::List(items) => { + let mut ptx = ftx.ptx.new_inner_scope(); + for item in items.iter() { + ptx.add_variable_from_value(&ident, item); + if let Value::Bool(false) = ptx.resolve(&expr)? { + return Ok(false); + } + } + Ok(true) + } + Value::Map(value) => { + let mut ptx = ftx.ptx.new_inner_scope(); + for key in value.map.keys() { + ptx.add_variable_from_value(&ident, key); + if let Value::Bool(false) = ptx.resolve(&expr)? { + return Ok(false); + } + } + Ok(true) + } + _ => Err(this.error_expected_type(ValueType::List)), + } +} + +/// Returns a boolean value indicating whether a or more values in the provided +/// list or map meet the predicate defined by the provided expression. +/// +/// If called on a map, the predicate is applied to the map keys. +/// +/// This function is intended to be used like the CEL-go `exists` macro: +/// +/// +/// # Example +/// ```cel +/// [1, 2, 3].exists(x, x > 0) == true +/// [{1:true, 2:true, 3:false}].exists(x, x > 0) == true +/// ``` +pub fn exists( + ftx: &FunctionContext, + This(this): This, + ident: Identifier, + expr: Expression, +) -> Result { + match this { + Value::List(items) => { + let mut ptx = ftx.ptx.new_inner_scope(); + for item in items.iter() { + ptx.add_variable_from_value(&ident, item); + if let Value::Bool(true) = ptx.resolve(&expr)? { + return Ok(true); + } + } + Ok(false) + } + Value::Map(value) => { + let mut ptx = ftx.ptx.new_inner_scope(); + for key in value.map.keys() { + ptx.add_variable_from_value(&ident, key); + if let Value::Bool(true) = ptx.resolve(&expr)? { + return Ok(true); + } + } + Ok(false) + } + _ => Err(this.error_expected_type(ValueType::List)), + } +} + +/// Returns a boolean value indicating whether only one value in the provided +/// list or map meets the predicate defined by the provided expression. +/// +/// If called on a map, the predicate is applied to the map keys. +/// +/// This function is intended to be used like the CEL-go `exists` macro: +/// +/// +/// # Example +/// ```cel +/// [1, 2, 3].exists_one(x, x > 0) == false +/// [1, 2, 3].exists_one(x, x == 1) == true +/// [{1:true, 2:true, 3:false}].exists_one(x, x > 0) == false +/// ``` +pub fn exists_one( + ftx: &FunctionContext, + This(this): This, + ident: Identifier, + expr: Expression, +) -> Result { + match this { + Value::List(items) => { + let mut ptx = ftx.ptx.new_inner_scope(); + let mut exists = false; + for item in items.iter() { + ptx.add_variable_from_value(&ident, item); + if let Value::Bool(true) = ptx.resolve(&expr)? { + if exists { + return Ok(false); + } + exists = true; + } + } + Ok(exists) + } + Value::Map(value) => { + let mut ptx = ftx.ptx.new_inner_scope(); + let mut exists = false; + for key in value.map.keys() { + ptx.add_variable_from_value(&ident, key); + if let Value::Bool(true) = ptx.resolve(&expr)? { + if exists { + return Ok(false); + } + exists = true; + } + } + Ok(exists) + } + _ => Err(this.error_expected_type(ValueType::List)), + } +} + +#[cfg(feature = "chrono")] +pub use time::duration; +#[cfg(feature = "chrono")] +pub use time::timestamp; + +#[cfg(feature = "chrono")] +pub mod time { + use super::Result; + use crate::magic::This; + use crate::{ExecutionError, Value}; + use chrono::{Datelike, Days, Months, Timelike}; + use std::sync::Arc; + + /// Duration parses the provided argument into a [`Value::Duration`] value. + /// + /// The argument must be string, and must be in the format of a duration. See + /// the [`parse_duration`] documentation for more information on the supported + /// formats. + /// + /// # Examples + /// - `1h` parses as 1 hour + /// - `1.5h` parses as 1 hour and 30 minutes + /// - `1h30m` parses as 1 hour and 30 minutes + /// - `1h30m1s` parses as 1 hour, 30 minutes, and 1 second + /// - `1ms` parses as 1 millisecond + /// - `1.5ms` parses as 1 millisecond and 500 microseconds + /// - `1ns` parses as 1 nanosecond + /// - `1.5ns` parses as 1 nanosecond (sub-nanosecond durations not supported) + pub fn duration(value: Arc) -> crate::functions::Result { + Ok(Value::Duration(_duration(value.as_str())?)) + } + + /// Timestamp parses the provided argument into a [`Value::Timestamp`] value. + /// The + pub fn timestamp(value: Arc) -> Result { + Ok(Value::Timestamp( + chrono::DateTime::parse_from_rfc3339(value.as_str()) + .map_err(|e| ExecutionError::function_error("timestamp", e.to_string().as_str()))?, + )) + } + + /// A wrapper around [`parse_duration`] that converts errors into [`ExecutionError`]. + /// and only returns the duration, rather than returning the remaining input. + fn _duration(i: &str) -> Result { + let (_, duration) = crate::duration::parse_duration(i) + .map_err(|e| ExecutionError::function_error("duration", e.to_string()))?; + Ok(duration) + } + + fn _timestamp(i: &str) -> Result> { + chrono::DateTime::parse_from_rfc3339(i) + .map_err(|e| ExecutionError::function_error("timestamp", e.to_string())) + } + + pub fn timestamp_year( + This(this): This>, + ) -> Result { + Ok(this.year().into()) + } + + pub fn timestamp_month( + This(this): This>, + ) -> Result { + Ok((this.month0() as i32).into()) + } + + pub fn timestamp_year_day( + This(this): This>, + ) -> Result { + let year = this + .checked_sub_days(Days::new(this.day0() as u64)) + .unwrap() + .checked_sub_months(Months::new(this.month0())) + .unwrap(); + Ok(this.signed_duration_since(year).num_days().into()) + } + + pub fn timestamp_month_day( + This(this): This>, + ) -> Result { + Ok((this.day0() as i32).into()) + } + + pub fn timestamp_date( + This(this): This>, + ) -> Result { + Ok((this.day() as i32).into()) + } + + pub fn timestamp_weekday( + This(this): This>, + ) -> Result { + Ok((this.weekday().num_days_from_sunday() as i32).into()) + } + + pub fn timestamp_hours( + This(this): This>, + ) -> Result { + Ok((this.hour() as i32).into()) + } + + pub fn timestamp_minutes( + This(this): This>, + ) -> Result { + Ok((this.minute() as i32).into()) + } + + pub fn timestamp_seconds( + This(this): This>, + ) -> Result { + Ok((this.second() as i32).into()) + } + + pub fn timestamp_millis( + This(this): This>, + ) -> Result { + Ok((this.timestamp_subsec_millis() as i32).into()) + } +} + +pub fn max(Arguments(args): Arguments) -> Result { + // If items is a list of values, then operate on the list + let items = if args.len() == 1 { + match &args[0] { + Value::List(values) => values, + _ => return Ok(args[0].clone()), + } + } else { + &args + }; + + items + .iter() + .skip(1) + .try_fold(items.first().unwrap_or(&Value::Null), |acc, x| { + match acc.partial_cmp(x) { + Some(Ordering::Greater) => Ok(acc), + Some(_) => Ok(x), + None => Err(ExecutionError::ValuesNotComparable(acc.clone(), x.clone())), + } + }) + .cloned() +} + +#[cfg(test)] +mod tests { + use crate::context::Context; + use crate::tests::test_script; + + fn assert_script(input: &(&str, &str)) { + assert_eq!(test_script(input.1, None), Ok(true.into()), "{}", input.0); + } + + #[test] + fn test_size() { + [ + ("size of list", "size([1, 2, 3]) == 3"), + ("size of map", "size({'a': 1, 'b': 2, 'c': 3}) == 3"), + ("size of string", "size('foo') == 3"), + ("size of bytes", "size(b'foo') == 3"), + ("size as a list method", "[1, 2, 3].size() == 3"), + ("size as a string method", "'foobar'.size() == 6"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_has() { + let tests = vec![ + ("map has", "has(foo.bar) == true"), + ("map has", "has(foo.bar) == true"), + ("map not has", "has(foo.baz) == false"), + ("map deep not has", "has(foo.baz.bar) == false"), + ]; + + for (name, script) in tests { + let mut ctx = Context::default(); + ctx.add_variable_from_value("foo", std::collections::HashMap::from([("bar", 1)])); + assert_eq!(test_script(script, Some(ctx)), Ok(true.into()), "{}", name); + } + } + + #[test] + fn test_map() { + [ + ("map list", "[1, 2, 3].map(x, x * 2) == [2, 4, 6]"), + ("map list 2", "[1, 2, 3].map(y, y + 1) == [2, 3, 4]"), + ( + "nested map", + "[[1, 2], [2, 3]].map(x, x.map(x, x * 2)) == [[2, 4], [4, 6]]", + ), + ( + "map to list", + r#"{'John': 'smart'}.map(key, key) == ['John']"#, + ), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_filter() { + [("filter list", "[1, 2, 3].filter(x, x > 2) == [3]")] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_all() { + [ + ("all list #1", "[0, 1, 2].all(x, x >= 0)"), + ("all list #2", "[0, 1, 2].all(x, x > 0) == false"), + ("all map", "{0: 0, 1:1, 2:2}.all(x, x >= 0) == true"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_exists() { + [ + ("exist list #1", "[0, 1, 2].exists(x, x > 0)"), + ("exist list #2", "[0, 1, 2].exists(x, x == 3) == false"), + ("exist list #3", "[0, 1, 2, 2].exists(x, x == 2)"), + ("exist map", "{0: 0, 1:1, 2:2}.exists(x, x > 0)"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_exists_one() { + [ + ("exist list #1", "[0, 1, 2].exists_one(x, x > 0) == false"), + ("exist list #2", "[0, 1, 2].exists_one(x, x == 0)"), + ("exist map", "{0: 0, 1:1, 2:2}.exists_one(x, x == 2)"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_max() { + [ + ("max single", "max(1) == 1"), + ("max multiple", "max(1, 2, 3) == 3"), + ("max negative", "max(-1, 0) == 0"), + ("max float", "max(-1.0, 0.0) == 0.0"), + ("max list", "max([1, 2, 3]) == 3"), + ("max empty list", "max([]) == null"), + ("max no args", "max() == null"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_starts_with() { + [ + ("starts with true", "'foobar'.startsWith('foo') == true"), + ("starts with false", "'foobar'.startsWith('bar') == false"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_ends_with() { + [ + ("ends with true", "'foobar'.endsWith('bar') == true"), + ("ends with false", "'foobar'.endsWith('foo') == false"), + ] + .iter() + .for_each(assert_script); + } + + #[cfg(feature = "chrono")] + #[test] + fn test_timestamp() { + [( + "comparison", + "timestamp('2023-05-29T00:00:00Z') > timestamp('2023-05-28T00:00:00Z')", + ), + ( + "comparison", + "timestamp('2023-05-29T00:00:00Z') < timestamp('2023-05-30T00:00:00Z')", + ), + ( + "subtracting duration", + "timestamp('2023-05-29T00:00:00Z') - duration('24h') == timestamp('2023-05-28T00:00:00Z')", + ), + ( + "subtracting date", + "timestamp('2023-05-29T00:00:00Z') - timestamp('2023-05-28T00:00:00Z') == duration('24h')", + ), + ( + "adding duration", + "timestamp('2023-05-28T00:00:00Z') + duration('24h') == timestamp('2023-05-29T00:00:00Z')", + ), + ( + "timestamp string", + "timestamp('2023-05-28T00:00:00Z').string() == '2023-05-28T00:00:00+00:00'", + ), + ( + "timestamp getFullYear", + "timestamp('2023-05-28T00:00:00Z').getFullYear() == 2023", + ), + ( + "timestamp getMonth", + "timestamp('2023-05-28T00:00:00Z').getMonth() == 4", + ), + ( + "timestamp getDayOfMonth", + "timestamp('2023-05-28T00:00:00Z').getDayOfMonth() == 27", + ), + ( + "timestamp getDayOfYear", + "timestamp('2023-05-28T00:00:00Z').getDayOfYear() == 147", + ), + ( + "timestamp getDate", + "timestamp('2023-05-28T00:00:00Z').getDate() == 28", + ), + ( + "timestamp getDayOfWeek", + "timestamp('2023-05-28T00:00:00Z').getDayOfWeek() == 0", + ), + ( + "timestamp getHours", + "timestamp('2023-05-28T02:00:00Z').getHours() == 2", + ), + ( + "timestamp getMinutes", + " timestamp('2023-05-28T00:05:00Z').getMinutes() == 5", + ), + ( + "timestamp getSeconds", + "timestamp('2023-05-28T00:00:06Z').getSeconds() == 6", + ), + ( + "timestamp getMilliseconds", + "timestamp('2023-05-28T00:00:42.123Z').getMilliseconds() == 123", + ), + + ] + .iter() + .for_each(assert_script); + } + + #[cfg(feature = "chrono")] + #[test] + fn test_duration() { + [ + ("duration equal 1", "duration('1s') == duration('1000ms')"), + ("duration equal 2", "duration('1m') == duration('60s')"), + ("duration equal 3", "duration('1h') == duration('60m')"), + ("duration comparison 1", "duration('1m') > duration('1s')"), + ("duration comparison 2", "duration('1m') < duration('1h')"), + ( + "duration subtraction", + "duration('1h') - duration('1m') == duration('59m')", + ), + ( + "duration addition", + "duration('1h') + duration('1m') == duration('1h1m')", + ), + ] + .iter() + .for_each(assert_script); + } + + #[cfg(feature = "chrono")] + #[test] + fn test_timestamp_variable() { + let mut context = Context::default(); + let ts: chrono::DateTime = + chrono::DateTime::parse_from_rfc3339("2023-05-29T00:00:00Z").unwrap(); + context + .add_variable("ts", crate::Value::Timestamp(ts)) + .unwrap(); + + let program = crate::Program::compile("ts == timestamp('2023-05-29T00:00:00Z')").unwrap(); + let result = program.execute(&context).unwrap(); + assert_eq!(result, true.into()); + } + + #[cfg(feature = "chrono")] + #[test] + fn test_chrono_string() { + [ + ("duration", "duration('1h30m').string() == '1h30m0s'"), + ( + "timestamp", + "timestamp('2023-05-29T00:00:00Z').string() == '2023-05-29T00:00:00+00:00'", + ), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_contains() { + let tests = vec![ + ("list", "[1, 2, 3].contains(3) == true"), + ("map", "{1: true, 2: true, 3: true}.contains(3) == true"), + ("string", "'foobar'.contains('bar') == true"), + ("bytes", "b'foobar'.contains(b'o') == true"), + ]; + + for (name, script) in tests { + assert_eq!(test_script(script, None), Ok(true.into()), "{}", name); + } + } + + #[cfg(feature = "regex")] + #[test] + fn test_matches() { + let tests = vec![ + ("string", "'foobar'.matches('^[a-zA-Z]*$') == true"), + ( + "map", + "{'1': 'abc', '2': 'def', '3': 'ghi'}.all(key, key.matches('^[a-zA-Z]*$')) == false", + ), + ]; + + for (name, script) in tests { + assert_eq!( + test_script(script, None), + Ok(true.into()), + ".matches failed for '{name}'" + ); + } + } + + #[cfg(feature = "regex")] + #[test] + fn test_matches_err() { + assert_eq!( + test_script( + "'foobar'.matches('(foo') == true", None), + Err( + crate::ExecutionError::FunctionError { + function: "matches".to_string(), + message: "'(foo' not a valid regex:\nregex parse error:\n (foo\n ^\nerror: unclosed group".to_string() + } + ) + ); + } + + #[test] + fn test_string() { + [ + ("string", "'foo'.string() == 'foo'"), + ("int", "10.string() == '10'"), + ("float", "10.5.string() == '10.5'"), + ("bytes", "b'foo'.string() == 'foo'"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_bytes() { + [ + ("string", "bytes('abc') == b'abc'"), + ("bytes", "bytes('abc') == b'\\x61b\\x63'"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_double() { + [ + ("string", "'10'.double() == 10.0"), + ("int", "10.double() == 10.0"), + ("double", "10.0.double() == 10.0"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_uint() { + [ + ("string", "'10'.uint() == 10.uint()"), + ("double", "10.5.uint() == 10.uint()"), + ] + .iter() + .for_each(assert_script); + } + + #[test] + fn test_int() { + [ + ("string", "'10'.int() == 10"), + ("int", "10.int() == 10"), + ("uint", "10.uint().int() == 10"), + ("double", "10.5.int() == 10"), + ] + .iter() + .for_each(assert_script); + } +} diff --git a/libs/core/cel-rust/interpreter/src/json.rs b/libs/core/cel-rust/interpreter/src/json.rs new file mode 100644 index 0000000..ac06794 --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/json.rs @@ -0,0 +1,117 @@ +use crate::Value; +use base64::prelude::*; +#[cfg(feature = "chrono")] +use chrono::Duration; +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +#[error("unable to convert value to json: {0:?}")] +pub enum ConvertToJsonError<'a> { + /// We cannot convert the CEL value to JSON. Some CEL types (like functions) are + /// not representable in JSON. + #[error("unable to convert value to json: {0:?}")] + Value(&'a Value), + + #[cfg(feature = "chrono")] + /// The duration is too large to convert to nanoseconds. Any duration of 2^63 + /// nanoseconds or more will overflow. We'll return the duration type in the + /// error message. + #[error("duration too large to convert to nanoseconds: {0:?}")] + DurationOverflow(&'a Duration), +} + +impl Value { + /// Converts a CEL value to a JSON value. + /// + /// # Example + /// ``` + /// use cel_interpreter::{Context, Program}; + /// + /// let program = Program::compile("null").unwrap(); + /// let value = program.execute(&Context::default()).unwrap(); + /// let result = value.json().unwrap(); + /// + /// assert_eq!(result, serde_json::Value::Null); + /// ``` + pub fn json(&self) -> Result { + Ok(match *self { + Value::List(ref vec) => serde_json::Value::Array( + vec.iter() + .map(|v| v.json()) + .collect::, _>>()?, + ), + Value::Map(ref map) => { + let mut obj = serde_json::Map::new(); + for (k, v) in map.map.iter() { + obj.insert(k.to_string(), v.json()?); + } + serde_json::Value::Object(obj) + } + Value::Int(i) => i.into(), + Value::UInt(u) => u.into(), + Value::Float(f) => f.into(), + Value::String(ref s) => s.to_string().into(), + Value::Bool(b) => b.into(), + Value::Bytes(ref b) => BASE64_STANDARD.encode(b.as_slice()).to_string().into(), + Value::Null => serde_json::Value::Null, + #[cfg(feature = "chrono")] + Value::Timestamp(ref dt) => dt.to_rfc3339().into(), + #[cfg(feature = "chrono")] + Value::Duration(ref v) => serde_json::Value::Number(serde_json::Number::from( + v.num_nanoseconds() + .ok_or(ConvertToJsonError::DurationOverflow(v))?, + )), + _ => return Err(ConvertToJsonError::Value(self)), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::objects::Map; + use crate::Value as CelValue; + #[cfg(feature = "chrono")] + use chrono::Duration; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn test_cel_value_to_json() { + let mut tests = vec![ + (json!("hello"), CelValue::String("hello".to_string().into())), + (json!(42), CelValue::Int(42)), + (json!(42.0), CelValue::Float(42.0)), + (json!(true), CelValue::Bool(true)), + (json!(null), CelValue::Null), + ( + json!([true, null]), + CelValue::List(vec![CelValue::Bool(true), CelValue::Null].into()), + ), + ( + json!({"hello": "world"}), + CelValue::Map(Map::from(HashMap::from([( + "hello".to_string(), + CelValue::String("world".to_string().into()), + )]))), + ), + ]; + + #[cfg(feature = "chrono")] + if true { + tests.push(( + json!(1_000_000_000), + CelValue::Duration(Duration::seconds(1)), + )); + } + + for (expected, value) in tests.iter() { + assert_eq!( + value.json().unwrap(), + *expected, + "{:?}={:?}", + value, + expected + ); + } + } +} diff --git a/libs/core/cel-rust/interpreter/src/lib.rs b/libs/core/cel-rust/interpreter/src/lib.rs new file mode 100644 index 0000000..21dd740 --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/lib.rs @@ -0,0 +1,269 @@ +extern crate core; + +use cel_parser::{parse, ExpressionReferences, Member}; +use std::convert::TryFrom; +use std::sync::Arc; +use thiserror::Error; + +mod macros; + +pub mod context; +pub use cel_parser::error::ParseError; +pub use cel_parser::Expression; +pub use context::Context; +pub use functions::FunctionContext; +pub use objects::{ResolveResult, Value}; +pub mod functions; +mod magic; +pub mod objects; +mod resolvers; + +#[cfg(feature = "chrono")] +mod duration; +#[cfg(feature = "chrono")] +pub use ser::{Duration, Timestamp}; + +mod ser; +pub use ser::to_value; +pub use ser::SerializationError; + +#[cfg(feature = "json")] +mod json; +#[cfg(feature = "json")] +pub use json::ConvertToJsonError; + +use magic::FromContext; + +pub mod extractors { + pub use crate::magic::{Arguments, Identifier, This}; +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum ExecutionError { + #[error("Invalid argument count: expected {expected}, got {actual}")] + InvalidArgumentCount { expected: usize, actual: usize }, + #[error("Invalid argument type: {:?}", .target)] + UnsupportedTargetType { target: Value }, + #[error("Method '{method}' not supported on type '{target:?}'")] + NotSupportedAsMethod { method: String, target: Value }, + /// Indicates that the script attempted to use a value as a key in a map, + /// but the type of the value was not supported as a key. + #[error("Unable to use value '{0:?}' as a key")] + UnsupportedKeyType(Value), + #[error("Unexpected type: got '{got}', want '{want}'")] + UnexpectedType { got: String, want: String }, + /// Indicates that the script attempted to reference a key on a type that + /// was missing the requested key. + #[error("No such key: {0}")] + NoSuchKey(Arc), + /// Indicates that the script attempted to reference an undeclared variable + /// method, or function. + #[error("Undeclared reference to '{0}'")] + UndeclaredReference(Arc), + /// Indicates that a function expected to be called as a method, or to be + /// called with at least one parameter. + #[error("Missing argument or target")] + MissingArgumentOrTarget, + /// Indicates that a comparison could not be performed. + #[error("{0:?} can not be compared to {1:?}")] + ValuesNotComparable(Value, Value), + /// Indicates that an operator was used on a type that does not support it. + #[error("Unsupported unary operator '{0}': {1:?}")] + UnsupportedUnaryOperator(&'static str, Value), + /// Indicates that an unsupported binary operator was applied on two values + /// where it's unsupported, for example list + map. + #[error("Unsupported binary operator '{0}': {1:?}, {2:?}")] + UnsupportedBinaryOperator(&'static str, Value, Value), + /// Indicates that an unsupported type was used to index a map + #[error("Cannot use value as map index: {0:?}")] + UnsupportedMapIndex(Value), + /// Indicates that an unsupported type was used to index a list + #[error("Cannot use value as list index: {0:?}")] + UnsupportedListIndex(Value), + /// Indicates that an unsupported type was used to index a list + #[error("Cannot use value {0:?} to index {1:?}")] + UnsupportedIndex(Value, Value), + /// Indicates that a function call occurred without an [`Expression::Ident`] + /// as the function identifier. + #[error("Unsupported function call identifier type: {0:?}")] + UnsupportedFunctionCallIdentifierType(Expression), + /// Indicates that a [`Member::Fields`] construction was attempted + /// which is not yet supported. + #[error("Unsupported fields construction: {0:?}")] + UnsupportedFieldsConstruction(Member), + /// Indicates that a function had an error during execution. + #[error("Error executing function '{function}': {message}")] + FunctionError { function: String, message: String }, +} + +impl ExecutionError { + pub fn no_such_key(name: &str) -> Self { + ExecutionError::NoSuchKey(Arc::new(name.to_string())) + } + + pub fn undeclared_reference(name: &str) -> Self { + ExecutionError::UndeclaredReference(Arc::new(name.to_string())) + } + + pub fn invalid_argument_count(expected: usize, actual: usize) -> Self { + ExecutionError::InvalidArgumentCount { expected, actual } + } + + pub fn function_error(function: &str, error: E) -> Self { + ExecutionError::FunctionError { + function: function.to_string(), + message: error.to_string(), + } + } + + pub fn unsupported_target_type(target: Value) -> Self { + ExecutionError::UnsupportedTargetType { target } + } + + pub fn not_supported_as_method(method: &str, target: Value) -> Self { + ExecutionError::NotSupportedAsMethod { + method: method.to_string(), + target, + } + } + + pub fn unsupported_key_type(value: Value) -> Self { + ExecutionError::UnsupportedKeyType(value) + } + + pub fn missing_argument_or_target() -> Self { + ExecutionError::MissingArgumentOrTarget + } +} + +#[derive(Debug)] +pub struct Program { + expression: Expression, +} + +impl Program { + pub fn compile(source: &str) -> Result { + parse(source).map(|expression| Program { expression }) + } + + pub fn execute(&self, context: &Context) -> ResolveResult { + Value::resolve(&self.expression, context) + } + + /// Returns the variables and functions referenced by the CEL program + /// + /// # Example + /// ```rust + /// # use cel_interpreter::Program; + /// let program = Program::compile("size(foo) > 0").unwrap(); + /// let references = program.references(); + /// + /// assert!(references.has_function("size")); + /// assert!(references.has_variable("foo")); + /// ``` + pub fn references(&self) -> ExpressionReferences { + self.expression.references() + } +} + +impl TryFrom<&str> for Program { + type Error = ParseError; + + fn try_from(value: &str) -> Result { + Program::compile(value) + } +} + +#[cfg(test)] +mod tests { + use crate::context::Context; + use crate::objects::{ResolveResult, Value}; + use crate::{ExecutionError, Program}; + use std::collections::HashMap; + use std::convert::TryInto; + + /// Tests the provided script and returns the result. An optional context can be provided. + pub(crate) fn test_script(script: &str, ctx: Option) -> ResolveResult { + let program = Program::compile(script).unwrap(); + program.execute(&ctx.unwrap_or_default()) + } + + #[test] + fn parse() { + Program::compile("1 + 1").unwrap(); + } + + #[test] + fn from_str() { + let input = "1.1"; + let _p: Program = input.try_into().unwrap(); + } + + #[test] + fn variables() { + fn assert_output(script: &str, expected: ResolveResult) { + let mut ctx = Context::default(); + ctx.add_variable_from_value("foo", HashMap::from([("bar", 1i64)])); + ctx.add_variable_from_value("arr", vec![1i64, 2, 3]); + ctx.add_variable_from_value("str", "foobar".to_string()); + assert_eq!(test_script(script, Some(ctx)), expected); + } + + // Test methods + assert_output("size([1, 2, 3]) == 3", Ok(true.into())); + assert_output("size([]) == 3", Ok(false.into())); + + // Test variable attribute traversals + assert_output("foo.bar == 1", Ok(true.into())); + + // Test that we can index into an array + assert_output("arr[0] == 1", Ok(true.into())); + + // Test that we can index into a string + assert_output("str[0] == 'f'", Ok(true.into())); + + // Test that we can merge two maps + assert_output( + "{'a': 1} + {'a': 2, 'b': 3}", + Ok(HashMap::from([("a", 2), ("b", 3)]).into()), + ); + } + + #[test] + fn test_execution_errors() { + let tests = vec![ + ( + "no such key", + "foo.baz.bar == 1", + ExecutionError::no_such_key("baz"), + ), + ( + "undeclared reference", + "missing == 1", + ExecutionError::undeclared_reference("missing"), + ), + ( + "undeclared method", + "1.missing()", + ExecutionError::undeclared_reference("missing"), + ), + ( + "undeclared function", + "missing(1)", + ExecutionError::undeclared_reference("missing"), + ), + ( + "unsupported key type", + "{null: true}", + ExecutionError::unsupported_key_type(Value::Null), + ), + ]; + + for (name, script, error) in tests { + let mut ctx = Context::default(); + ctx.add_variable_from_value("foo", HashMap::from([("bar", 1)])); + let res = test_script(script, Some(ctx)); + assert_eq!(res, error.into(), "{}", name); + } + } +} diff --git a/libs/core/cel-rust/interpreter/src/macros.rs b/libs/core/cel-rust/interpreter/src/macros.rs new file mode 100644 index 0000000..08776ee --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/macros.rs @@ -0,0 +1,98 @@ +#[macro_export] +macro_rules! impl_conversions { + // Capture pairs separated by commas, where each pair is separated by => + ($($target_type:ty => $value_variant:path),* $(,)?) => { + $( + impl FromValue for $target_type { + fn from_value(expr: &Value) -> Result { + if let $value_variant(v) = expr { + Ok(v.clone()) + } else { + Err(ExecutionError::UnexpectedType { + got: format!("{:?}", expr), + want: stringify!($target_type).to_string(), + }) + } + } + } + + impl FromValue for Option<$target_type> { + fn from_value(expr: &Value) -> Result { + match expr { + Value::Null => Ok(None), + $value_variant(v) => Ok(Some(v.clone())), + _ => Err(ExecutionError::UnexpectedType { + got: format!("{:?}", expr), + want: stringify!($target_type).to_string(), + }), + } + } + } + + impl From<$target_type> for Value { + fn from(value: $target_type) -> Self { + $value_variant(value) + } + } + + impl $crate::magic::IntoResolveResult for $target_type { + fn into_resolve_result(self) -> ResolveResult { + Ok($value_variant(self)) + } + } + + impl $crate::magic::IntoResolveResult for Result<$target_type, ExecutionError> { + fn into_resolve_result(self) -> ResolveResult { + self.map($value_variant) + } + } + + impl<'a, 'context> FromContext<'a, 'context> for $target_type { + fn from_context(ctx: &'a mut FunctionContext<'context>) -> Result + where + Self: Sized, + { + arg_value_from_context(ctx).and_then(|v| FromValue::from_value(&v)) + } + } + )* + } +} + +#[macro_export] +macro_rules! impl_handler { + ($($t:ty),*) => { + paste::paste! { + impl Handler<($($t,)*)> for F + where + F: Fn($($t,)*) -> R + Clone, + $($t: for<'a, 'context> $crate::FromContext<'a, 'context>,)* + R: IntoResolveResult, + { + fn call(self, _ftx: &mut FunctionContext) -> ResolveResult { + $( + let [] = $t::from_context(_ftx)?; + )* + self($([],)*).into_resolve_result() + } + } + + impl Handler<(WithFunctionContext, $($t,)*)> for F + where + F: Fn(&FunctionContext, $($t,)*) -> R + Clone, + $($t: for<'a, 'context> $crate::FromContext<'a, 'context>,)* + R: IntoResolveResult, + { + fn call(self, _ftx: &mut FunctionContext) -> ResolveResult { + $( + let [] = $t::from_context(_ftx)?; + )* + self(_ftx, $([],)*).into_resolve_result() + } + } + } + }; +} + +pub(crate) use impl_conversions; +pub(crate) use impl_handler; diff --git a/libs/core/cel-rust/interpreter/src/magic.rs b/libs/core/cel-rust/interpreter/src/magic.rs new file mode 100644 index 0000000..4bdfaff --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/magic.rs @@ -0,0 +1,403 @@ +use crate::macros::{impl_conversions, impl_handler}; +use crate::resolvers::{AllArguments, Argument}; +use crate::{ExecutionError, FunctionContext, ResolveResult, Value}; +use cel_parser::Expression; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; + +impl_conversions!( + i64 => Value::Int, + u64 => Value::UInt, + f64 => Value::Float, + Arc => Value::String, + Arc> => Value::Bytes, + bool => Value::Bool, + Arc> => Value::List +); + +#[cfg(feature = "chrono")] +impl_conversions!( + chrono::Duration => Value::Duration, + chrono::DateTime => Value::Timestamp, +); + +impl From for Value { + fn from(value: i32) -> Self { + Value::Int(value as i64) + } +} + +/// Describes any type that can be converted from a [`Value`] into itself. +/// This is commonly used to convert from [`Value`] into primitive types, +/// e.g. from `Value::Bool(true) -> true`. This trait is auto-implemented +/// for many CEL-primitive types. +trait FromValue { + fn from_value(value: &Value) -> Result + where + Self: Sized; +} + +impl FromValue for Value { + fn from_value(value: &Value) -> Result + where + Self: Sized, + { + Ok(value.clone()) + } +} + +/// A trait for types that can be converted into a [`ResolveResult`]. Every function that can +/// be registered to the CEL context must return a value that implements this trait. +pub trait IntoResolveResult { + fn into_resolve_result(self) -> ResolveResult; +} + +impl IntoResolveResult for String { + fn into_resolve_result(self) -> ResolveResult { + Ok(Value::String(Arc::new(self))) + } +} + +impl IntoResolveResult for Result { + fn into_resolve_result(self) -> ResolveResult { + self + } +} + +/// Describes any type that can be converted from a [`FunctionContext`] into +/// itself, for example CEL primitives implement this trait to allow them to +/// be used as arguments to functions. This trait is core to the 'magic function +/// parameter' system. Every argument to a function that can be registered to +/// the CEL context must implement this type. +pub(crate) trait FromContext<'a, 'context> { + fn from_context(ctx: &'a mut FunctionContext<'context>) -> Result + where + Self: Sized; +} + +/// A function argument abstraction enabling dynamic method invocation on a +/// target instance or on the first argument if the function is not called +/// as a method. +/// +/// This is similar to how methods can be called as functions using the +/// [fully-qualified syntax](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name). +/// +/// # Using `This` +/// ``` +/// # use std::sync::Arc; +/// # use cel_interpreter::{Program, Context}; +/// use cel_interpreter::extractors::This; +/// # let mut context = Context::default(); +/// # context.add_function("startsWith", starts_with); +/// +/// /// Notice how `This` refers to the target value when called as a method, +/// /// but the first argument when called as a function. +/// let program1 = "'foobar'.startsWith('foo') == true"; +/// let program2 = "startsWith('foobar', 'foo') == true"; +/// # let program1 = Program::compile(program1).unwrap(); +/// # let program2 = Program::compile(program2).unwrap(); +/// # let value = program1.execute(&context).unwrap(); +/// # assert_eq!(value, true.into()); +/// # let value = program2.execute(&context).unwrap(); +/// # assert_eq!(value, true.into()); +/// +/// fn starts_with(This(this): This>, prefix: Arc) -> bool { +/// this.starts_with(prefix.as_str()) +/// } +/// ``` +/// +/// # Type of `This` +/// This also accepts a type `T` which determines the specific type +/// that's extracted. Any type that supports [`FromValue`] can be used. +/// In the previous example, the method `startsWith` is only ever called +/// on a string, so we can use `This>` to extract the string +/// automatically prior to our method actually being called. +/// +/// In some cases, you may want access to the raw [`Value`] instead, for +/// example, the `contains` method works for several different types. In these +/// cases, you can use `This` to extract the raw value. +/// +/// ```skip +/// pub fn contains(This(this): This, arg: Value) -> Result { +/// Ok(match this { +/// Value::List(v) => v.contains(&arg), +/// ... +/// } +/// } +/// ``` +pub struct This(pub T); + +impl<'a, 'context, T> FromContext<'a, 'context> for This +where + T: FromValue, +{ + fn from_context(ctx: &'a mut FunctionContext<'context>) -> Result + where + Self: Sized, + { + if let Some(ref this) = ctx.this { + Ok(This(T::from_value(this)?)) + } else { + let arg = arg_value_from_context(ctx) + .map_err(|_| ExecutionError::missing_argument_or_target())?; + Ok(This(T::from_value(&arg)?)) + } + } +} + +/// Identifier is an argument extractor that attempts to extract an identifier +/// from an argument's expression. +/// +/// It fails if the argument is not available, or if the argument cannot be +/// converted into an expression. +/// +/// # Examples +/// Identifiers are useful for functions like `.map` or `.filter` where one +/// of the arguments is the declaration of a variable. In this case, as noted +/// below, the x is an identifier, and we want to be able to parse it +/// automatically. +/// +/// ```javascript +/// // Identifier +/// // โ†“ +/// [1, 2, 3].map(x, x * 2) == [2, 4, 6] +/// ``` +/// +/// The function signature for the Rust implementation of `map` looks like this +/// +/// ```skip +/// pub fn map( +/// ftx: &FunctionContext, +/// This(this): This, // <- [1, 2, 3] +/// ident: Identifier, // <- x +/// expr: Expression, // <- x * 2 +/// ) -> Result; +/// ``` +#[derive(Clone)] +pub struct Identifier(pub Arc); + +impl<'a, 'context> FromContext<'a, 'context> for Identifier { + fn from_context(ctx: &'a mut FunctionContext<'context>) -> Result + where + Self: Sized, + { + match arg_expr_from_context(ctx) { + Expression::Ident(ident) => Ok(Identifier(ident.clone())), + expr => Err(ExecutionError::UnexpectedType { + got: format!("{:?}", expr), + want: "identifier".to_string(), + }), + } + } +} + +impl From<&Identifier> for String { + fn from(value: &Identifier) -> Self { + value.0.to_string() + } +} + +impl From for String { + fn from(value: Identifier) -> Self { + value.0.as_ref().clone() + } +} + +/// An argument extractor that extracts all the arguments passed to a function, resolves their +/// expressions and returns a vector of [`Value`]. +/// +/// This is useful for functions that accept a variable number of arguments rather than known +/// arguments and types (for example a `sum` function). +/// +/// # Example +/// ```javascript +/// sum(1, 2.0, uint(3)) == 5.0 +/// ``` +/// +/// ```rust +/// # use cel_interpreter::{Value}; +/// use cel_interpreter::extractors::Arguments; +/// pub fn sum(Arguments(args): Arguments) -> Value { +/// args.iter().fold(0.0, |acc, val| match val { +/// Value::Int(x) => *x as f64 + acc, +/// Value::UInt(x) => *x as f64 + acc, +/// Value::Float(x) => *x + acc, +/// _ => acc, +/// }).into() +/// } +/// ``` +#[derive(Clone)] +pub struct Arguments(pub Arc>); + +impl<'a> FromContext<'a, '_> for Arguments { + fn from_context(ctx: &'a mut FunctionContext) -> Result + where + Self: Sized, + { + match ctx.resolve(AllArguments)? { + Value::List(list) => Ok(Arguments(list.clone())), + _ => todo!(), + } + } +} + +impl<'a, 'context> FromContext<'a, 'context> for Value { + fn from_context(ctx: &'a mut FunctionContext<'context>) -> Result + where + Self: Sized, + { + arg_value_from_context(ctx) + } +} + +impl<'a, 'context> FromContext<'a, 'context> for Expression { + fn from_context(ctx: &'a mut FunctionContext<'context>) -> Result + where + Self: Sized, + { + Ok(arg_expr_from_context(ctx)) + } +} + +/// Returns the next argument specified by the context's `arg_idx` field as an expression +/// (i.e. not resolved). Calling this multiple times will increment the `arg_idx` which will +/// return subsequent arguments every time. +/// +/// Calling this function when there are no more arguments will result in a panic. Since this +/// function is only ever called within the context of a controlled macro that calls it once +/// for each argument, this should never happen. +fn arg_expr_from_context(ctx: &mut FunctionContext) -> Expression { + let idx = ctx.arg_idx; + ctx.arg_idx += 1; + ctx.args[idx].clone() +} + +/// Returns the next argument specified by the context's `arg_idx` field as after resolving +/// it. Calling this multiple times will increment the `arg_idx` which will return subsequent +/// arguments every time. +/// +/// Calling this function when there are no more arguments will result in a panic. Since this +/// function is only ever called within the context of a controlled macro that calls it once +/// for each argument, this should never happen. +fn arg_value_from_context(ctx: &mut FunctionContext) -> Result { + let idx = ctx.arg_idx; + ctx.arg_idx += 1; + ctx.resolve(Argument(idx)) +} + +pub struct WithFunctionContext; + +impl_handler!(); +impl_handler!(C1); +impl_handler!(C1, C2); +impl_handler!(C1, C2, C3); +impl_handler!(C1, C2, C3, C4); +impl_handler!(C1, C2, C3, C4, C5); +impl_handler!(C1, C2, C3, C4, C5, C6); +impl_handler!(C1, C2, C3, C4, C5, C6, C7); +impl_handler!(C1, C2, C3, C4, C5, C6, C7, C8); +impl_handler!(C1, C2, C3, C4, C5, C6, C7, C8, C9); + +// Heavily inspired by https://users.rust-lang.org/t/common-data-type-for-functions-with-different-parameters-e-g-axum-route-handlers/90207/6 +// and https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c6744c27c2358ec1d1196033a0ec11e4 + +#[derive(Default)] +pub struct FunctionRegistry { + functions: HashMap>, +} + +impl FunctionRegistry { + pub(crate) fn add(&mut self, name: &str, handler: H) + where + H: Handler + 'static + Send + Sync, + T: 'static, + { + self.functions.insert( + name.to_string(), + Box::new(HandlerFunction { + handler, + into_callable: |h, ctx| Box::new(HandlerCallable::new(h, ctx)), + }), + ); + } + + pub(crate) fn get(&self, name: &str) -> Option> { + self.functions.get(name).map(|f| f.clone_box()) + } + + pub(crate) fn has(&self, name: &str) -> bool { + self.functions.contains_key(name) + } +} + +pub trait Function: Send + Sync { + fn clone_box(&self) -> Box; + fn into_callable<'a>(self: Box, ctx: &'a mut FunctionContext) -> Box; + fn call_with_context(self: Box, ctx: &mut FunctionContext) -> ResolveResult; +} + +pub struct HandlerFunction { + pub handler: H, + pub into_callable: for<'a> fn(H, &'a mut FunctionContext) -> Box, +} + +impl Clone for HandlerFunction { + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + into_callable: self.into_callable, + } + } +} + +impl Function for HandlerFunction +where + H: Clone + Send + Sync + 'static, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn into_callable<'a>(self: Box, ctx: &'a mut FunctionContext) -> Box { + (self.into_callable)(self.handler, ctx) + } + + fn call_with_context(self: Box, ctx: &mut FunctionContext) -> ResolveResult { + self.into_callable(ctx).call() + } +} + +// Callable and HandlerCallable +pub trait Callable { + fn call(&mut self) -> ResolveResult; +} + +pub struct HandlerCallable<'a, 'context, H, T> { + handler: H, + context: &'a mut FunctionContext<'context>, + _marker: PhantomData T>, +} + +impl<'a, 'context, H, T> HandlerCallable<'a, 'context, H, T> { + pub fn new(handler: H, ctx: &'a mut FunctionContext<'context>) -> Self { + Self { + handler, + context: ctx, + _marker: PhantomData, + } + } +} + +impl Callable for HandlerCallable<'_, '_, H, T> +where + H: Handler + Clone + 'static, +{ + fn call(&mut self) -> ResolveResult { + self.handler.clone().call(self.context) + } +} + +pub trait Handler: Clone { + fn call(self, ctx: &mut FunctionContext) -> ResolveResult; +} diff --git a/libs/core/cel-rust/interpreter/src/objects.rs b/libs/core/cel-rust/interpreter/src/objects.rs new file mode 100644 index 0000000..f4f2589 --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/objects.rs @@ -0,0 +1,994 @@ +use crate::context::Context; +use crate::functions::FunctionContext; +use crate::ExecutionError; +use cel_parser::ast::*; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::convert::{Infallible, TryFrom, TryInto}; +use std::fmt::{Display, Formatter}; +use std::ops; +use std::sync::Arc; + +#[derive(Debug, PartialEq, Clone)] +pub struct Map { + pub map: Arc>, +} + +impl PartialOrd for Map { + fn partial_cmp(&self, _: &Self) -> Option { + None + } +} + +impl Map { + /// Returns a reference to the value corresponding to the key. Implicitly converts between int + /// and uint keys. + pub fn get(&self, key: &Key) -> Option<&Value> { + self.map.get(key).or_else(|| { + // Also check keys that are cross type comparable. + let converted = match key { + Key::Int(k) => Key::Uint(u64::try_from(*k).ok()?), + Key::Uint(k) => Key::Int(i64::try_from(*k).ok()?), + _ => return None, + }; + self.map.get(&converted) + }) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Ord, Clone, PartialOrd)] +pub enum Key { + Int(i64), + Uint(u64), + Bool(bool), + String(Arc), +} + +/// Implement conversions from primitive types to [`Key`] +impl From for Key { + fn from(v: String) -> Self { + Key::String(v.into()) + } +} + +impl From> for Key { + fn from(v: Arc) -> Self { + Key::String(v.clone()) + } +} + +impl<'a> From<&'a str> for Key { + fn from(v: &'a str) -> Self { + Key::String(Arc::new(v.into())) + } +} + +impl From for Key { + fn from(v: bool) -> Self { + Key::Bool(v) + } +} + +impl From for Key { + fn from(v: i64) -> Self { + Key::Int(v) + } +} + +impl From for Key { + fn from(v: u64) -> Self { + Key::Uint(v) + } +} + +impl serde::Serialize for Key { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Key::Int(v) => v.serialize(serializer), + Key::Uint(v) => v.serialize(serializer), + Key::Bool(v) => v.serialize(serializer), + Key::String(v) => v.serialize(serializer), + } + } +} + +impl Display for Key { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Key::Int(v) => write!(f, "{}", v), + Key::Uint(v) => write!(f, "{}", v), + Key::Bool(v) => write!(f, "{}", v), + Key::String(v) => write!(f, "{}", v), + } + } +} + +/// Implement conversions from [`Key`] into [`Value`] +impl TryInto for Value { + type Error = Value; + + #[inline(always)] + fn try_into(self) -> Result { + match self { + Value::Int(v) => Ok(Key::Int(v)), + Value::UInt(v) => Ok(Key::Uint(v)), + Value::String(v) => Ok(Key::String(v)), + Value::Bool(v) => Ok(Key::Bool(v)), + _ => Err(self), + } + } +} + +// Implement conversion from HashMap into CelMap +impl, V: Into> From> for Map { + fn from(map: HashMap) -> Self { + let mut new_map = HashMap::new(); + for (k, v) in map { + new_map.insert(k.into(), v.into()); + } + Map { + map: Arc::new(new_map), + } + } +} + +pub trait TryIntoValue { + type Error: std::error::Error + 'static + Send + Sync; + fn try_into_value(self) -> Result; +} + +impl TryIntoValue for T { + type Error = crate::ser::SerializationError; + fn try_into_value(self) -> Result { + crate::ser::to_value(self) + } +} +impl TryIntoValue for Value { + type Error = Infallible; + fn try_into_value(self) -> Result { + Ok(self) + } +} + +#[derive(Debug, Clone)] +pub enum Value { + List(Arc>), + Map(Map), + + Function(Arc, Option>), + + // Atoms + Int(i64), + UInt(u64), + Float(f64), + String(Arc), + Bytes(Arc>), + Bool(bool), + #[cfg(feature = "chrono")] + Duration(chrono::Duration), + #[cfg(feature = "chrono")] + Timestamp(chrono::DateTime), + Null, +} + +#[derive(Clone, Copy, Debug)] +pub enum ValueType { + List, + Map, + Function, + Int, + UInt, + Float, + String, + Bytes, + Bool, + Duration, + Timestamp, + Null, +} + +impl Display for ValueType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ValueType::List => write!(f, "list"), + ValueType::Map => write!(f, "map"), + ValueType::Function => write!(f, "function"), + ValueType::Int => write!(f, "int"), + ValueType::UInt => write!(f, "uint"), + ValueType::Float => write!(f, "float"), + ValueType::String => write!(f, "string"), + ValueType::Bytes => write!(f, "bytes"), + ValueType::Bool => write!(f, "bool"), + ValueType::Duration => write!(f, "duration"), + ValueType::Timestamp => write!(f, "timestamp"), + ValueType::Null => write!(f, "null"), + } + } +} + +impl Value { + pub fn type_of(&self) -> ValueType { + match self { + Value::List(_) => ValueType::List, + Value::Map(_) => ValueType::Map, + Value::Function(_, _) => ValueType::Function, + Value::Int(_) => ValueType::Int, + Value::UInt(_) => ValueType::UInt, + Value::Float(_) => ValueType::Float, + Value::String(_) => ValueType::String, + Value::Bytes(_) => ValueType::Bytes, + Value::Bool(_) => ValueType::Bool, + #[cfg(feature = "chrono")] + Value::Duration(_) => ValueType::Duration, + #[cfg(feature = "chrono")] + Value::Timestamp(_) => ValueType::Timestamp, + Value::Null => ValueType::Null, + } + } + + pub fn error_expected_type(&self, expected: ValueType) -> ExecutionError { + ExecutionError::UnexpectedType { + got: self.type_of().to_string(), + want: expected.to_string(), + } + } +} + +impl From<&Value> for Value { + fn from(value: &Value) -> Self { + value.clone() + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Value::Map(a), Value::Map(b)) => a == b, + (Value::List(a), Value::List(b)) => a == b, + (Value::Function(a1, a2), Value::Function(b1, b2)) => a1 == b1 && a2 == b2, + (Value::Int(a), Value::Int(b)) => a == b, + (Value::UInt(a), Value::UInt(b)) => a == b, + (Value::Float(a), Value::Float(b)) => a == b, + (Value::String(a), Value::String(b)) => a == b, + (Value::Bytes(a), Value::Bytes(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Null, Value::Null) => true, + #[cfg(feature = "chrono")] + (Value::Duration(a), Value::Duration(b)) => a == b, + #[cfg(feature = "chrono")] + (Value::Timestamp(a), Value::Timestamp(b)) => a == b, + // Allow different numeric types to be compared without explicit casting. + (Value::Int(a), Value::UInt(b)) => a + .to_owned() + .try_into() + .map(|a: u64| a == *b) + .unwrap_or(false), + (Value::Int(a), Value::Float(b)) => (*a as f64) == *b, + (Value::UInt(a), Value::Int(b)) => a + .to_owned() + .try_into() + .map(|a: i64| a == *b) + .unwrap_or(false), + (Value::UInt(a), Value::Float(b)) => (*a as f64) == *b, + (Value::Float(a), Value::Int(b)) => *a == (*b as f64), + (Value::Float(a), Value::UInt(b)) => *a == (*b as f64), + (_, _) => false, + } + } +} + +impl Eq for Value {} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Value::Int(a), Value::Int(b)) => Some(a.cmp(b)), + (Value::UInt(a), Value::UInt(b)) => Some(a.cmp(b)), + (Value::Float(a), Value::Float(b)) => a.partial_cmp(b), + (Value::String(a), Value::String(b)) => Some(a.cmp(b)), + (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)), + (Value::Null, Value::Null) => Some(Ordering::Equal), + #[cfg(feature = "chrono")] + (Value::Duration(a), Value::Duration(b)) => Some(a.cmp(b)), + #[cfg(feature = "chrono")] + (Value::Timestamp(a), Value::Timestamp(b)) => Some(a.cmp(b)), + // Allow different numeric types to be compared without explicit casting. + (Value::Int(a), Value::UInt(b)) => Some( + a.to_owned() + .try_into() + .map(|a: u64| a.cmp(b)) + // If the i64 doesn't fit into a u64 it must be less than 0. + .unwrap_or(Ordering::Less), + ), + (Value::Int(a), Value::Float(b)) => (*a as f64).partial_cmp(b), + (Value::UInt(a), Value::Int(b)) => Some( + a.to_owned() + .try_into() + .map(|a: i64| a.cmp(b)) + // If the u64 doesn't fit into a i64 it must be greater than i64::MAX. + .unwrap_or(Ordering::Greater), + ), + (Value::UInt(a), Value::Float(b)) => (*a as f64).partial_cmp(b), + (Value::Float(a), Value::Int(b)) => a.partial_cmp(&(*b as f64)), + (Value::Float(a), Value::UInt(b)) => a.partial_cmp(&(*b as f64)), + _ => None, + } + } +} + +impl From<&Key> for Value { + fn from(value: &Key) -> Self { + match value { + Key::Int(v) => Value::Int(*v), + Key::Uint(v) => Value::UInt(*v), + Key::Bool(v) => Value::Bool(*v), + Key::String(v) => Value::String(v.clone()), + } + } +} + +impl From for Value { + fn from(value: Key) -> Self { + match value { + Key::Int(v) => Value::Int(v), + Key::Uint(v) => Value::UInt(v), + Key::Bool(v) => Value::Bool(v), + Key::String(v) => Value::String(v), + } + } +} + +impl From<&Key> for Key { + fn from(key: &Key) -> Self { + key.clone() + } +} + +// Convert Vec to Value +impl> From> for Value { + fn from(v: Vec) -> Self { + Value::List(v.into_iter().map(|v| v.into()).collect::>().into()) + } +} + +// Convert Vec to Value +impl From> for Value { + fn from(v: Vec) -> Self { + Value::Bytes(v.into()) + } +} + +// Convert String to Value +impl From for Value { + fn from(v: String) -> Self { + Value::String(v.into()) + } +} + +impl From<&str> for Value { + fn from(v: &str) -> Self { + Value::String(v.to_string().into()) + } +} + +// Convert Option to Value +impl> From> for Value { + fn from(v: Option) -> Self { + match v { + Some(v) => v.into(), + None => Value::Null, + } + } +} + +// Convert HashMap to Value +impl, V: Into> From> for Value { + fn from(v: HashMap) -> Self { + Value::Map(v.into()) + } +} + +impl From for ResolveResult { + fn from(value: ExecutionError) -> Self { + Err(value) + } +} + +pub type ResolveResult = Result; + +impl From for ResolveResult { + fn from(value: Value) -> Self { + Ok(value) + } +} + +impl Value { + pub fn resolve_all(expr: &[Expression], ctx: &Context) -> ResolveResult { + let mut res = Vec::with_capacity(expr.len()); + for expr in expr { + res.push(Value::resolve(expr, ctx)?); + } + Ok(Value::List(res.into())) + } + + #[inline(always)] + pub fn resolve(expr: &Expression, ctx: &Context) -> ResolveResult { + match expr { + Expression::Atom(atom) => Ok(atom.into()), + Expression::Arithmetic(left, op, right) => { + let left = Value::resolve(left, ctx)?; + let right = Value::resolve(right, ctx)?; + + match op { + ArithmeticOp::Add => left + right, + ArithmeticOp::Subtract => left - right, + ArithmeticOp::Divide => left / right, + ArithmeticOp::Multiply => left * right, + ArithmeticOp::Modulus => left % right, + } + } + Expression::Relation(left, op, right) => { + let left = Value::resolve(left, ctx)?; + let right = Value::resolve(right, ctx)?; + let res = match op { + RelationOp::LessThan => { + left.partial_cmp(&right) + .ok_or(ExecutionError::ValuesNotComparable(left, right))? + == Ordering::Less + } + RelationOp::LessThanEq => { + left.partial_cmp(&right) + .ok_or(ExecutionError::ValuesNotComparable(left, right))? + != Ordering::Greater + } + RelationOp::GreaterThan => { + left.partial_cmp(&right) + .ok_or(ExecutionError::ValuesNotComparable(left, right))? + == Ordering::Greater + } + RelationOp::GreaterThanEq => { + left.partial_cmp(&right) + .ok_or(ExecutionError::ValuesNotComparable(left, right))? + != Ordering::Less + } + RelationOp::Equals => right.eq(&left), + RelationOp::NotEquals => right.ne(&left), + RelationOp::In => match (left, right) { + (Value::String(l), Value::String(r)) => r.contains(&*l), + (any, Value::List(v)) => v.contains(&any), + (any, Value::Map(m)) => match any.try_into() { + Ok(key) => m.map.contains_key(&key), + Err(_) => false, + }, + (left, right) => Err(ExecutionError::ValuesNotComparable(left, right))?, + }, + }; + Value::Bool(res).into() + } + Expression::Ternary(cond, left, right) => { + let cond = Value::resolve(cond, ctx)?; + if cond.to_bool() { + Value::resolve(left, ctx) + } else { + Value::resolve(right, ctx) + } + } + Expression::Or(left, right) => { + let left = Value::resolve(left, ctx)?; + if left.to_bool() { + left.into() + } else { + Value::resolve(right, ctx) + } + } + Expression::And(left, right) => { + let left = Value::resolve(left, ctx)?; + if !left.to_bool() { + Value::Bool(false).into() + } else { + let right = Value::resolve(right, ctx)?; + Value::Bool(right.to_bool()).into() + } + } + Expression::Unary(op, expr) => { + let expr = Value::resolve(expr, ctx)?; + match op { + UnaryOp::Not => Ok(Value::Bool(!expr.to_bool())), + UnaryOp::DoubleNot => Ok(Value::Bool(expr.to_bool())), + UnaryOp::Minus => match expr { + Value::Int(i) => Ok(Value::Int(-i)), + Value::Float(i) => Ok(Value::Float(-i)), + value => Err(ExecutionError::UnsupportedUnaryOperator("minus", value)), + }, + UnaryOp::DoubleMinus => match expr { + Value::Int(_) => Ok(expr), + Value::UInt(_) => Ok(expr), + Value::Float(_) => Ok(expr), + value => Err(ExecutionError::UnsupportedUnaryOperator("negate", value)), + }, + } + } + Expression::Member(left, right) => { + let left = Value::resolve(left, ctx)?; + left.member(right, ctx) + } + Expression::List(items) => { + let list = items + .iter() + .map(|i| Value::resolve(i, ctx)) + .collect::, _>>()?; + Value::List(list.into()).into() + } + Expression::Map(items) => { + let mut map = HashMap::default(); + for (k, v) in items.iter() { + let key = Value::resolve(k, ctx)? + .try_into() + .map_err(ExecutionError::UnsupportedKeyType)?; + let value = Value::resolve(v, ctx)?; + map.insert(key, value); + } + Ok(Value::Map(Map { + map: Arc::from(map), + })) + } + Expression::Ident(name) => ctx.get_variable(&***name), + Expression::FunctionCall(name, target, args) => { + if let Expression::Ident(name) = &**name { + let func = ctx + .get_function(&**name) + .ok_or_else(|| ExecutionError::UndeclaredReference(Arc::clone(name)))?; + match target { + None => { + let mut ctx = + FunctionContext::new(name.clone(), None, ctx, args.clone()); + func.call_with_context(&mut ctx) + } + Some(target) => { + let mut ctx = FunctionContext::new( + name.clone(), + Some(Value::resolve(target, ctx)?), + ctx, + args.clone(), + ); + func.call_with_context(&mut ctx) + } + } + } else { + Err(ExecutionError::UnsupportedFunctionCallIdentifierType( + (**name).clone(), + )) + } + } + } + } + + // >> a(b) + // Member(Ident("a"), + // FunctionCall([Ident("b")])) + // >> a.b(c) + // Member(Member(Ident("a"), + // Attribute("b")), + // FunctionCall([Ident("c")])) + + fn member(self, member: &Member, ctx: &Context) -> ResolveResult { + match member { + Member::Index(idx) => { + let idx = Value::resolve(idx, ctx)?; + match (self, idx) { + (Value::List(items), Value::Int(idx)) => items + .get(idx as usize) + .cloned() + .unwrap_or(Value::Null) + .into(), + (Value::String(str), Value::Int(idx)) => { + match str.get(idx as usize..(idx + 1) as usize) { + None => Ok(Value::Null), + Some(str) => Ok(Value::String(str.to_string().into())), + } + } + (Value::Map(map), Value::String(property)) => map + .get(&property.into()) + .cloned() + .unwrap_or(Value::Null) + .into(), + (Value::Map(map), Value::Bool(property)) => map + .get(&property.into()) + .cloned() + .unwrap_or(Value::Null) + .into(), + (Value::Map(map), Value::Int(property)) => map + .get(&property.into()) + .cloned() + .unwrap_or(Value::Null) + .into(), + (Value::Map(map), Value::UInt(property)) => map + .get(&property.into()) + .cloned() + .unwrap_or(Value::Null) + .into(), + (Value::Map(_), index) => Err(ExecutionError::UnsupportedMapIndex(index)), + (Value::List(_), index) => Err(ExecutionError::UnsupportedListIndex(index)), + (value, index) => Err(ExecutionError::UnsupportedIndex(value, index)), + } + } + Member::Fields(_) => Err(ExecutionError::UnsupportedFieldsConstruction( + member.clone(), + )), + Member::Attribute(name) => { + // This will always either be because we're trying to access + // a property on self, or a method on self. + let child = match self { + Value::Map(ref m) => m.map.get(&name.clone().into()).cloned(), + _ => None, + }; + + // If the property is both an attribute and a method, then we + // give priority to the property. Maybe we can implement lookahead + // to see if the next token is a function call? + match (child, ctx.has_function(&***name)) { + (None, false) => ExecutionError::NoSuchKey(name.clone()).into(), + (Some(child), _) => child.into(), + (None, true) => Value::Function(name.clone(), Some(self.into())).into(), + } + } + } + } + + #[inline(always)] + fn to_bool(&self) -> bool { + match self { + Value::List(v) => !v.is_empty(), + Value::Map(v) => !v.map.is_empty(), + Value::Int(v) => *v != 0, + Value::UInt(v) => *v != 0, + Value::Float(v) => *v != 0.0, + Value::String(v) => !v.is_empty(), + Value::Bytes(v) => !v.is_empty(), + Value::Bool(v) => *v, + Value::Null => false, + #[cfg(feature = "chrono")] + Value::Duration(v) => v.num_nanoseconds().map(|n| n != 0).unwrap_or(false), + #[cfg(feature = "chrono")] + Value::Timestamp(v) => v.timestamp_nanos_opt().unwrap_or_default() > 0, + Value::Function(_, _) => false, + } + } +} + +impl From<&Atom> for Value { + #[inline(always)] + fn from(atom: &Atom) -> Self { + match atom { + Atom::Int(v) => Value::Int(*v), + Atom::UInt(v) => Value::UInt(*v), + Atom::Float(v) => Value::Float(*v), + Atom::String(v) => Value::String(v.clone()), + Atom::Bytes(v) => Value::Bytes(v.clone()), + Atom::Bool(v) => Value::Bool(*v), + Atom::Null => Value::Null, + } + } +} + +impl ops::Add for Value { + type Output = ResolveResult; + + #[inline(always)] + fn add(self, rhs: Value) -> Self::Output { + match (self, rhs) { + (Value::Int(l), Value::Int(r)) => Value::Int(l + r).into(), + (Value::UInt(l), Value::UInt(r)) => Value::UInt(l + r).into(), + + // Float matrix + (Value::Float(l), Value::Float(r)) => Value::Float(l + r).into(), + (Value::Int(l), Value::Float(r)) => Value::Float(l as f64 + r).into(), + (Value::Float(l), Value::Int(r)) => Value::Float(l + r as f64).into(), + (Value::UInt(l), Value::Float(r)) => Value::Float(l as f64 + r).into(), + (Value::Float(l), Value::UInt(r)) => Value::Float(l + r as f64).into(), + + (Value::List(l), Value::List(r)) => { + Value::List(l.iter().chain(r.iter()).cloned().collect::>().into()).into() + } + (Value::String(l), Value::String(r)) => { + let mut new = String::with_capacity(l.len() + r.len()); + new.push_str(&l); + new.push_str(&r); + Value::String(new.into()).into() + } + // Merge two maps should overwrite keys in the left map with the right map + (Value::Map(l), Value::Map(r)) => { + let mut new = HashMap::default(); + for (k, v) in l.map.iter() { + new.insert(k.clone(), v.clone()); + } + for (k, v) in r.map.iter() { + new.insert(k.clone(), v.clone()); + } + Value::Map(Map { map: Arc::new(new) }).into() + } + #[cfg(feature = "chrono")] + (Value::Duration(l), Value::Duration(r)) => Value::Duration(l + r).into(), + #[cfg(feature = "chrono")] + (Value::Timestamp(l), Value::Duration(r)) => Value::Timestamp(l + r).into(), + #[cfg(feature = "chrono")] + (Value::Duration(l), Value::Timestamp(r)) => Value::Timestamp(r + l).into(), + (left, right) => Err(ExecutionError::UnsupportedBinaryOperator( + "add", left, right, + )), + } + } +} + +impl ops::Sub for Value { + type Output = ResolveResult; + + #[inline(always)] + fn sub(self, rhs: Value) -> Self::Output { + match (self, rhs) { + (Value::Int(l), Value::Int(r)) => Value::Int(l - r).into(), + (Value::UInt(l), Value::UInt(r)) => Value::UInt(l - r).into(), + + // Float matrix + (Value::Float(l), Value::Float(r)) => Value::Float(l - r).into(), + (Value::Int(l), Value::Float(r)) => Value::Float(l as f64 - r).into(), + (Value::Float(l), Value::Int(r)) => Value::Float(l - r as f64).into(), + (Value::UInt(l), Value::Float(r)) => Value::Float(l as f64 - r).into(), + (Value::Float(l), Value::UInt(r)) => Value::Float(l - r as f64).into(), + // todo: implement checked sub for these over-flowable operations + #[cfg(feature = "chrono")] + (Value::Duration(l), Value::Duration(r)) => Value::Duration(l - r).into(), + #[cfg(feature = "chrono")] + (Value::Timestamp(l), Value::Duration(r)) => Value::Timestamp(l - r).into(), + #[cfg(feature = "chrono")] + (Value::Timestamp(l), Value::Timestamp(r)) => Value::Duration(l - r).into(), + (left, right) => Err(ExecutionError::UnsupportedBinaryOperator( + "sub", left, right, + )), + } + } +} + +impl ops::Div for Value { + type Output = ResolveResult; + + #[inline(always)] + fn div(self, rhs: Value) -> Self::Output { + match (self, rhs) { + (Value::Int(l), Value::Int(r)) => Value::Int(l / r).into(), + (Value::UInt(l), Value::UInt(r)) => Value::UInt(l / r).into(), + + // Float matrix + (Value::Float(l), Value::Float(r)) => Value::Float(l / r).into(), + (Value::Int(l), Value::Float(r)) => Value::Float(l as f64 / r).into(), + (Value::Float(l), Value::Int(r)) => Value::Float(l / r as f64).into(), + (Value::UInt(l), Value::Float(r)) => Value::Float(l as f64 / r).into(), + (Value::Float(l), Value::UInt(r)) => Value::Float(l / r as f64).into(), + + (left, right) => Err(ExecutionError::UnsupportedBinaryOperator( + "div", left, right, + )), + } + } +} + +impl ops::Mul for Value { + type Output = ResolveResult; + + #[inline(always)] + fn mul(self, rhs: Value) -> Self::Output { + match (self, rhs) { + (Value::Int(l), Value::Int(r)) => Value::Int(l * r).into(), + (Value::UInt(l), Value::UInt(r)) => Value::UInt(l * r).into(), + + // Float matrix + (Value::Float(l), Value::Float(r)) => Value::Float(l * r).into(), + (Value::Int(l), Value::Float(r)) => Value::Float(l as f64 * r).into(), + (Value::Float(l), Value::Int(r)) => Value::Float(l * r as f64).into(), + (Value::UInt(l), Value::Float(r)) => Value::Float(l as f64 * r).into(), + (Value::Float(l), Value::UInt(r)) => Value::Float(l * r as f64).into(), + + (left, right) => Err(ExecutionError::UnsupportedBinaryOperator( + "mul", left, right, + )), + } + } +} + +impl ops::Rem for Value { + type Output = ResolveResult; + + #[inline(always)] + fn rem(self, rhs: Value) -> Self::Output { + match (self, rhs) { + (Value::Int(l), Value::Int(r)) => Value::Int(l % r).into(), + (Value::UInt(l), Value::UInt(r)) => Value::UInt(l % r).into(), + + // Float matrix + (Value::Float(l), Value::Float(r)) => Value::Float(l % r).into(), + (Value::Int(l), Value::Float(r)) => Value::Float(l as f64 % r).into(), + (Value::Float(l), Value::Int(r)) => Value::Float(l % r as f64).into(), + (Value::UInt(l), Value::Float(r)) => Value::Float(l as f64 % r).into(), + (Value::Float(l), Value::UInt(r)) => Value::Float(l % r as f64).into(), + + (left, right) => Err(ExecutionError::UnsupportedBinaryOperator( + "rem", left, right, + )), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{objects::Key, Context, ExecutionError, Program, Value}; + use std::collections::HashMap; + use std::sync::Arc; + + #[test] + fn test_indexed_map_access() { + let mut context = Context::default(); + let mut headers = HashMap::new(); + headers.insert("Content-Type", "application/json".to_string()); + context.add_variable_from_value("headers", headers); + + let program = Program::compile("headers[\"Content-Type\"]").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, "application/json".into()); + } + + #[test] + fn test_numeric_map_access() { + let mut context = Context::default(); + let mut numbers = HashMap::new(); + numbers.insert(Key::Uint(1), "one".to_string()); + context.add_variable_from_value("numbers", numbers); + + let program = Program::compile("numbers[1]").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, "one".into()); + } + + #[test] + fn test_heterogeneous_compare() { + let context = Context::default(); + + let program = Program::compile("1 < uint(2)").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); + + let program = Program::compile("1 < 1.1").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); + + let program = Program::compile("uint(0) > -10").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!( + value, + true.into(), + "negative signed ints should be less than uints" + ); + } + + #[test] + fn test_float_compare() { + let context = Context::default(); + + let program = Program::compile("1.0 > 0.0").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); + + let program = Program::compile("double('NaN') == double('NaN')").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, false.into(), "NaN should not equal itself"); + + let program = Program::compile("1.0 > double('NaN')").unwrap(); + let result = program.execute(&context); + assert!( + result.is_err(), + "NaN should not be comparable with inequality operators" + ); + } + + #[test] + fn test_invalid_compare() { + let context = Context::default(); + + let program = Program::compile("{} == []").unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, false.into()); + } + + #[test] + fn test_size_fn_var() { + let program = Program::compile("size(requests) + size == 5").unwrap(); + let mut context = Context::default(); + let requests = vec![Value::Int(42), Value::Int(42)]; + context + .add_variable("requests", Value::List(Arc::new(requests))) + .unwrap(); + context.add_variable("size", Value::Int(3)).unwrap(); + assert_eq!(program.execute(&context).unwrap(), Value::Bool(true)); + } + + fn test_execution_error(program: &str, expected: ExecutionError) { + let program = Program::compile(program).unwrap(); + let result = program.execute(&Context::default()); + assert_eq!(result.unwrap_err(), expected); + } + + #[test] + fn test_invalid_sub() { + test_execution_error( + "'foo' - 10", + ExecutionError::UnsupportedBinaryOperator("sub", "foo".into(), Value::Int(10)), + ); + } + + #[test] + fn test_invalid_add() { + test_execution_error( + "'foo' + 10", + ExecutionError::UnsupportedBinaryOperator("add", "foo".into(), Value::Int(10)), + ); + } + + #[test] + fn test_invalid_div() { + test_execution_error( + "'foo' / 10", + ExecutionError::UnsupportedBinaryOperator("div", "foo".into(), Value::Int(10)), + ); + } + + #[test] + fn test_invalid_rem() { + test_execution_error( + "'foo' % 10", + ExecutionError::UnsupportedBinaryOperator("rem", "foo".into(), Value::Int(10)), + ); + } + + #[test] + fn out_of_bound_list_access() { + let program = Program::compile("list[10]").unwrap(); + let mut context = Context::default(); + context + .add_variable("list", Value::List(Arc::new(vec![]))) + .unwrap(); + let result = program.execute(&context); + assert_eq!(result.unwrap(), Value::Null); + } + + #[test] + fn reference_to_value() { + let test = "example".to_string(); + let direct: Value = test.as_str().into(); + assert_eq!(direct, Value::String(Arc::new(String::from("example")))); + + let vec = vec![test.as_str()]; + let indirect: Value = vec.into(); + assert_eq!( + indirect, + Value::List(Arc::new(vec![Value::String(Arc::new(String::from( + "example" + )))])) + ); + } + + #[test] + fn test_short_circuit_and() { + let mut context = Context::default(); + let data: HashMap = HashMap::new(); + context.add_variable_from_value("data", data); + + let program = Program::compile("has(data.x) && data.x.startsWith(\"foo\")").unwrap(); + let value = program.execute(&context); + assert!( + value.is_ok(), + "The AND expression should support short-circuit evaluation." + ); + } +} diff --git a/libs/core/cel-rust/interpreter/src/resolvers.rs b/libs/core/cel-rust/interpreter/src/resolvers.rs new file mode 100644 index 0000000..0d9fa3b --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/resolvers.rs @@ -0,0 +1,64 @@ +use crate::{ExecutionError, FunctionContext, ResolveResult, Value}; +use cel_parser::Expression; + +/// Resolver knows how to resolve a [`Value`] from a [`FunctionContext`]. +/// At their core, resolvers are responsible for taking Expressions and +/// turned them into values, but this trait allows us to abstract away +/// some of the complexity surrounding how the expression is obtained in +/// the first place. +/// +/// For example, the [`Argument`] resolver takes an index and resolves the +/// corresponding argument from the [`FunctionContext`]. Resolver makes it +/// easy to (1) get the expression for a specific argument index, (2) +/// return an error if the argument is missing, and (3) resolve the expression +/// into a value. +pub trait Resolver { + fn resolve(&self, ctx: &FunctionContext) -> ResolveResult; +} + +impl Resolver for Expression { + fn resolve(&self, ctx: &FunctionContext) -> ResolveResult { + Value::resolve(self, ctx.ptx) + } +} + +/// Argument is a [`Resolver`] that resolves to the nth argument. +/// +/// # Example +/// ```skip +/// let arg0 = ftx.resolve(Argument(0))?; +/// ``` +pub(crate) struct Argument(pub usize); + +impl Resolver for Argument { + fn resolve(&self, ctx: &FunctionContext) -> ResolveResult { + let index = self.0; + let arg = ctx + .args + .get(index) + .ok_or(ExecutionError::invalid_argument_count( + index + 1, + ctx.args.len(), + ))?; + Value::resolve(arg, ctx.ptx) + } +} + +/// A resolver for all arguments passed to a function. Each argument will be +/// resolved and then returned as a [`Value::List`] +/// +/// # Example +/// ```skip +/// let args = ftx.resolve(AllArguments)?; +/// ``` +pub(crate) struct AllArguments; + +impl Resolver for AllArguments { + fn resolve(&self, ctx: &FunctionContext) -> ResolveResult { + let mut args = Vec::with_capacity(ctx.args.len()); + for arg in ctx.args.iter() { + args.push(Value::resolve(arg, ctx.ptx)?); + } + Ok(Value::List(args.into())) + } +} diff --git a/libs/core/cel-rust/interpreter/src/ser.rs b/libs/core/cel-rust/interpreter/src/ser.rs new file mode 100644 index 0000000..683acdb --- /dev/null +++ b/libs/core/cel-rust/interpreter/src/ser.rs @@ -0,0 +1,1372 @@ +// The serde_json crate implements a Serializer for its own Value enum, that is +// almost exactly the same to our Value enum, so this is more or less copied +// from [serde_json](https://github.com/serde-rs/json/blob/master/src/value/ser.rs), +// also mentioned in the [serde documentation](https://serde.rs/). + +use crate::{objects::Key, Value}; +use serde::{ + ser::{self, Impossible, SerializeStruct}, + Serialize, +}; +use std::{collections::HashMap, fmt::Display, iter::FromIterator, sync::Arc}; +use thiserror::Error; + +#[cfg(feature = "chrono")] +use chrono::FixedOffset; + +pub struct Serializer; +pub struct KeySerializer; + +/// A wrapper Duration type which allows conversion to [Value::Duration] for +/// types using automatic conversion with [serde::Serialize]. +/// +/// # Examples +/// +/// ``` +/// use cel_interpreter::{Context, Duration, Program}; +/// use serde::Serialize; +/// +/// #[derive(Serialize)] +/// struct MyStruct { +/// dur: Duration, +/// } +/// +/// let mut context = Context::default(); +/// +/// // MyStruct will be implicitly serialized into the CEL appropriate types +/// context +/// .add_variable( +/// "foo", +/// MyStruct { +/// dur: chrono::Duration::hours(2).into(), +/// }, +/// ) +/// .unwrap(); +/// +/// let program = Program::compile("foo.dur == duration('2h')").unwrap(); +/// let value = program.execute(&context).unwrap(); +/// assert_eq!(value, true.into()); +/// ``` +#[cfg(feature = "chrono")] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct Duration(pub chrono::Duration); + +#[cfg(feature = "chrono")] +impl Duration { + // Since serde can't natively represent durations, we serialize a special + // newtype to indicate we want to rebuild the duration in the result, while + // remaining compatible with most other Serializer implementations. + const NAME: &str = "$__cel_private_Duration"; + const STRUCT_NAME: &str = "Duration"; + const SECS_FIELD: &str = "secs"; + const NANOS_FIELD: &str = "nanos"; +} + +#[cfg(feature = "chrono")] +impl From for chrono::Duration { + fn from(value: Duration) -> Self { + value.0 + } +} + +#[cfg(feature = "chrono")] +impl From for Duration { + fn from(value: chrono::Duration) -> Self { + Self(value) + } +} + +#[cfg(feature = "chrono")] +impl ser::Serialize for Duration { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: ser::Serializer, + { + // chrono::Duration's Serialize impl isn't stable yet and relies on + // private fields, so attempt to mimic serde's default impl for std + // Duration. + struct DurationProxy(chrono::Duration); + impl Serialize for DurationProxy { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result { + let mut s = serializer.serialize_struct(Duration::STRUCT_NAME, 2)?; + s.serialize_field(Duration::SECS_FIELD, &self.0.num_seconds())?; + s.serialize_field(Duration::NANOS_FIELD, &self.0.subsec_nanos())?; + s.end() + } + } + serializer.serialize_newtype_struct(Self::NAME, &DurationProxy(self.0)) + } +} + +/// A wrapper Timestamp type which allows conversion to [Value::Timestamp] for +/// types using automatic conversion with [serde::Serialize]. +/// +/// # Examples +/// +/// ``` +/// use cel_interpreter::{Context, Timestamp, Program}; +/// use serde::Serialize; +/// +/// #[derive(Serialize)] +/// struct MyStruct { +/// ts: Timestamp, +/// } +/// +/// let mut context = Context::default(); +/// +/// // MyStruct will be implicitly serialized into the CEL appropriate types +/// context +/// .add_variable( +/// "foo", +/// MyStruct { +/// ts: chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z") +/// .unwrap() +/// .into(), +/// }, +/// ) +/// .unwrap(); +/// +/// let program = Program::compile("foo.ts == timestamp('2025-01-01T00:00:00Z')").unwrap(); +/// let value = program.execute(&context).unwrap(); +/// assert_eq!(value, true.into()); +/// ``` +#[cfg(feature = "chrono")] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct Timestamp(pub chrono::DateTime); + +#[cfg(feature = "chrono")] +impl Timestamp { + // Since serde can't natively represent timestamps, we serialize a special + // newtype to indicate we want to rebuild the timestamp in the result, + // while remaining compatible with most other Serializer implementations. + const NAME: &str = "$__cel_private_Timestamp"; +} + +#[cfg(feature = "chrono")] +impl From for chrono::DateTime { + fn from(value: Timestamp) -> Self { + value.0 + } +} + +#[cfg(feature = "chrono")] +impl From> for Timestamp { + fn from(value: chrono::DateTime) -> Self { + Self(value) + } +} + +#[cfg(feature = "chrono")] +impl ser::Serialize for Timestamp { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: ser::Serializer, + { + serializer.serialize_newtype_struct(Self::NAME, &self.0) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum SerializationError { + InvalidKey(String), + SerdeError(String), +} + +impl ser::Error for SerializationError { + fn custom(msg: T) -> Self + where + T: std::fmt::Display, + { + SerializationError::SerdeError(msg.to_string()) + } +} + +impl Display for SerializationError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SerializationError::SerdeError(msg) => formatter.write_str(msg), + SerializationError::InvalidKey(msg) => formatter.write_str(msg), + } + } +} + +pub type Result = std::result::Result; + +pub fn to_value(value: T) -> Result +where + T: Serialize, +{ + value.serialize(Serializer) +} + +impl ser::Serializer for Serializer { + type Ok = Value; + type Error = SerializationError; + + type SerializeSeq = SerializeVec; + type SerializeTuple = SerializeVec; + type SerializeTupleStruct = SerializeVec; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeMap; + type SerializeStructVariant = SerializeStructVariant; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Value::Bool(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_i64(i64::from(v)) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_i64(i64::from(v)) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_i64(i64::from(v)) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Value::Int(v)) + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_u64(u64::from(v)) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_u64(u64::from(v)) + } + + fn serialize_u32(self, v: u32) -> Result { + self.serialize_u64(u64::from(v)) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Value::UInt(v)) + } + + fn serialize_f32(self, v: f32) -> Result { + self.serialize_f64(f64::from(v)) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Value::Float(v)) + } + + fn serialize_char(self, v: char) -> Result { + self.serialize_str(&v.to_string()) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Value::String(Arc::new(v.to_string()))) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(Value::Bytes(Arc::new(v.to_vec()))) + } + + fn serialize_none(self) -> Result { + self.serialize_unit() + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(Value::Null) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct(self, name: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + match name { + #[cfg(feature = "chrono")] + Duration::NAME => value.serialize(TimeSerializer::Duration), + #[cfg(feature = "chrono")] + Timestamp::NAME => value.serialize(TimeSerializer::Timestamp), + _ => value.serialize(self), + } + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Ok(HashMap::from_iter([(variant.to_string(), value.serialize(Serializer)?)]).into()) + } + + fn serialize_seq(self, _len: Option) -> Result { + Ok(SerializeVec { + vec: Vec::with_capacity(_len.unwrap_or(0)), + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeTupleVariant { + name: String::from(variant), + vec: Vec::with_capacity(_len), + }) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(SerializeMap { + map: HashMap::new(), + next_key: None, + }) + } + + fn serialize_struct(self, _name: &'static str, len: usize) -> Result { + self.serialize_map(Some(len)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeStructVariant { + name: String::from(variant), + map: HashMap::new(), + }) + } +} + +pub struct SerializeVec { + vec: Vec, +} + +pub struct SerializeTupleVariant { + name: String, + vec: Vec, +} + +pub struct SerializeMap { + map: HashMap, + next_key: Option, +} + +pub struct SerializeStructVariant { + name: String, + map: HashMap, +} + +#[cfg(feature = "chrono")] +#[derive(Debug, Default)] +struct SerializeTimestamp { + secs: i64, + nanos: i32, +} + +impl ser::SerializeSeq for SerializeVec { + type Ok = Value; + type Error = SerializationError; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.vec.push(to_value(value)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::List(Arc::new(self.vec))) + } +} + +impl ser::SerializeTuple for SerializeVec { + type Ok = Value; + type Error = SerializationError; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl ser::SerializeTupleStruct for SerializeVec { + type Ok = Value; + type Error = SerializationError; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl ser::SerializeTupleVariant for SerializeTupleVariant { + type Ok = Value; + type Error = SerializationError; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.vec.push(to_value(value)?); + Ok(()) + } + + fn end(self) -> Result { + let map = HashMap::from_iter([(self.name, Arc::new(self.vec))]); + Ok(map.into()) + } +} + +impl ser::SerializeMap for SerializeMap { + type Ok = Value; + type Error = SerializationError; + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.next_key = Some(key.serialize(KeySerializer)?); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.map.insert( + self.next_key.clone().ok_or_else(|| { + SerializationError::InvalidKey( + "serialize_value called before serialize_key".to_string(), + ) + })?, + value.serialize(Serializer)?, + ); + Ok(()) + } + + fn end(self) -> Result { + Ok(self.map.into()) + } +} + +impl ser::SerializeStruct for SerializeMap { + type Ok = Value; + type Error = SerializationError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeMap::serialize_entry(self, key, value) + } + + fn end(self) -> Result { + serde::ser::SerializeMap::end(self) + } +} + +impl ser::SerializeStructVariant for SerializeStructVariant { + type Ok = Value; + type Error = SerializationError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.map + .insert(key.serialize(KeySerializer)?, to_value(value)?); + Ok(()) + } + + fn end(self) -> Result { + let map: HashMap = HashMap::from_iter([(self.name, self.map.into())]); + Ok(map.into()) + } +} + +#[cfg(feature = "chrono")] +impl ser::SerializeStruct for SerializeTimestamp { + type Ok = Value; + type Error = SerializationError; + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> std::result::Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + match key { + Duration::SECS_FIELD => { + let Value::Int(val) = value.serialize(Serializer)? else { + return Err(SerializationError::SerdeError( + "invalid type of value in timestamp struct".to_owned(), + )); + }; + self.secs = val; + Ok(()) + } + Duration::NANOS_FIELD => { + let Value::Int(val) = value.serialize(Serializer)? else { + return Err(SerializationError::SerdeError( + "invalid type of value in timestamp struct".to_owned(), + )); + }; + self.nanos = val.try_into().map_err(|_| { + SerializationError::SerdeError( + "timestamp struct nanos field is invalid".to_owned(), + ) + })?; + Ok(()) + } + _ => Err(SerializationError::SerdeError( + "invalid field in duration struct".to_owned(), + )), + } + } + + fn end(self) -> std::result::Result { + Ok(chrono::Duration::seconds(self.secs) + .checked_add(&chrono::Duration::nanoseconds(self.nanos.into())) + .unwrap() + .into()) + } +} + +impl ser::Serializer for KeySerializer { + type Ok = Key; + type Error = SerializationError; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Key::Bool(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_i64(i64::from(v)) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_i64(i64::from(v)) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_i64(i64::from(v)) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Key::Int(v)) + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_u64(u64::from(v)) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_u64(u64::from(v)) + } + + fn serialize_u32(self, v: u32) -> Result { + self.serialize_u64(u64::from(v)) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Key::Uint(v)) + } + + fn serialize_f32(self, _v: f32) -> Result { + Err(SerializationError::InvalidKey( + "Float is not supported".to_string(), + )) + } + + fn serialize_f64(self, _v: f64) -> Result { + Err(SerializationError::InvalidKey( + "Float is not supported".to_string(), + )) + } + + fn serialize_char(self, v: char) -> Result { + self.serialize_str(&v.to_string()) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Key::String(Arc::new(v.to_string()))) + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(SerializationError::InvalidKey( + "Bytes are not supported".to_string(), + )) + } + + fn serialize_none(self) -> Result { + Err(SerializationError::InvalidKey( + "None is not supported".to_string(), + )) + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Err(SerializationError::InvalidKey( + "Null is not supported".to_string(), + )) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(SerializationError::InvalidKey( + "Empty unit structs are not supported".to_string(), + )) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(Key::String(Arc::new(variant.to_string()))) + } + + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(KeySerializer) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Err(SerializationError::InvalidKey( + "Newtype variant is not supported".to_string(), + )) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(SerializationError::InvalidKey( + "Sequences are not supported".to_string(), + )) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(SerializationError::InvalidKey( + "Tuples are not supported".to_string(), + )) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerializationError::InvalidKey( + "Structs are not supported".to_string(), + )) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerializationError::InvalidKey( + "Tuple variants are not supported".to_string(), + )) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(SerializationError::InvalidKey( + "Map variants are not supported".to_string(), + )) + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + Err(SerializationError::InvalidKey( + "Structs are not supported".to_string(), + )) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerializationError::InvalidKey( + "Struct variants are not supported".to_string(), + )) + } +} + +#[cfg(feature = "chrono")] +#[derive(Debug)] +enum TimeSerializer { + Duration, + Timestamp, +} + +#[cfg(feature = "chrono")] +impl ser::Serializer for TimeSerializer { + type Ok = Value; + type Error = SerializationError; + + type SerializeStruct = SerializeTimestamp; + + // Should never be used, so just reuse existing. + type SerializeSeq = SerializeVec; + type SerializeTuple = SerializeVec; + type SerializeTupleStruct = SerializeVec; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStructVariant = SerializeStructVariant; + + fn serialize_struct(self, name: &'static str, len: usize) -> Result { + if !matches!(self, Self::Duration { .. }) || name != Duration::STRUCT_NAME { + return Err(SerializationError::SerdeError( + "expected Duration struct with Duration marker newtype struct".to_owned(), + )); + } + if len != 2 { + return Err(SerializationError::SerdeError( + "expected Duration struct to have 2 fields".to_owned(), + )); + } + Ok(SerializeTimestamp::default()) + } + + fn serialize_str(self, v: &str) -> Result { + if !matches!(self, Self::Timestamp) { + return Err(SerializationError::SerdeError( + "expected Timestamp string with Timestamp marker newtype struct".to_owned(), + )); + } + Ok(v.parse::>() + .map_err(|e| SerializationError::SerdeError(e.to_string()))? + .into()) + } + + fn serialize_bool(self, _v: bool) -> Result { + unreachable!() + } + + fn serialize_i8(self, _v: i8) -> Result { + unreachable!() + } + + fn serialize_i16(self, _v: i16) -> Result { + unreachable!() + } + + fn serialize_i32(self, _v: i32) -> Result { + unreachable!() + } + + fn serialize_i64(self, _v: i64) -> Result { + unreachable!() + } + + fn serialize_u8(self, _v: u8) -> Result { + unreachable!() + } + + fn serialize_u16(self, _v: u16) -> Result { + unreachable!() + } + + fn serialize_u32(self, _v: u32) -> Result { + unreachable!() + } + + fn serialize_u64(self, _v: u64) -> Result { + unreachable!() + } + + fn serialize_f32(self, _v: f32) -> Result { + unreachable!() + } + + fn serialize_f64(self, _v: f64) -> Result { + unreachable!() + } + + fn serialize_char(self, _v: char) -> Result { + unreachable!() + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + unreachable!() + } + + fn serialize_none(self) -> Result { + unreachable!() + } + + fn serialize_some(self, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + unreachable!() + } + + fn serialize_unit(self) -> Result { + unreachable!() + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unreachable!() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unreachable!() + } + + fn serialize_newtype_struct(self, _name: &'static str, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + unreachable!() + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + unreachable!() + } + + fn serialize_seq(self, _len: Option) -> Result { + unreachable!() + } + + fn serialize_tuple(self, _len: usize) -> Result { + unreachable!() + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + fn serialize_map(self, _len: Option) -> Result { + unreachable!() + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } +} + +#[cfg(test)] +mod tests { + use crate::{objects::Key, to_value, Value}; + use crate::{Context, Program}; + use serde::Serialize; + use serde_bytes::Bytes; + use std::{collections::HashMap, iter::FromIterator, sync::Arc}; + + #[cfg(feature = "chrono")] + use super::{Duration, Timestamp}; + + macro_rules! primitive_test { + ($functionName:ident, $strValue: literal, $value: expr) => { + #[test] + fn $functionName() { + let program = Program::compile($strValue).unwrap(); + let result = program.execute(&Context::default()); + assert_eq!(Value::from($value), result.unwrap()); + } + }; + } + + primitive_test!(test_u64_zero, "0u", 0_u64); + primitive_test!(test_i64_zero, "0", 0_i64); + primitive_test!(test_f64_zero, "0.0", 0_f64); + //primitive_test!(test_f64_zero, "0.", 0_f64); this test fails + primitive_test!(test_bool_false, "false", false); + primitive_test!(test_bool_true, "true", true); + primitive_test!(test_string_empty, "\"\"", ""); + primitive_test!(test_string_non_empty, "\"test\"", "test"); + primitive_test!(test_byte_ones, r#"b"\001\001""#, vec!(1_u8, 1_u8)); + // primitive_test!(test_triple_double_quoted_string, #"r"""""""#, ""); + // primitive_test!(test_triple_single_quoted_string, "r''''''", ""); + primitive_test!(test_utf8_character_as_bytes, "b'รฟ'", vec!(195_u8, 191_u8)); + + #[test] + fn test_json_data_conversion() { + #[derive(Serialize)] + struct TestPrimitives { + bool: bool, + u8: u8, + u16: u16, + u32: u32, + u64: u64, + int8: i8, + int16: i16, + int32: i32, + int64: i64, + f32: f32, + f64: f64, + char: char, + string: String, + bytes: &'static Bytes, + } + + let test = TestPrimitives { + bool: true, + int8: 8_i8, + int16: 16_i16, + int32: 32_i32, + int64: 64_i64, + u8: 8_u8, + u16: 16_u16, + u32: 32_u32, + u64: 64_u64, + f32: 0.32_f32, + f64: 0.64_f64, + char: 'a', + string: "string".to_string(), + bytes: Bytes::new(&[1_u8, 1_u8, 1_u8, 1_u8]), + }; + + let serialized = to_value(test).unwrap(); + let expected: Value = HashMap::from_iter([ + (Key::String(Arc::new("bool".to_string())), Value::Bool(true)), + (Key::String(Arc::new("int8".to_string())), Value::Int(8)), + (Key::String(Arc::new("int16".to_string())), Value::Int(16)), + (Key::String(Arc::new("int32".to_string())), Value::Int(32)), + (Key::String(Arc::new("int64".to_string())), Value::Int(64)), + (Key::String(Arc::new("u8".to_string())), Value::UInt(8)), + (Key::String(Arc::new("u16".to_string())), Value::UInt(16)), + (Key::String(Arc::new("u32".to_string())), Value::UInt(32)), + (Key::String(Arc::new("u64".to_string())), Value::UInt(64)), + ( + Key::String(Arc::new("f32".to_string())), + Value::Float(f64::from(0.32_f32)), + ), + (Key::String(Arc::new("f64".to_string())), Value::Float(0.64)), + ( + Key::String(Arc::new("char".to_string())), + Value::String(Arc::new("a".to_string())), + ), + ( + Key::String(Arc::new("string".to_string())), + Value::String(Arc::new("string".to_string())), + ), + ( + Key::String(Arc::new("bytes".to_string())), + Value::Bytes(Arc::new(vec![1, 1, 1, 1])), + ), + ]) + .into(); + + // Test with CEL because iterator is not implemented for Value::Map + let program = Program::compile( + "expected.all(key, (has(serialized[key]) && (serialized[key] == expected[key])))", + ) + .unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("serialized", serialized).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()) + } + + #[derive(Serialize)] + enum TestCompoundTypes { + Unit, + Newtype(u32), + Wrapped(Option), + Tuple(u32, u32), + Struct { + a: i32, + nested: HashMap>>, + }, + Map(HashMap), + } + #[test] + fn test_unit() { + let unit = to_value(TestCompoundTypes::Unit).unwrap(); + let expected: Value = "Unit".into(); + let program = Program::compile("test == expected").unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("test", unit).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()) + } + #[test] + fn test_newtype() { + let newtype = to_value(TestCompoundTypes::Newtype(32)).unwrap(); + let expected: Value = HashMap::from([("Newtype", Value::UInt(32))]).into(); + let program = Program::compile("test == expected").unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("test", newtype).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()) + } + #[test] + fn test_options() { + // Test Option serialization + let wrapped = to_value(TestCompoundTypes::Wrapped(None)).unwrap(); + let expected: Value = HashMap::from([("Wrapped", Value::Null)]).into(); + let program = Program::compile("test == expected").unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("test", wrapped).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); + + let wrapped = to_value(TestCompoundTypes::Wrapped(Some(8))).unwrap(); + let expected: Value = HashMap::from([("Wrapped", Value::UInt(8))]).into(); + let program = Program::compile("test == expected").unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("test", wrapped).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()) + } + + #[test] + fn test_tuples() { + // Test Tuple serialization + let tuple = to_value(TestCompoundTypes::Tuple(12, 16)).unwrap(); + let expected: Value = HashMap::from([( + "Tuple", + Value::List(Arc::new(vec![12_u64.into(), 16_u64.into()])), + )]) + .into(); + let program = Program::compile("test == expected").unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("test", tuple).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()) + } + + #[test] + fn test_structs() { + // Test Struct serialization + let test_struct = TestCompoundTypes::Struct { + a: 32_i32, + nested: HashMap::from_iter([( + true, + HashMap::from_iter([( + "Test".to_string(), + vec!["a".to_string(), "b".to_string(), "c".to_string()], + )]), + )]), + }; + let expected: Value = HashMap::::from([( + "Struct".into(), + HashMap::::from_iter([ + ("a".into(), 32_i32.into()), + ( + "nested".into(), + HashMap::::from_iter([( + true.into(), + HashMap::::from_iter([( + "Test".into(), + vec!["a".to_string(), "b".to_string(), "c".to_string()].into(), + )]) + .into(), + )]) + .into(), + ), + ]) + .into(), + )]) + .into(); + let program = Program::compile("expected.all(key, test[key] == expected[key])").unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("test", test_struct).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); + } + + #[test] + fn test_maps() { + // Test Map serialization + let map = to_value(TestCompoundTypes::Map( + HashMap::::from_iter([( + "Test".to_string(), + Bytes::new(&[0_u8, 0_u8, 0_u8, 0_u8]), + )]), + )) + .unwrap(); + let expected: Value = HashMap::from([( + "Map", + HashMap::::from_iter([( + "Test".into(), + Value::Bytes(Arc::new(vec![0_u8, 0_u8, 0_u8, 0_u8])), + )]), + )]) + .into(); + assert_eq!(map, expected) + } + + #[cfg(feature = "chrono")] + #[derive(Serialize)] + struct TestTimeTypes { + dur: Duration, + ts: Timestamp, + } + + #[cfg(feature = "chrono")] + #[test] + fn test_time_types() { + use chrono::FixedOffset; + + let tests = to_value([ + TestTimeTypes { + dur: chrono::Duration::milliseconds(1527).into(), + ts: chrono::DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00") + .unwrap() + .into(), + }, + // Let's test chrono::Duration's particular handling around math + // and negatives and timestamps from BCE. + TestTimeTypes { + dur: chrono::Duration::milliseconds(-1527).into(), + ts: "-0001-12-01T00:00:00-08:00" + .parse::>() + .unwrap() + .into(), + }, + TestTimeTypes { + dur: (chrono::Duration::seconds(1) - chrono::Duration::nanoseconds(1000000001)) + .into(), + ts: chrono::DateTime::parse_from_rfc3339("0001-12-01T00:00:00+08:00") + .unwrap() + .into(), + }, + TestTimeTypes { + dur: (chrono::Duration::seconds(-1) + chrono::Duration::nanoseconds(1000000001)) + .into(), + ts: chrono::DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00") + .unwrap() + .into(), + }, + ]) + .unwrap(); + let expected: Value = vec![ + Value::Map( + HashMap::<_, Value>::from([ + ("dur", chrono::Duration::milliseconds(1527).into()), + ( + "ts", + chrono::DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00") + .unwrap() + .into(), + ), + ]) + .into(), + ), + Value::Map( + HashMap::<_, Value>::from([ + ("dur", chrono::Duration::nanoseconds(-1527000000).into()), + ( + "ts", + "-0001-12-01T00:00:00-08:00" + .parse::>() + .unwrap() + .into(), + ), + ]) + .into(), + ), + Value::Map( + HashMap::<_, Value>::from([ + ("dur", chrono::Duration::nanoseconds(-1).into()), + ( + "ts", + chrono::DateTime::parse_from_rfc3339("0001-12-01T00:00:00+08:00") + .unwrap() + .into(), + ), + ]) + .into(), + ), + Value::Map( + HashMap::<_, Value>::from([ + ("dur", chrono::Duration::nanoseconds(1).into()), + ( + "ts", + chrono::DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00") + .unwrap() + .into(), + ), + ]) + .into(), + ), + ] + .into(); + assert_eq!(tests, expected); + + let program = Program::compile("test == expected").unwrap(); + let mut context = Context::default(); + context.add_variable("expected", expected).unwrap(); + context.add_variable("test", tests).unwrap(); + let value = program.execute(&context).unwrap(); + assert_eq!(value, true.into()); + } + + #[cfg(feature = "chrono")] + #[cfg(feature = "json")] + #[test] + fn test_time_json() { + use chrono::FixedOffset; + + // Test that Durations and Timestamps serialize correctly with + // serde_json. + let tests = [ + TestTimeTypes { + dur: chrono::Duration::milliseconds(1527).into(), + ts: chrono::DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00") + .unwrap() + .into(), + }, + TestTimeTypes { + dur: chrono::Duration::milliseconds(-1527).into(), + ts: "-0001-12-01T00:00:00-08:00" + .parse::>() + .unwrap() + .into(), + }, + TestTimeTypes { + dur: (chrono::Duration::seconds(1) - chrono::Duration::nanoseconds(1000000001)) + .into(), + ts: chrono::DateTime::parse_from_rfc3339("0001-12-01T00:00:00+08:00") + .unwrap() + .into(), + }, + TestTimeTypes { + dur: (chrono::Duration::seconds(-1) + chrono::Duration::nanoseconds(1000000001)) + .into(), + ts: chrono::DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00") + .unwrap() + .into(), + }, + ]; + + let expect = "[\ +{\"dur\":{\"secs\":1,\"nanos\":527000000},\"ts\":\"1996-12-19T16:39:57-08:00\"},\ +{\"dur\":{\"secs\":-1,\"nanos\":-527000000},\"ts\":\"-0001-12-01T00:00:00-08:00\"},\ +{\"dur\":{\"secs\":0,\"nanos\":-1},\"ts\":\"0001-12-01T00:00:00+08:00\"},\ +{\"dur\":{\"secs\":0,\"nanos\":1},\"ts\":\"1996-12-19T16:39:57-08:00\"}\ +]"; + let actual = serde_json::to_string(&tests).unwrap(); + assert_eq!(actual, expect); + } +} diff --git a/libs/core/cel-rust/parser/CHANGELOG.md b/libs/core/cel-rust/parser/CHANGELOG.md new file mode 100644 index 0000000..95f9d00 --- /dev/null +++ b/libs/core/cel-rust/parser/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.8.0](https://github.com/clarkmcc/cel-rust/compare/cel-parser-v0.7.1...cel-parser-v0.8.0) - 2024-10-30 + +### Other + +- Detailed parse error ([#102](https://github.com/clarkmcc/cel-rust/pull/102)) +- Update lalrpop to 0.22 from 0.19.x ([#99](https://github.com/clarkmcc/cel-rust/pull/99)) +- Fix `clippy::empty_line_after_doc_comments` lints ([#98](https://github.com/clarkmcc/cel-rust/pull/98)) +- Conformance test fixes ([#79](https://github.com/clarkmcc/cel-rust/pull/79)) diff --git a/libs/core/cel-rust/parser/Cargo.toml b/libs/core/cel-rust/parser/Cargo.toml new file mode 100644 index 0000000..0c48312 --- /dev/null +++ b/libs/core/cel-rust/parser/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cel-parser" +description = "A parser for the Common Expression Language (CEL)" +repository = "https://github.com/clarkmcc/cel-rust" +version = "0.8.0" +authors = ["Tom Forbes ", "Clark McCauley "] +edition = "2021" +license = "MIT" +categories = ["parsing"] + +[dependencies] +lalrpop-util = { version = "0.22.0", features = ["lexer"] } +regex = "1.4.2" +thiserror = "1.0.40" + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } + +[build-dependencies] +lalrpop = { version = "0.22.0", features = ["lexer"] } + +[[bench]] +name = "runtime" +harness = false diff --git a/libs/core/cel-rust/parser/README.md b/libs/core/cel-rust/parser/README.md new file mode 100644 index 0000000..029b91c --- /dev/null +++ b/libs/core/cel-rust/parser/README.md @@ -0,0 +1,14 @@ +# CEL Parser + +This module implements a LALRPOP parser for the [Common Expression Language](https://github.com/google/cel-spec). + +## Usage: + +```rust +use cel_parser::parse; + +pub fn main() { + let expr = parse("1 + 1").unwrap(); + println!("{:?}", expr); +} +``` diff --git a/libs/core/cel-rust/parser/benches/runtime.rs b/libs/core/cel-rust/parser/benches/runtime.rs new file mode 100644 index 0000000..4a5ffa0 --- /dev/null +++ b/libs/core/cel-rust/parser/benches/runtime.rs @@ -0,0 +1,35 @@ +use cel_parser::{parse_bytes, parse_string}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +pub fn parse_string_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("string parsing"); + let expressions = vec![ + ("text", "\"text\""), + ("raw", "r\"text\""), + ("single unicode escape sequence", "\"\\U0001f431\""), + ("single hex escape sequence", "\"\\x0D\""), + ("single oct escape sequence", "\"\\015\""), + ]; + + for (name, expr) in black_box(expressions) { + group.bench_function(name, |b| b.iter(|| parse_string(expr))); + } + group.finish() +} + +pub fn parse_bytes_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("bytes parsing"); + let expressions = vec![ + ("bytes", "text"), + ("single hex escape sequence", "x0D"), + ("single oct escape sequence", "015"), + ]; + + for (name, expr) in black_box(expressions) { + group.bench_function(name, |b| b.iter(|| parse_bytes(expr))); + } + group.finish() +} + +criterion_group!(benches, parse_string_benchmark, parse_bytes_benchmark); +criterion_main!(benches); diff --git a/libs/core/cel-rust/parser/build.rs b/libs/core/cel-rust/parser/build.rs new file mode 100644 index 0000000..23c7d3f --- /dev/null +++ b/libs/core/cel-rust/parser/build.rs @@ -0,0 +1,5 @@ +extern crate lalrpop; + +fn main() { + lalrpop::process_root().unwrap(); +} diff --git a/libs/core/cel-rust/parser/src/ast.rs b/libs/core/cel-rust/parser/src/ast.rs new file mode 100644 index 0000000..fd55029 --- /dev/null +++ b/libs/core/cel-rust/parser/src/ast.rs @@ -0,0 +1,223 @@ +use std::collections::HashSet; +use std::sync::Arc; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum RelationOp { + LessThan, + LessThanEq, + GreaterThan, + GreaterThanEq, + Equals, + NotEquals, + In, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum ArithmeticOp { + Add, + Subtract, + Divide, + Multiply, + Modulus, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum UnaryOp { + Not, + DoubleNot, + Minus, + DoubleMinus, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Expression { + Arithmetic(Box, ArithmeticOp, Box), + Relation(Box, RelationOp, Box), + + Ternary(Box, Box, Box), + Or(Box, Box), + And(Box, Box), + Unary(UnaryOp, Box), + + Member(Box, Box), + FunctionCall(Box, Option>, Vec), + + List(Vec), + Map(Vec<(Expression, Expression)>), + + Atom(Atom), + Ident(Arc), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Member { + Attribute(Arc), + Index(Box), + Fields(Vec<(Arc, Expression)>), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Atom { + Int(i64), + UInt(u64), + Float(f64), + String(Arc), + Bytes(Arc>), + Bool(bool), + Null, +} + +/// A collection of all the references that an expression makes to variables and functions. +pub struct ExpressionReferences<'expr> { + variables: HashSet<&'expr str>, + functions: HashSet<&'expr str>, +} + +impl ExpressionReferences<'_> { + /// Returns true if the expression references the provided variable name. + /// + /// # Example + /// ```rust + /// # use cel_parser::parse; + /// let expression = parse("foo.bar == true").unwrap(); + /// let references = expression.references(); + /// assert!(references.has_variable("foo")); + /// ``` + pub fn has_variable(&self, name: impl AsRef) -> bool { + self.variables.contains(name.as_ref()) + } + + /// Returns true if the expression references the provided variable name. + /// + /// # Example + /// ```rust + /// # use cel_parser::parse; + /// let expression = parse("size(foo) > 0").unwrap(); + /// let references = expression.references(); + /// assert!(references.has_function("size")); + /// ``` + pub fn has_function(&self, name: impl AsRef) -> bool { + self.functions.contains(name.as_ref()) + } + + /// Returns a list of all variables referenced in the expression. + /// + /// # Example + /// ```rust + /// # use cel_parser::parse; + /// let expression = parse("foo.bar == true").unwrap(); + /// let references = expression.references(); + /// assert_eq!(vec!["foo"], references.variables()); + /// ``` + pub fn variables(&self) -> Vec<&str> { + self.variables.iter().copied().collect() + } + + /// Returns a list of all functions referenced in the expression. + /// + /// # Example + /// ```rust + /// # use cel_parser::parse; + /// let expression = parse("size(foo) > 0").unwrap(); + /// let references = expression.references(); + /// assert_eq!(vec!["size"], references.functions()); + /// ``` + pub fn functions(&self) -> Vec<&str> { + self.functions.iter().copied().collect() + } +} + +impl Expression { + /// Returns a set of all variables referenced in the expression. Variable identifiers + /// are represented internally as [`Arc`] and this function simply clones those + /// references into a [`HashSet`]. + /// + /// # Example + /// ```rust + /// # use cel_parser::parse; + /// let expression = parse("foo && size(foo) > 0").unwrap(); + /// let references = expression.references(); + /// + /// assert!(references.has_variable("foo")); + /// assert!(references.has_function("size")); + /// ``` + pub fn references(&self) -> ExpressionReferences { + let mut variables = HashSet::new(); + let mut functions = HashSet::new(); + self._references(&mut variables, &mut functions); + ExpressionReferences { + variables, + functions, + } + } + + /// Internal recursive function to collect all variable and function references in the expression. + fn _references<'expr>( + &'expr self, + variables: &mut HashSet<&'expr str>, + functions: &mut HashSet<&'expr str>, + ) { + match self { + Expression::Arithmetic(e1, _, e2) + | Expression::Relation(e1, _, e2) + | Expression::Ternary(e1, _, e2) + | Expression::Or(e1, e2) + | Expression::And(e1, e2) => { + e1._references(variables, functions); + e2._references(variables, functions); + } + Expression::Unary(_, e) => { + e._references(variables, functions); + } + Expression::Member(e, _) => { + e._references(variables, functions); + } + Expression::FunctionCall(name, target, args) => { + if let Expression::Ident(v) = &**name { + functions.insert(v.as_str()); + } + if let Some(target) = target { + target._references(variables, functions); + } + for e in args { + e._references(variables, functions); + } + } + Expression::List(e) => { + for e in e { + e._references(variables, functions); + } + } + Expression::Map(v) => { + for (e1, e2) in v { + e1._references(variables, functions); + e2._references(variables, functions); + } + } + Expression::Atom(_) => {} + Expression::Ident(v) => { + variables.insert(v.as_str()); + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::parse; + + #[test] + fn test_references() { + let expr = + parse("foo.bar.baz == true && size(foo) > 0 && foo[0] == 1 && bar.startsWith('a')") + .unwrap(); + let refs = expr.references(); + assert!(refs.has_variable("foo")); + assert!(refs.has_variable("bar")); + assert_eq!(refs.variables.len(), 2); + + assert!(refs.has_function("size")); + assert!(refs.has_function("startsWith")); + assert_eq!(refs.functions.len(), 2); + } +} diff --git a/libs/core/cel-rust/parser/src/cel.lalrpop b/libs/core/cel-rust/parser/src/cel.lalrpop new file mode 100644 index 0000000..57ab0a6 --- /dev/null +++ b/libs/core/cel-rust/parser/src/cel.lalrpop @@ -0,0 +1,172 @@ +use crate::{RelationOp, ArithmeticOp, Expression, UnaryOp, Member, Atom, parse_bytes, parse_string}; +use std::sync::Arc; + +grammar; + +match { + // Skip whitespace and comments + r"\s*" => { }, + r"//[^\n\r]*[\n\r]*" => { }, +} else { + _ +} + +pub Expression: Expression = { + Conditional +}; + +pub Conditional: Expression = { + "?" ":" => Expression::Ternary(condition.into(), if_true.into(), if_false.into()), + LogicalOr +}; + +pub LogicalOr: Expression = { + "||" => Expression::Or(left.into(), right.into()), + LogicalAnd +}; + +pub LogicalAnd: Expression = { + "&&" => Expression::And(left.into(), right.into()), + Relations +}; + +pub Relations: Expression = { + => Expression::Relation(left.into(), op, right.into()), + ArithmeticAddSub +}; + +pub ArithmeticAddSub: Expression = { + => Expression::Arithmetic(left.into(), op, right.into()), + ArithmeticMulDivMod +}; + +pub ArithmeticMulDivMod: Expression = { + => Expression::Arithmetic(left.into(), op, right.into()), + Unary +}; + +pub Unary: Expression = { + => Expression::Unary(op, right.into()), + Member +}; + +pub Member: Expression = { + "." => Expression::Member(left.into(), Member::Attribute(identifier.into()).into()).into(), + "." "(" > ")" => { + Expression::FunctionCall(Expression::Ident(identifier).into(), Some(left.into()), arguments).into() + }, + "[" "]" => Expression::Member(left.into(), Member::Index(expression.into()).into()).into(), + "{" > "}" => Expression::Member(left.into(), Member::Fields(fields.into()).into()).into(), + Primary, +} + +pub Primary: Expression = { + "."? => Expression::Ident(<>.into()).into(), + "."? "(" > ")" => { + Expression::FunctionCall(Expression::Ident(identifier).into(), None, arguments).into() + }, + Atom => Expression::Atom(<>).into(), + "[" > "]" => Expression::List(<>).into(), + "{" > "}" => Expression::Map(<>).into(), + "(" ")" +} + +pub FieldInits: (Arc, Expression) = { + ":" +} + +pub MapInits: (Expression, Expression) = { + ":" +} + +CommaSeparated: Vec = { + ",")*> => match e { + None => v, + Some(e) => { + let mut v = v; + v.push(e); + v + } + } +}; + +ArithmeticOpAddSub: ArithmeticOp = { + "+" => ArithmeticOp::Add, + "-" => ArithmeticOp::Subtract +}; + +ArithmeticOpMulDivMod: ArithmeticOp = { + "*" => ArithmeticOp::Multiply, + "/" => ArithmeticOp::Divide, + "%" => ArithmeticOp::Modulus +}; + + +UnaryOp: UnaryOp = { + "!" => UnaryOp::Not, + "!!" => UnaryOp::DoubleNot, + "-" => UnaryOp::Minus, + "--" => UnaryOp::DoubleMinus, +} + +RelationOp: RelationOp = { + "<" => RelationOp::LessThan, + "<=" => RelationOp::LessThanEq, + ">" => RelationOp::GreaterThan, + ">=" => RelationOp::GreaterThanEq, + "==" => RelationOp::Equals, + "!=" => RelationOp::NotEquals, + "in" => RelationOp::In +} + +Atom: Atom = { + // Integer literals. Annoying to parse :/ + r"-?[0-9]+" => Atom::Int(<>.parse().unwrap()), + => Atom::Int(i64::from_str_radix(&s.chars().filter(|&x| x != 'x' && x != 'X').collect::(), 16).unwrap()), + => Atom::UInt(s[..s.len()-1].parse().unwrap()), + => Atom::UInt(u64::from_str_radix(&s.chars().filter(|&x| x != 'x' && x != 'X' && x != 'u' && x != 'U').collect::(), 16).unwrap()), + + // Float with decimals and optional exponent + r"([-+]?[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?)" => Atom::Float(<>.parse().unwrap()), + // Float with no decimals and required exponent + r"[-+]?[0-9]+[eE][-+]?[0-9]+" => Atom::Float(<>.parse().unwrap()), + + // NOTE: I've commented out some of the more complex string parsing rules + // because they're causing "attempt to subtract with overflow" errors within + // the LALRPOP parser. + + // Double quoted string + // I used ChatGPT to come up with this pattern and the explanation is as follows: + // 1. `"`: Match the opening double quote. + // 2. `([^"\\]*(?:\\.[^"\\]*)*)`: This is the main part of the regex which matches the content inside the double quotes. + // a. `[^"\\]*`: Match any sequence of characters that are neither a double quote nor a backslash. + // b. `(?:\\.[^"\\]*)*`: This part matches an escaped character followed by any sequence of characters that are + // neither a double quote nor a backslash. It uses a non-capturing group (?:...) to repeat the pattern. + // This handles sequences like \", \\, or any other escaped character. + // 3. `"`: Match the closing double quote. + r#""([^"\\]*(?:\\.[^"\\]*)*)""# => Atom::String(parse_string(<>).unwrap().into()), + r#"[rR]"([^"\\]*(?:\\.[^"\\]*)*)""# => Atom::String(parse_string(<>).unwrap().into()), + // r#""""(\\.|[^"{3}])*""""# => Atom::String(<>.to_string().into()), + + // Single quoted string + // Uses similar regex as above, but replace double quote with a single one + r#"'([^'\\]*(?:\\.[^'\\]*)*)'"# => Atom::String(parse_string(<>).unwrap().into()), + r#"[rR]'([^'\\]*(?:\\.[^'\\]*)*)'"# => Atom::String(parse_string(<>).unwrap().into()), + // r#"'''(\\.|[^'{3}])*'''"# => Atom::String(<>.to_string().into()), + + // Double quoted bytes + r#"[bB]"(\\.|[^"\n])*""# => Atom::Bytes(parse_bytes(&<>[2..<>.len()-1]).unwrap().into()), + // r#"[bB]"""(\\.|[^"{3}])*""""# => Atom::Bytes(Vec::from(<>.as_bytes()).into()), + + // Single quoted bytes + r#"[bB]'(\\.|[^'\n])*'"# => Atom::Bytes(parse_bytes(&<>[2..<>.len()-1]).unwrap().into()), + // r#"[bB]'''(\\.|[^'{3}])*'''"# => Atom::Bytes(Vec::from(<>.as_bytes()).into()), + + "true" => Atom::Bool(true), + "false" => Atom::Bool(false), + "null" => Atom::Null, +}; + +Ident: Arc = { + r"[_a-zA-Z][_a-zA-Z0-9]*" => Arc::from(<>.to_string()) +} \ No newline at end of file diff --git a/libs/core/cel-rust/parser/src/error.rs b/libs/core/cel-rust/parser/src/error.rs new file mode 100644 index 0000000..e6279e1 --- /dev/null +++ b/libs/core/cel-rust/parser/src/error.rs @@ -0,0 +1,140 @@ +use std::{fmt::Display, iter}; + +use lalrpop_util::lexer::Token; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq)] +pub struct Location { + pub line: usize, + pub column: usize, + pub absolute: usize, +} + +#[derive(Debug, Default)] +pub struct Span { + pub start: Option, + pub end: Option, +} + +impl Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match (&self.start, &self.end) { + (Some(start), Some(end)) if start == end => { + write!(f, "[{}:{}]", start.line, start.column) + } + (Some(location), None) | (None, Some(location)) => { + write!(f, "[{}:{}]", location.line, location.column) + } + (Some(start), Some(end)) => write!( + f, + "[{}:{}]->[{}:{}]", + start.line, start.column, end.line, end.column + ), + (None, None) => write!(f, "?",), + } + } +} + +impl Span { + fn single_location(location: Location) -> Span { + Span { + start: Some(location.clone()), + end: Some(location), + } + } +} + +#[derive(Error, Debug)] +#[error("Error parsing: {msg} at {span}")] +pub struct ParseError { + pub msg: String, + pub expected: Vec, + pub span: Span, +} + +impl ParseError { + pub(super) fn from_lalrpop( + src_str: &str, + err: lalrpop_util::ParseError, &str>, + ) -> Self { + use lalrpop_util::ParseError::*; + + match err { + InvalidToken { location } => ParseError { + span: byte_pos_to_src_location(src_str, location) + .map(Span::single_location) + .unwrap_or_default(), + expected: Vec::new(), + msg: "invalid token".into(), + }, + UnrecognizedEof { location, expected } => ParseError { + msg: "unrecognized eof".into(), + span: byte_pos_to_src_location(src_str, location) + .map(Span::single_location) + .unwrap_or_default(), + expected, + }, + UnrecognizedToken { + token: (start, token, end), + expected, + } => ParseError { + msg: format!("unrecognized token: '{}'", token), + span: Span { + start: byte_pos_to_src_location(src_str, start), + end: byte_pos_to_src_location(src_str, end), + }, + expected, + }, + ExtraToken { + token: (start, token, end), + } => ParseError { + msg: format!("extra token: '{}'", token), + span: Span { + start: byte_pos_to_src_location(src_str, start), + end: byte_pos_to_src_location(src_str, end), + }, + expected: Vec::new(), + }, + User { error } => ParseError { + msg: error.into(), + expected: Vec::new(), + span: Span::default(), + }, + } + } +} + +// Slightly simplified but heavily inspired by +// https://github.com/gluon-lang/gluon/blob/f8326d21a14b5f21d203e9c43fa5bb7f0688a74c/base/src/source.rs +fn byte_pos_to_src_location(src_str: &str, byte_pos: usize) -> Option { + let src_bytes = src_str.as_bytes(); + let total_len = src_bytes.len(); + + let line_indices: Vec = { + let input_indices = src_bytes + .iter() + .enumerate() + .filter(|&(_, b)| *b == b'\n') + .map(|(i, _)| i + 1); // index of first char in the line + + iter::once(0).chain(input_indices).collect() + }; + + if byte_pos <= total_len { + let num_lines = line_indices.len(); + + let line_index = (0..num_lines) + .find(|&i| line_indices[i] > byte_pos) + .map(|i| i - 1) + .unwrap_or(num_lines - 1); + + let line_byte_pos = line_indices[line_index]; + Some(Location { + line: line_index, + column: byte_pos - line_byte_pos, + absolute: byte_pos, + }) + } else { + None + } +} diff --git a/libs/core/cel-rust/parser/src/lib.rs b/libs/core/cel-rust/parser/src/lib.rs new file mode 100644 index 0000000..121f2f5 --- /dev/null +++ b/libs/core/cel-rust/parser/src/lib.rs @@ -0,0 +1,549 @@ +use lalrpop_util::lalrpop_mod; + +pub mod ast; +pub use ast::*; + +pub mod parse; +pub use parse::*; + +pub mod error; +pub use error::ParseError; + +lalrpop_mod!(#[allow(clippy::all)] pub parser, "/cel.rs"); + +/// Parses a CEL expression and returns it. +/// +/// # Example +/// ``` +/// use cel_parser::parse; +/// let expr = parse("1 + 1").unwrap(); +/// println!("{:?}", expr); +/// ``` +pub fn parse(input: &str) -> Result { + // Wrap the internal parser function - whether larlpop or chumsky + + // Example for a possible new chumsky based parser... + // parser().parse(input) + // .into_result() + // .map_err(|e| { + // ParseError { + // msg: e.iter().map(|e| format!("{}", e)).collect::>().join("\n") + // } + // }) + + // Existing Larlpop Parser: + crate::parser::ExpressionParser::new() + .parse(input) + .map_err(|e| ParseError::from_lalrpop(input, e)) +} + +#[cfg(test)] +mod tests { + use crate::{ + ArithmeticOp, Atom, Atom::*, Expression, Expression::*, Member::*, RelationOp, UnaryOp, + }; + + fn parse(input: &str) -> Expression { + crate::parse(input).unwrap_or_else(|e| panic!("{}", e)) + } + + fn assert_parse_eq(input: &str, expected: Expression) { + assert_eq!(parse(input), expected); + } + + #[test] + fn ident() { + assert_parse_eq("a", Ident("a".to_string().into())); + assert_parse_eq("hello ", Ident("hello".to_string().into())); + } + + #[test] + fn simple_int() { + assert_parse_eq("1", Atom(Int(1))) + } + + #[test] + fn simple_float() { + assert_parse_eq("1.0", Atom(Float(1.0))) + } + + #[test] + fn other_floats() { + assert_parse_eq("1e3", Expression::Atom(Atom::Float(1000.0))); + assert_parse_eq("1e-3", Expression::Atom(Atom::Float(0.001))); + assert_parse_eq("1.4e-3", Expression::Atom(Atom::Float(0.0014))); + } + + #[test] + fn single_quote_str() { + assert_parse_eq("'foobar'", Atom(String("foobar".to_string().into()))) + } + + #[test] + fn double_quote_str() { + assert_parse_eq(r#""foobar""#, Atom(String("foobar".to_string().into()))) + } + + // #[test] + // fn single_quote_raw_str() { + // assert_parse_eq( + // "r'\n'", + // Expression::Atom(String("\n".to_string().into())), + // ); + // } + + #[test] + fn single_quote_bytes() { + assert_parse_eq("b'foo'", Atom(Bytes(b"foo".to_vec().into()))); + assert_parse_eq("b''", Atom(Bytes(b"".to_vec().into()))); + } + + #[test] + fn double_quote_bytes() { + assert_parse_eq(r#"b"foo""#, Atom(Bytes(b"foo".to_vec().into()))); + assert_parse_eq(r#"b"""#, Atom(Bytes(b"".to_vec().into()))); + } + + #[test] + fn bools() { + assert_parse_eq("true", Atom(Bool(true))); + assert_parse_eq("false", Atom(Bool(false))); + } + + #[test] + fn nulls() { + assert_parse_eq("null", Atom(Null)); + } + + #[test] + fn structure() { + println!("{:+?}", parse("{1 + a: 3}")); + } + + #[test] + fn simple_str() { + assert_parse_eq(r#"'foobar'"#, Atom(String("foobar".to_string().into()))); + println!("{:?}", parse(r#"1 == '1'"#)) + } + + #[test] + fn test_parse_map_macro() { + assert_parse_eq( + "[1, 2, 3].map(x, x * 2)", + FunctionCall( + Box::new(Ident("map".to_string().into())), + Some(Box::new(List(vec![ + Atom(Int(1)), + Atom(Int(2)), + Atom(Int(3)), + ]))), + vec![ + Ident("x".to_string().into()), + Arithmetic( + Box::new(Ident("x".to_string().into())), + ArithmeticOp::Multiply, + Box::new(Atom(Int(2))), + ), + ], + ), + ) + } + + #[test] + fn nested_attributes() { + assert_parse_eq( + "a.b[1]", + Member( + Member( + Ident("a".to_string().into()).into(), + Attribute("b".to_string().into()).into(), + ) + .into(), + Index(Atom(Int(1)).into()).into(), + ), + ) + } + + #[test] + fn function_call_no_args() { + assert_parse_eq( + "a()", + FunctionCall(Box::new(Ident("a".to_string().into())), None, vec![]), + ); + } + + #[test] + fn test_parser_bool_unary_ops() { + assert_parse_eq( + "!false", + Unary(UnaryOp::Not, Box::new(Expression::Atom(Atom::Bool(false)))), + ); + assert_parse_eq( + "!true", + Unary(UnaryOp::Not, Box::new(Expression::Atom(Atom::Bool(true)))), + ); + } + + #[test] + fn test_parser_binary_bool_expressions() { + assert_parse_eq( + "true == true", + Relation( + Box::new(Expression::Atom(Atom::Bool(true))), + RelationOp::Equals, + Box::new(Expression::Atom(Atom::Bool(true))), + ), + ); + } + + #[test] + fn test_parser_bool_unary_ops_repeated() { + assert_eq!( + parse("!!true"), + (Unary( + UnaryOp::DoubleNot, + Box::new(Expression::Atom(Atom::Bool(true))), + )) + ); + } + + #[test] + fn delimited_expressions() { + assert_parse_eq( + "(-((1)))", + Unary(UnaryOp::Minus, Box::new(Expression::Atom(Atom::Int(1)))), + ); + } + + #[test] + fn test_empty_list_parsing() { + assert_eq!(parse("[]"), (List(vec![]))); + } + + #[test] + fn test_int_list_parsing() { + assert_parse_eq( + "[1,2,3]", + List(vec![ + Expression::Atom(Atom::Int(1)), + Expression::Atom(Atom::Int(2)), + Expression::Atom(Atom::Int(3)), + ]), + ); + } + + #[test] + fn list_index_parsing() { + assert_parse_eq( + "[1,2,3][0]", + Member( + Box::new(List(vec![ + Expression::Atom(Int(1)), + Expression::Atom(Int(2)), + Expression::Atom(Int(3)), + ])), + Box::new(Index(Box::new(Expression::Atom(Int(0))))), + ), + ); + } + + #[test] + fn mixed_type_list() { + assert_parse_eq( + "['0', 1, 3.0, null]", + //"['0', 1, 2u, 3.0, null]", + List(vec![ + Expression::Atom(String("0".to_string().into())), + Expression::Atom(Int(1)), + //Expression::Atom(UInt(2)), + Expression::Atom(Float(3.0)), + Expression::Atom(Null), + ]), + ); + } + + #[test] + fn test_nested_list_parsing() { + assert_parse_eq( + "[[], [], [[1]]]", + List(vec![ + List(vec![]), + List(vec![]), + List(vec![List(vec![Expression::Atom(Int(1))])]), + ]), + ); + } + + #[test] + fn test_in_list_relation() { + assert_parse_eq( + "2 in [2]", + Relation( + Box::new(Expression::Atom(Int(2))), + RelationOp::In, + Box::new(List(vec![Expression::Atom(Int(2))])), + ), + ); + } + + #[test] + fn test_empty_map_parsing() { + assert_eq!(parse("{}"), (Map(vec![]))); + } + + #[test] + fn test_nonempty_map_parsing() { + assert_parse_eq( + "{'a': 1, 'b': 2}", + Map(vec![ + ( + Expression::Atom(String("a".to_string().into())), + Expression::Atom(Int(1)), + ), + ( + Expression::Atom(String("b".to_string().into())), + Expression::Atom(Int(2)), + ), + ]), + ); + } + + #[test] + fn nonempty_map_index_parsing() { + assert_parse_eq( + "{'a': 1, 'b': 2}[0]", + Member( + Box::new(Map(vec![ + ( + Expression::Atom(String("a".to_string().into())), + Expression::Atom(Int(1)), + ), + ( + Expression::Atom(String("b".to_string().into())), + Expression::Atom(Int(2)), + ), + ])), + Box::new(Index(Box::new(Expression::Atom(Int(0))))), + ), + ); + } + + #[test] + fn integer_relations() { + assert_parse_eq( + "2 != 3", + Relation( + Box::new(Expression::Atom(Int(2))), + RelationOp::NotEquals, + Box::new(Expression::Atom(Int(3))), + ), + ); + assert_parse_eq( + "2 == 3", + Relation( + Box::new(Expression::Atom(Int(2))), + RelationOp::Equals, + Box::new(Expression::Atom(Int(3))), + ), + ); + + assert_parse_eq( + "2 < 3", + Relation( + Box::new(Expression::Atom(Int(2))), + RelationOp::LessThan, + Box::new(Expression::Atom(Int(3))), + ), + ); + + assert_parse_eq( + "2 <= 3", + Relation( + Box::new(Expression::Atom(Int(2))), + RelationOp::LessThanEq, + Box::new(Expression::Atom(Int(3))), + ), + ); + } + + #[test] + fn binary_product_expressions() { + assert_parse_eq( + "2 * 3", + Arithmetic( + Box::new(Expression::Atom(Atom::Int(2))), + ArithmeticOp::Multiply, + Box::new(Expression::Atom(Atom::Int(3))), + ), + ); + } + + // #[test] + // fn binary_product_negated_expressions() { + // assert_parse_eq( + // "2 * -3", + // Arithmetic( + // Box::new(Expression::Atom(Atom::Int(2))), + // ArithmeticOp::Multiply, + // Box::new(Unary( + // UnaryOp::Minus, + // Box::new(Expression::Atom(Atom::Int(3))), + // )), + // ), + // ); + // + // assert_parse_eq( + // "2 / -3", + // Arithmetic( + // Box::new(Expression::Atom(Int(2))), + // ArithmeticOp::Divide, + // Box::new(Unary( + // UnaryOp::Minus, + // Box::new(Expression::Atom(Int(3))), + // )), + // ), + // ); + // } + + #[test] + fn test_parser_sum_expressions() { + assert_parse_eq( + "2 + 3", + Arithmetic( + Box::new(Expression::Atom(Atom::Int(2))), + ArithmeticOp::Add, + Box::new(Expression::Atom(Atom::Int(3))), + ), + ); + + // assert_parse_eq( + // "2 - -3", + // Arithmetic( + // Box::new(Expression::Atom(Atom::Int(2))), + // ArithmeticOp::Subtract, + // Box::new(Unary( + // UnaryOp::Minus, + // Box::new(Expression::Atom(Atom::Int(3))), + // )), + // ), + // ); + } + + #[test] + fn conditionals() { + assert_parse_eq( + "true && true", + And( + Box::new(Expression::Atom(Bool(true))), + Box::new(Expression::Atom(Bool(true))), + ), + ); + assert_parse_eq( + "false || true", + Or( + Box::new(Expression::Atom(Bool(false))), + Box::new(Expression::Atom(Bool(true))), + ), + ); + } + #[test] + fn test_ternary_true_condition() { + assert_parse_eq( + "true ? 'result_true' : 'result_false'", + Ternary( + Box::new(Expression::Atom(Bool(true))), + Box::new(Expression::Atom(String("result_true".to_string().into()))), + Box::new(Expression::Atom(String("result_false".to_string().into()))), + ), + ); + + assert_parse_eq( + "true ? 100 : 200", + Ternary( + Box::new(Expression::Atom(Bool(true))), + Box::new(Expression::Atom(Int(100))), + Box::new(Expression::Atom(Int(200))), + ), + ); + } + + #[test] + fn test_ternary_false_condition() { + assert_parse_eq( + "false ? 'result_true' : 'result_false'", + Ternary( + Box::new(Expression::Atom(Bool(false))), + Box::new(Expression::Atom(String("result_true".to_string().into()))), + Box::new(Expression::Atom(String("result_false".to_string().into()))), + ), + ); + } + + #[test] + fn test_operator_precedence() { + assert_parse_eq( + "a && b == 'string'", + And( + Box::new(Ident("a".to_string().into())), + Box::new(Relation( + Box::new(Ident("b".to_string().into())), + RelationOp::Equals, + Box::new(Expression::Atom(String("string".to_string().into()))), + )), + ), + ); + } + + #[test] + fn test_foobar() { + println!("{:?}", parse("foo.bar.baz == 10 && size(requests) == 3")) + } + + #[test] + fn test_unrecognized_token_error() { + let source = r#" + account.balance == transaction.withdrawal + || (account.overdraftProtection + account.overdraftLimit >= transaction.withdrawal - account.balance) + "#; + + let err = crate::parse(source).unwrap_err(); + + assert_eq!(err.msg, "unrecognized token: 'account'"); + + assert_eq!(err.span.start.as_ref().unwrap().line, 3); + assert_eq!(err.span.start.as_ref().unwrap().column, 20); + assert_eq!(err.span.end.as_ref().unwrap().line, 3); + assert_eq!(err.span.end.as_ref().unwrap().column, 27); + } + + #[test] + fn test_unrecognized_eof_error() { + let source = r#" "#; + + let err = crate::parse(source).unwrap_err(); + + assert_eq!(err.msg, "unrecognized eof"); + + assert_eq!(err.span.start.as_ref().unwrap().line, 0); + assert_eq!(err.span.start.as_ref().unwrap().column, 0); + assert_eq!(err.span.end.as_ref().unwrap().line, 0); + assert_eq!(err.span.end.as_ref().unwrap().column, 0); + } + + #[test] + fn test_invalid_token_error() { + let source = r#" + account.balance == ยง + "#; + + let err = crate::parse(source).unwrap_err(); + + assert_eq!(err.msg, "invalid token"); + + assert_eq!(err.span.start.as_ref().unwrap().line, 1); + assert_eq!(err.span.start.as_ref().unwrap().column, 31); + assert_eq!(err.span.end.as_ref().unwrap().line, 1); + assert_eq!(err.span.end.as_ref().unwrap().column, 31); + } +} diff --git a/libs/core/cel-rust/parser/src/parse.rs b/libs/core/cel-rust/parser/src/parse.rs new file mode 100644 index 0000000..15aef5e --- /dev/null +++ b/libs/core/cel-rust/parser/src/parse.rs @@ -0,0 +1,537 @@ +use std::iter::Enumerate; +use std::num::ParseIntError; +use std::str::Chars; + +/// Error type of [unescape](unescape). +#[derive(Debug, PartialEq)] +pub enum ParseSequenceError { + InvalidSymbol { + symbol: String, + index: usize, + string: String, + }, + // #[error("invalid escape {escape} at {index} in {string}")] + InvalidEscape { + escape: String, + index: usize, + string: String, + }, + // #[error("\\u could not be parsed at {index} in {string}: {source}")] + InvalidUnicode { + // #[source] + source: ParseUnicodeError, + index: usize, + string: String, + }, + MissingOpeningQuote, + MissingClosingQuote, +} + +/// Source error type of [ParseError::InvalidUnicode](ParseError::InvalidUnicode). +#[derive(Debug, PartialEq, Clone)] +pub enum ParseUnicodeError { + // #[error("could not parse {string} as u32 hex: {source}")] + ParseHexFailed { + // #[source] + source: ParseIntError, + string: String, + }, + ParseOctFailed { + // #[source] + source: ParseIntError, + string: String, + }, + // #[error("could not parse {value} as a unicode char")] + ParseUnicodeFailed { + value: u32, + }, +} + +pub fn parse_bytes(s: &str) -> Result, ParseSequenceError> { + let mut chars = s.chars().enumerate(); + let mut res: Vec = Vec::with_capacity(s.len()); + + while let Some((idx, c)) = chars.next() { + if c == '\\' { + match chars.next() { + None => { + return Err(ParseSequenceError::InvalidEscape { + escape: format!("{}", c), + index: idx, + string: String::from(s), + }); + } + Some((idx, c2)) => { + let byte: u8 = match c2 { + 'x' => { + let hex: String = [ + chars + .next() + .ok_or(ParseSequenceError::InvalidEscape { + escape: "\\x".to_string(), + index: idx, + string: s.to_string(), + })? + .1, + chars + .next() + .ok_or(ParseSequenceError::InvalidEscape { + escape: "\\x".to_string(), + index: idx, + string: s.to_string(), + })? + .1, + ] + .iter() + .collect(); + u8::from_str_radix(&hex, 16).map_err(|_| { + ParseSequenceError::InvalidEscape { + escape: hex, + index: idx, + string: s.to_string(), + } + })? + } + n if ('0'..='3').contains(&n) => { + let octal: String = [ + n, + chars + .next() + .ok_or(ParseSequenceError::InvalidEscape { + escape: format!("\\{n}"), + index: idx, + string: s.to_string(), + })? + .1, + chars + .next() + .ok_or(ParseSequenceError::InvalidEscape { + escape: format!("\\{n}"), + index: idx, + string: s.to_string(), + })? + .1, + ] + .iter() + .collect(); + u8::from_str_radix(&octal, 8).map_err(|_| { + ParseSequenceError::InvalidEscape { + escape: octal, + index: idx, + string: s.to_string(), + } + })? + } + _ => { + return Err(ParseSequenceError::InvalidEscape { + escape: format!("{}{}", c, c2), + index: idx, + string: String::from(s), + }); + } + }; + + res.push(byte); + continue; + } + }; + } + let size = c.len_utf8(); + let mut buffer = [0; 4]; + c.encode_utf8(&mut buffer); + res.extend_from_slice(&buffer[..size]); + } + Ok(res) +} + +/// Parse the provided quoted string. +/// This function was adopted from [snailquote](https://docs.rs/snailquote/latest/snailquote/). +/// +/// # Details +/// +/// Parses a single or double quoted string and interprets escape sequences such as +/// '\n', '\r', '\'', etc. +/// +/// Supports raw strings prefixed with `r` or `R` in which case all escape sequences are ignored./// +/// +/// The full set of supported escapes between quotes may be found below: +/// +/// | Escape | Code | Description | +/// |------------|------------|------------------------------------------| +/// | \a | 0x07 | Bell | +/// | \b | 0x08 | Backspace | +/// | \v | 0x0B | Vertical tab | +/// | \f | 0x0C | Form feed | +/// | \n | 0x0A | Newline | +/// | \r | 0x0D | Carriage return | +/// | \t | 0x09 | Tab | +/// | \\ | 0x5C | Backslash | +/// | \? | 0x?? | Question mark | +/// | \" | 0x22 | Double quote | +/// | \' | 0x27 | Single quote | +/// | \` | 0x60 | Backtick | +/// | \xDD | 0xDD | Unicode character with hex code DD | +/// | \uDDDD | 0xDDDD | Unicode character with hex code DDDD | +/// | \UDDDDDDDD | 0xDDDDDDDD | Unicode character with hex code DDDDDDDD | +/// | \DDD | 0DDD | Unicode character with octal code DDD | +/// +/// # Errors +/// +/// The returned result can display a human readable error if the string cannot be parsed as a +/// valid quoted string. +pub fn parse_string(s: &str) -> Result { + let mut chars = s.chars().enumerate(); + let res = String::with_capacity(s.len()); + + match chars.next() { + Some((_, c)) if c == 'r' || c == 'R' => parse_raw_string(&mut chars, res), + Some((_, c)) if c == '\'' || c == '"' => parse_quoted_string(s, &mut chars, res, c), + _ => Err(ParseSequenceError::MissingOpeningQuote), + } +} + +fn parse_raw_string( + chars: &mut Enumerate, + mut res: String, +) -> Result { + let mut in_single_quotes = false; + let mut in_double_quotes = false; + + while let Some((_, c)) = chars.next() { + let in_quotes = in_single_quotes || in_double_quotes; + + if c == '\\' && in_quotes { + match chars.next() { + Some((_, c2)) => { + match c2 { + '"' => { + if in_single_quotes { + res.push(c); + } + } + '\'' => { + if in_double_quotes { + res.push(c); + } + } + _ => { + res.push(c); + } + }; + res.push(c2); + continue; + } + _ => { + res.push(c); + continue; + } + }; + } else if c == '\'' { + if in_double_quotes { + res.push(c); + continue; + } + + in_single_quotes = !in_single_quotes; + continue; + } else if c == '"' { + if in_single_quotes { + res.push(c); + continue; + } + + in_double_quotes = !in_double_quotes; + continue; + } else if !in_quotes { + return Err(ParseSequenceError::MissingOpeningQuote); + } + + res.push(c); + } + + Ok(res) +} + +fn parse_quoted_string( + s: &str, + mut chars: &mut Enumerate, + mut res: String, + quote: char, +) -> Result { + let mut in_single_quotes = quote == '\''; + let mut in_double_quotes = quote == '"'; + + while let Some((idx, c)) = chars.next() { + let in_quotes = in_single_quotes || in_double_quotes; + + if c == '\\' && in_quotes { + match chars.next() { + None => { + return Err(ParseSequenceError::InvalidEscape { + escape: format!("{}", c), + index: idx, + string: String::from(s), + }); + } + Some((idx, c2)) => { + let mut push_escape_character = false; + + let value = match c2 { + 'a' => '\u{07}', + 'b' => '\u{08}', + 'v' => '\u{0B}', + 'f' => '\u{0C}', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + '\\' => c2, + '?' => c2, + '\'' => { + push_escape_character = in_double_quotes; + c2 + } + '"' => { + push_escape_character = in_single_quotes; + c2 + } + '`' => c2, + 'x' | 'u' | 'U' => { + let length = match c2 { + 'x' => 2, + 'u' => 4, + 'U' => 8, + _ => unreachable!(), + }; + + parse_unicode_hex(length, &mut chars).map_err(|x| { + ParseSequenceError::InvalidUnicode { + source: x.clone(), + index: idx, + string: String::from(s), + } + })? + } + n if ('0'..='3').contains(&n) => parse_unicode_oct(&n, &mut chars) + .map_err(|x| ParseSequenceError::InvalidUnicode { + source: x.clone(), + index: idx, + string: String::from(s), + })?, + _ => { + return Err(ParseSequenceError::InvalidEscape { + escape: format!("{}{}", c, c2), + index: idx, + string: String::from(s), + }); + } + }; + + if push_escape_character { + res.push(c); + } + + res.push(value); + + continue; + } + }; + } else if c == '\'' { + if in_double_quotes { + res.push(c); + continue; + } + + in_single_quotes = !in_single_quotes; + continue; + } else if c == '"' { + if in_single_quotes { + res.push(c); + continue; + } + + in_double_quotes = !in_double_quotes; + continue; + } else if !in_quotes { + return Err(ParseSequenceError::MissingOpeningQuote); + } + + res.push(c); + } + + // Ensure string has a closing quote + if in_single_quotes || in_double_quotes { + return Err(ParseSequenceError::MissingClosingQuote); + } + + Ok(res) +} + +fn parse_unicode_hex(length: usize, chars: &mut I) -> Result +where + I: Iterator, +{ + let unicode_seq: String = chars.take(length).map(|(_, c)| c).collect(); + + u32::from_str_radix(&unicode_seq, 16) + .map_err(|e| ParseUnicodeError::ParseHexFailed { + source: e, + string: unicode_seq, + }) + .and_then(|u| char::from_u32(u).ok_or(ParseUnicodeError::ParseUnicodeFailed { value: u })) +} + +fn parse_unicode_oct(first_char: &char, chars: &mut I) -> Result +where + I: Iterator, +{ + let mut unicode_seq: String = String::with_capacity(3); + unicode_seq.push(*first_char); + chars.take(2).for_each(|(_, c)| unicode_seq.push(c)); + + u32::from_str_radix(&unicode_seq, 8) + .map_err(|e| ParseUnicodeError::ParseOctFailed { + source: e, + string: unicode_seq, + }) + .and_then(|u| { + if u <= 255 { + char::from_u32(u).ok_or(ParseUnicodeError::ParseUnicodeFailed { value: u }) + } else { + Err(ParseUnicodeError::ParseUnicodeFailed { value: u }) + } + }) +} + +#[cfg(test)] +mod tests { + use crate::parse::ParseSequenceError; + use crate::{parse_bytes, parse_string}; + + #[test] + fn single_quotes_interprets_escapes() { + let tests: Vec<(&str, Result)> = vec![ + ("'Hello \\a'", Ok(String::from("Hello \u{07}"))), + ("'Hello \\b'", Ok(String::from("Hello \u{08}"))), + ("'Hello \\v'", Ok(String::from("Hello \u{0b}"))), + ("'Hello \\f'", Ok(String::from("Hello \u{0c}"))), + ("'Hello \\n'", Ok(String::from("Hello \u{0a}"))), + ("'Hello \\r'", Ok(String::from("Hello \u{0d}"))), + ("'Hello \\t'", Ok(String::from("Hello \u{09}"))), + ("'Hello \\\\'", Ok(String::from("Hello \\"))), + ("'Hello \\?'", Ok(String::from("Hello ?"))), + ("'Hello \"'", Ok(String::from("Hello \""))), + ("'Hello \\''", Ok(String::from("Hello '"))), + ("'Hello \\`'", Ok(String::from("Hello `"))), + ("'Hello \\x20'", Ok(String::from("Hello "))), + ("'Hello \\u270c'", Ok(String::from("Hello โœŒ"))), + ("'Hello \\U0001f431'", Ok(String::from("Hello ๐Ÿฑ"))), + ("'Hello \\040'", Ok(String::from("Hello "))), + ( + "Missing closing quote'", + Err(ParseSequenceError::MissingOpeningQuote), + ), + ( + "'Missing closing quote", + Err(ParseSequenceError::MissingClosingQuote), + ), + // Testing octal value is out of range + ( + "'\\440'", + Err(ParseSequenceError::InvalidEscape { + escape: String::from("\\4"), + index: 2, + string: String::from("'\\440'"), + }), + ), + ]; + + for (s, expected) in tests { + let result = parse_string(s); + assert_eq!(result, expected); + } + } + + #[test] + fn double_quotes_interprets_escapes() { + let tests: Vec<(&str, Result)> = vec![ + ("\"Hello \\a\"", Ok(String::from("Hello \u{07}"))), + ("\"Hello \\b\"", Ok(String::from("Hello \u{08}"))), + ("\"Hello \\v\"", Ok(String::from("Hello \u{0b}"))), + ("\"Hello \\f\"", Ok(String::from("Hello \u{0c}"))), + ("\"Hello \\n\"", Ok(String::from("Hello \u{0a}"))), + ("\"Hello \\r\"", Ok(String::from("Hello \u{0d}"))), + ("\"Hello \\t\"", Ok(String::from("Hello \u{09}"))), + ("\"Hello \\\\\"", Ok(String::from("Hello \\"))), + ("\"Hello \\?\"", Ok(String::from("Hello ?"))), + ("\"Hello \\\"\"", Ok(String::from("Hello \""))), + ("\"Hello \\'\"", Ok(String::from("Hello \\'"))), + ("\"Hello \\`\"", Ok(String::from("Hello `"))), + ("\"Hello \\x20 \"", Ok(String::from("Hello "))), + ("\"Hello \\x60\"", Ok(String::from("Hello `"))), + ("\"Hello \\u270c\"", Ok(String::from("Hello โœŒ"))), + ("\"Hello \\U0001f431\"", Ok(String::from("Hello ๐Ÿฑ"))), + ("\"Hello \\040\"", Ok(String::from("Hello "))), + ( + "Missing closing quote\"", + Err(ParseSequenceError::MissingOpeningQuote), + ), + ( + "\"Missing closing quote", + Err(ParseSequenceError::MissingClosingQuote), + ), + // Testing octal value is out of range + ( + "\"\\440\"", + Err(ParseSequenceError::InvalidEscape { + escape: String::from("\\4"), + index: 2, + string: String::from("\"\\440\""), + }), + ), + ]; + + for (s, expected) in tests { + let result = parse_string(s); + assert_eq!(result, expected, "Testing {}", s); + } + } + + #[test] + fn raw_string_does_not_interpret_escapes() { + let tests: Vec<(&str, Result)> = vec![ + // Raw string in double quotes + // r"Hello \a \" ' \' \U0001f431 " => Hello \a " ' \' \U0001f431 + // R"Hello \a \" ' \' \U0001f431 " => Hello \a " ' \' \U0001f431 + ( + "r\"Hello \\a \\\" ' \\' \\U0001f431 \"", + Ok(String::from("Hello \\a \" ' \\' \\U0001f431 ")), + ), + ( + "R\"Hello \\a \\\" ' \\' \\U0001f431 \"", + Ok(String::from("Hello \\a \" ' \\' \\U0001f431 ")), + ), + // Raw string in single quotes + // r'Hello \a \" " \' \U0001f431 ' => Hello \a \" " ' \U0001f431 + // R'Hello \a \" " \' \U0001f431 ' => Hello \a \" " ' \U0001f431 + ( + "r'Hello \\a \\\" \" \\' \\U0001f431 '", + Ok(String::from("Hello \\a \\\" \" ' \\U0001f431 ")), + ), + ( + "R'Hello \\a \\\" \" \\' \\U0001f431 '", + Ok(String::from("Hello \\a \\\" \" ' \\U0001f431 ")), + ), + ]; + + for (s, expected) in tests { + let result = parse_string(s); + assert_eq!(result, expected, "Testing {}", s); + } + } + + #[test] + fn parses_bytes() { + let bytes = parse_bytes("abc๐Ÿ’–\\xFF\\376").expect("Must parse!"); + assert_eq!([97, 98, 99, 240, 159, 146, 150, 255, 254], *bytes) + } +} diff --git a/libs/core/cel-rust/release.toml b/libs/core/cel-rust/release.toml new file mode 100644 index 0000000..e8c5457 --- /dev/null +++ b/libs/core/cel-rust/release.toml @@ -0,0 +1,2 @@ +consolidate-commits = true +#no-dev-version = true \ No newline at end of file diff --git a/libs/core/package.json b/libs/core/package.json new file mode 100644 index 0000000..d3281fd --- /dev/null +++ b/libs/core/package.json @@ -0,0 +1,18 @@ +{ + "name": "@kevinmichaelchen/cel-typescript-core", + "version": "0.0.0", + "type": "module", + "description": "TypeScript bindings for the Common Expression Language (CEL) using cel-rust", + "files": ["dist/src/**/*"], + "types": "./dist/src/index.d.ts", + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + } + }, + "license": "MIT", + "engines": { + "node": ">= 10" + } +} diff --git a/src/index.ts b/libs/core/src/index.ts similarity index 91% rename from src/index.ts rename to libs/core/src/index.ts index baf68e4..00ea2cb 100644 --- a/src/index.ts +++ b/libs/core/src/index.ts @@ -1,3 +1,5 @@ +import { createRequire } from "node:module"; + export interface CelContext { [key: string]: any; } @@ -33,11 +35,11 @@ class CelProgram { static async compile(source: string): Promise { if (!CelProgram.nativeModule) { // Use the NAPI-RS generated loader which handles platform detection - const nativeBinding = await import( - "@kevinmichaelchen/cel-typescript/native" - ); + const require = createRequire(import.meta.url); + const nativeBinding = require("./native.cjs"); + CelProgram.nativeModule = nativeBinding.CelProgram; - console.log("Imported native CelProgram:", CelProgram.nativeModule); + console.log("Required native CelProgram:", CelProgram.nativeModule); } const native = await CelProgram.nativeModule.compile(source); return new CelProgram(native); diff --git a/src/lib.rs b/libs/core/src/lib.rs similarity index 100% rename from src/lib.rs rename to libs/core/src/lib.rs diff --git a/src/native.cjs b/libs/core/src/native.cjs similarity index 100% rename from src/native.cjs rename to libs/core/src/native.cjs diff --git a/src/native.d.ts b/libs/core/src/native.d.ts similarity index 100% rename from src/native.d.ts rename to libs/core/src/native.d.ts diff --git a/package.json b/package.json index 4e41dd6..69b7f88 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,8 @@ "name": "@kevinmichaelchen/cel-typescript", "version": "0.0.11", "type": "module", + "private": true, "description": "TypeScript bindings for the Common Expression Language (CEL) using cel-rust", - "files": ["dist/src/**/*"], - "types": "./dist/src/index.d.ts", - "exports": { - ".": { - "import": "./dist/src/index.js", - "types": "./dist/src/index.d.ts" - } - }, "napi": { "name": "@kevinmichaelchen/cel-typescript", "triples": { diff --git a/scripts/build.sh b/scripts/build.sh index f57b076..344665f 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -11,10 +11,10 @@ if [ "$USE_ZIG" = "true" ]; then --target "$1" \ # The filename and path of the JavaScript binding file - --js src/native.cjs \ + --js libs/core/src/native.cjs \ # TypeScript declaration file - --dts src/native.d.ts \ + --dts libs/core/src/native.d.ts \ # Bypass to cargo build --release --release \ @@ -26,8 +26,8 @@ else npx @napi-rs/cli build \ --platform \ --target "$1" \ - --js src/native.cjs \ - --dts src/native.d.ts \ + --js libs/core/src/native.cjs \ + --dts libs/core/src/native.d.ts \ --release fi From 48afd14a0623589659c94ab2341de85a3b669195 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 29 Apr 2025 13:27:15 -0400 Subject: [PATCH 7/7] chore: generate .node files from libs/core --- .gitignore | 2 +- biome.json | 8 ++++- libs/core/package.json | 36 ++++++++++++++++++++-- libs/core/project.json | 63 ++++++++++++++++++++++++++++++++++++++ libs/core/scripts/build.sh | 51 ++++++++++++++++++++++++++++++ libs/core/src/native.cjs | 36 +++++++++++----------- nx.json | 9 ------ package.json | 15 +-------- 8 files changed, 174 insertions(+), 46 deletions(-) create mode 100644 libs/core/project.json create mode 100755 libs/core/scripts/build.sh diff --git a/.gitignore b/.gitignore index ae786db..5118cbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Rust -/target +**/target **/*.rs.bk Cargo.lock diff --git a/biome.json b/biome.json index b632370..29d634a 100644 --- a/biome.json +++ b/biome.json @@ -5,7 +5,13 @@ }, "files": { "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"], - "ignore": [".nx", "dist/**/*", "src/native.cjs", "src/native.d.ts"] + "ignore": [ + ".nx", + "dist/**/*", + "src/native.cjs", + "src/native.d.ts", + "libs/core/target/**/*" + ] }, "formatter": { "enabled": true, diff --git a/libs/core/package.json b/libs/core/package.json index d3281fd..6236157 100644 --- a/libs/core/package.json +++ b/libs/core/package.json @@ -11,8 +11,38 @@ "types": "./dist/src/index.d.ts" } }, - "license": "MIT", + "napi": { + "name": "@kevinmichaelchen/cel-typescript", + "triples": { + "defaults": false, + "additional": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-msvc" + ] + } + }, "engines": { - "node": ">= 10" - } + "node": ">=18.0.0" + }, + "author": "Kevin Chen", + "repository": { + "type": "git", + "url": "git+https://github.com/kevinmichaelchen/cel-typescript.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/kevinmichaelchen/cel-typescript/issues" + }, + "homepage": "https://github.com/kevinmichaelchen/cel-typescript#readme", + "keywords": [ + "cel", + "common-expression-language", + "expression-language", + "policy", + "rust", + "napi-rs" + ] } diff --git a/libs/core/project.json b/libs/core/project.json new file mode 100644 index 0000000..279cef2 --- /dev/null +++ b/libs/core/project.json @@ -0,0 +1,63 @@ +{ + "name": "core", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/core/src", + "projectType": "library", + "targets": { + "build:native": { + "executor": "nx:run-commands", + "options": { + "command": "bash libs/core/scripts/build.sh" + }, + "cache": false, + "inputs": [ + "{projectRoot}/src/**/*", + "{projectRoot}/Cargo.toml", + "{projectRoot}/Cargo.lock", + "{projectRoot}/package.json" + ], + "outputs": ["{workspaceRoot}/cel-typescript.node"] + }, + "build:ts": { + "executor": "@nx/js:tsc", + "outputs": ["{workspaceRoot}/dist/{projectRoot}"], + "options": { + "outputPath": "dist/libs/core", + "main": "libs/core/src/index.ts", + "tsConfig": "libs/core/tsconfig.json", + "assets": ["libs/core/*.md"] + }, + "cache": true, + "inputs": [ + "{projectRoot}/src/**/*.ts", + "{projectRoot}/tsconfig.json", + "{workspaceRoot}/package.json" + ] + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/core/**/*.ts"] + } + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["coverage/libs/core"], + "options": { + "passWithNoTests": true, + "reportsDirectory": "../../coverage/libs/core" + }, + "cache": true, + "inputs": [ + "{projectRoot}/__tests__/**/*", + "{projectRoot}/src/**/*", + "{projectRoot}/vitest.config.ts", + "{projectRoot}/Cargo.toml", + "{projectRoot}/build.rs", + "{workspaceRoot}/package.json" + ] + } + }, + "tags": [] +} diff --git a/libs/core/scripts/build.sh b/libs/core/scripts/build.sh new file mode 100755 index 0000000..2b1993b --- /dev/null +++ b/libs/core/scripts/build.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Exit on error +set -e + +# See https://napi.rs/docs/cli/build for details + +# Debug information +echo "Script location: $0" +echo "Current directory: $(pwd)" +echo "Arguments: $@" + +# Change to the core directory where Cargo.toml is located +cd "$(dirname "$0")/.." || exit 1 +echo "Changed to directory: $(pwd)" + +# Set default target if not provided +TARGET=${1:-""} + +# Build command with proper error handling +build_native() { + # Use a temporary file to capture any error output + local temp_err=$(mktemp) + + if [ "$USE_ZIG" = "true" ]; then + npx @napi-rs/cli build \ + --platform \ + --target "$TARGET" \ + --js src/native.cjs \ + --dts src/native.d.ts \ + --release \ + --zig 2>"$temp_err" || { cat "$temp_err"; rm "$temp_err"; exit 1; } + else + npx @napi-rs/cli build \ + --platform \ + --target "$TARGET" \ + --js src/native.cjs \ + --dts src/native.d.ts \ + --release 2>"$temp_err" || { cat "$temp_err"; rm "$temp_err"; exit 1; } + fi + + rm "$temp_err" + return 0 +} + +# Execute the build function +build_native + +# If we get here, the build was successful +echo "Build completed successfully" +exit 0 diff --git a/libs/core/src/native.cjs b/libs/core/src/native.cjs index 76e6c6a..4c0323e 100644 --- a/libs/core/src/native.cjs +++ b/libs/core/src/native.cjs @@ -37,7 +37,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.android-arm64.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-android-arm64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-android-arm64') } } catch (e) { loadError = e @@ -49,7 +49,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.android-arm-eabi.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-android-arm-eabi') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-android-arm-eabi') } } catch (e) { loadError = e @@ -69,7 +69,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.win32-x64-msvc.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-win32-x64-msvc') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-win32-x64-msvc') } } catch (e) { loadError = e @@ -83,7 +83,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.win32-ia32-msvc.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-win32-ia32-msvc') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-win32-ia32-msvc') } } catch (e) { loadError = e @@ -97,7 +97,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.win32-arm64-msvc.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-win32-arm64-msvc') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-win32-arm64-msvc') } } catch (e) { loadError = e @@ -113,7 +113,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.darwin-universal.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-darwin-universal') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-darwin-universal') } break } catch {} @@ -124,7 +124,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.darwin-x64.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-darwin-x64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-darwin-x64') } } catch (e) { loadError = e @@ -138,7 +138,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.darwin-arm64.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-darwin-arm64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-darwin-arm64') } } catch (e) { loadError = e @@ -157,7 +157,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.freebsd-x64.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-freebsd-x64') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-freebsd-x64') } } catch (e) { loadError = e @@ -174,7 +174,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-x64-musl.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-x64-musl') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-x64-musl') } } catch (e) { loadError = e @@ -187,7 +187,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-x64-gnu.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-x64-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-x64-gnu') } } catch (e) { loadError = e @@ -203,7 +203,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm64-musl.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm64-musl') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-arm64-musl') } } catch (e) { loadError = e @@ -216,7 +216,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm64-gnu.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm64-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-arm64-gnu') } } catch (e) { loadError = e @@ -232,7 +232,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm-musleabihf.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm-musleabihf') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-arm-musleabihf') } } catch (e) { loadError = e @@ -245,7 +245,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-arm-gnueabihf.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-arm-gnueabihf') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-arm-gnueabihf') } } catch (e) { loadError = e @@ -261,7 +261,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-riscv64-musl.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-riscv64-musl') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-riscv64-musl') } } catch (e) { loadError = e @@ -274,7 +274,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-riscv64-gnu.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-riscv64-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-riscv64-gnu') } } catch (e) { loadError = e @@ -289,7 +289,7 @@ switch (platform) { if (localFileExisted) { nativeBinding = require('./@kevinmichaelchen/cel-typescript.linux-s390x-gnu.node') } else { - nativeBinding = require('@kevinmichaelchen/cel-typescript-linux-s390x-gnu') + nativeBinding = require('@kevinmichaelchen/cel-typescript-core-linux-s390x-gnu') } } catch (e) { loadError = e diff --git a/nx.json b/nx.json index 5ad414c..97a68db 100644 --- a/nx.json +++ b/nx.json @@ -2,15 +2,6 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "defaultBase": "main", "targetDefaults": { - "build:native": { - "cache": true, - "inputs": [ - "{projectRoot}/src/**/*", - "{projectRoot}/Cargo.toml", - "{projectRoot}/Cargo.lock" - ], - "outputs": ["{workspaceRoot}/cel-typescript.*.node"] - }, "build:ts": { "cache": true, "inputs": [ diff --git a/package.json b/package.json index 69b7f88..d8fc145 100644 --- a/package.json +++ b/package.json @@ -4,21 +4,8 @@ "type": "module", "private": true, "description": "TypeScript bindings for the Common Expression Language (CEL) using cel-rust", - "napi": { - "name": "@kevinmichaelchen/cel-typescript", - "triples": { - "defaults": false, - "additional": [ - "aarch64-apple-darwin", - "x86_64-apple-darwin", - "x86_64-unknown-linux-gnu", - "aarch64-unknown-linux-gnu", - "x86_64-pc-windows-msvc" - ] - } - }, "scripts": { - "build:native": "nx build:native", + "build:native": "nx run core:build:native", "build:ts": "nx build:ts", "format": "nx run cel-typescript:format", "format:check": "nx run cel-typescript:format:check",