@marcbachmann/cel-js
Advanced tools
+8
-4
@@ -50,4 +50,4 @@ import {createRegistry, RootContext} from './registry.js' | ||
| registerFunction(string, handler) { | ||
| this.#registry.registerFunctionOverload(string, handler) | ||
| registerFunction(signature, handler, opts) { | ||
| this.#registry.registerFunctionOverload(signature, handler, opts) | ||
| return this | ||
@@ -66,4 +66,4 @@ } | ||
| registerVariable(name, type) { | ||
| this.#registry.registerVariable(name, type) | ||
| registerVariable(name, type, opts) { | ||
| this.#registry.registerVariable(name, type, opts) | ||
| return this | ||
@@ -81,2 +81,6 @@ } | ||
| getDefinitions() { | ||
| return this.#registry.getDefinitions() | ||
| } | ||
| check(expression) { | ||
@@ -83,0 +87,0 @@ try { |
+150
-32
@@ -93,3 +93,3 @@ import {EvaluationError} from './errors.js' | ||
| get(name) { | ||
| return super.get(name) ?? (RESERVED.has(name) ? undefined : dynType) | ||
| return super.get(name) ?? (RESERVED.has(name) ? undefined : dynVariableDeclaration) | ||
| } | ||
@@ -316,14 +316,27 @@ } | ||
| export class VariableDeclaration { | ||
| constructor({name, type, description}) { | ||
| this.name = name | ||
| this.type = type | ||
| this.description = description ?? null | ||
| objFreeze(this) | ||
| } | ||
| } | ||
| export class FunctionDeclaration { | ||
| #hasPlaceholderTypes | ||
| constructor({name, receiverType, argTypes, returnType, handler}) { | ||
| constructor({name, receiverType, returnType, handler, description, params}) { | ||
| if (typeof name !== 'string') throw new Error('name must be a string') | ||
| if (typeof handler !== 'function') throw new Error('handler must be a function') | ||
| this.name = name | ||
| this.receiverType = receiverType || null | ||
| this.argTypes = argTypes | ||
| this.receiverType = receiverType ?? null | ||
| this.returnType = returnType | ||
| this.description = description ?? null | ||
| this.params = params | ||
| this.argTypes = params.map((p) => p.type) | ||
| this.macro = this.argTypes.includes(astType) | ||
| this.macro = argTypes.includes(astType) | ||
| const receiverString = receiverType ? `${receiverType}.` : '' | ||
| this.signature = `${receiverString}${name}(${argTypes.join(', ')}): ${returnType}` | ||
| this.signature = `${receiverString}${name}(${this.argTypes.join(', ')}): ${returnType}` | ||
| this.handler = this.macro ? wrapMacroExpander(this.signature, handler) : handler | ||
@@ -426,2 +439,3 @@ | ||
| const dynType = _createDynType() | ||
| const dynVariableDeclaration = new VariableDeclaration({name: 'dyn', type: dynType}) | ||
| const astType = _createPrimitiveType('ast') | ||
@@ -545,2 +559,4 @@ const listType = _createListType(dynType) | ||
| const invalidVar = 'Invalid variable declaration:' | ||
| export class Registry { | ||
@@ -587,6 +603,25 @@ #overloadResolutionCache = {} | ||
| registerVariable(name, type) { | ||
| if (RESERVED.has(name)) throw new Error(`Cannot register reserved variable name: ${name}`) | ||
| if (this.variables.has(name)) throw new Error(`Variable already registered: ${name}`) | ||
| this.variables.set(name, type instanceof TypeDeclaration ? type : this.getType(type)) | ||
| registerVariable(name, type, opts) { | ||
| let description = opts?.description | ||
| if ( | ||
| typeof name === 'string' && | ||
| typeof type === 'object' && | ||
| !(type instanceof TypeDeclaration) | ||
| ) { | ||
| description = type.description | ||
| type = type.type | ||
| } else if (typeof name === 'object') { | ||
| type = name.type | ||
| description = name.description | ||
| name = name.name | ||
| } | ||
| if (typeof name !== 'string' || !name) throw new Error(`${invalidVar} name must be a string`) | ||
| if (RESERVED.has(name)) throw new Error(`${invalidVar} '${name}' is a reserved name`) | ||
| if (this.variables.has(name)) throw new Error(`${invalidVar} '${name}' is already registered`) | ||
| if (typeof type === 'string') type = this.getType(type) | ||
| else if (!(type instanceof TypeDeclaration)) throw new Error(`${invalidVar} type is required`) | ||
| this.variables.set(name, new VariableDeclaration({name, type, description})) | ||
| return this | ||
@@ -596,3 +631,9 @@ } | ||
| registerConstant(name, type, value) { | ||
| this.registerVariable(name, type) | ||
| if (typeof name === 'object') { | ||
| value = name.value | ||
| this.registerVariable(name) | ||
| name = name.name | ||
| } else { | ||
| this.registerVariable(name, type) | ||
| } | ||
| this.constants.set(name, value) | ||
@@ -697,2 +738,6 @@ return this | ||
| if (typeof typeStr !== 'string' || !typeStr.length) { | ||
| throw new Error(`Invalid type: must be a string`) | ||
| } | ||
| match = typeStr.match(/^[A-Z]$/) | ||
@@ -973,22 +1018,44 @@ if (match) return this.#createDeclaration(_createPlaceholderType, typeStr, typeStr) | ||
| /** @param {string} signature */ | ||
| #parseFunctionDeclaration(signature, handler) { | ||
| // Parse "optional<A>.value(): A" or "dyn(A): dyn<A>" or "string.indexOf(string): int" | ||
| const match = signature.match(/^(?:([a-zA-Z0-9.<>]+)\.)?(\w+)\((.*?)\):\s*(.+)$/) | ||
| if (!match) throw new Error(`Invalid signature: ${signature}`) | ||
| const [, receiverType, name, argsStr, _returnType] = match | ||
| getDefinitions() { | ||
| const variables = [] | ||
| const functions = [] | ||
| for (const [, varDecl] of this.variables) { | ||
| if (!varDecl) continue | ||
| variables.push({ | ||
| name: varDecl.name, | ||
| description: varDecl.description || null, | ||
| type: varDecl.type.name | ||
| }) | ||
| } | ||
| try { | ||
| return new FunctionDeclaration({ | ||
| name: name, | ||
| receiverType: receiverType ? this.getType(receiverType) : null, | ||
| returnType: this.getType(_returnType.trim()), | ||
| argTypes: splitByComma(argsStr).map((s) => this.getFunctionType(s)), | ||
| handler | ||
| for (const [, decl] of this.#functionDeclarations) { | ||
| functions.push({ | ||
| signature: decl.signature, | ||
| name: decl.name, | ||
| description: decl.description, | ||
| receiverType: decl.receiverType ? decl.receiverType.name : null, | ||
| returnType: decl.returnType.name, | ||
| params: decl.params.map((p) => ({ | ||
| name: p.name, | ||
| type: p.type.name, | ||
| description: p.description | ||
| })) | ||
| }) | ||
| } catch (e) { | ||
| throw new Error(`Invalid function declaration: ${signature}: ${e.message}`) | ||
| } | ||
| return {variables, functions} | ||
| } | ||
| #parseSignature(signature) { | ||
| if (typeof signature !== 'string') throw new Error('Invalid signature: must be a string') | ||
| const match = signature.match(/^(?:([a-zA-Z0-9.<>]+)\.)?(\w+)\((.*?)\):\s*(.+)$/) | ||
| if (!match) throw new Error(`Invalid signature: ${signature}`) | ||
| return { | ||
| receiverType: match[1] || null, | ||
| name: match[2], | ||
| argTypes: splitByComma(match[3]), | ||
| returnType: match[4].trim() | ||
| } | ||
| } | ||
| /** | ||
@@ -1012,3 +1079,3 @@ * @param {FunctionDeclaration} a | ||
| const o = a.argTypes[i] | ||
| return t === o || t === astType || o === astType || t === dynType || o === dynType | ||
| return t === o || t === dynType || o === dynType | ||
| })) | ||
@@ -1028,5 +1095,56 @@ ) | ||
| registerFunctionOverload(s, _opts) { | ||
| const handler = typeof _opts === 'function' ? _opts : _opts?.handler | ||
| const dec = this.#parseFunctionDeclaration(s, handler) | ||
| #normalizeParam(i, aType, param) { | ||
| if (!param) return {type: this.getFunctionType(aType), name: `arg${i}`, description: null} | ||
| const type = param.type || aType | ||
| if (!type) throw new Error(`params[${i}].type is required`) | ||
| if (aType && type !== aType) throw new Error(`params[${i}].type not equal to signature type`) | ||
| return { | ||
| name: param.name || `arg${i}`, | ||
| type: this.getFunctionType(type), | ||
| description: param.description ?? null | ||
| } | ||
| } | ||
| registerFunctionOverload(s, handler, opts) { | ||
| if (typeof s === 'object') opts = s | ||
| else if (typeof handler === 'object') opts = handler | ||
| else if (!opts) opts = {} | ||
| const sig = typeof s === 'string' ? s : (opts.signature ?? undefined) | ||
| const parsed = sig !== undefined ? this.#parseSignature(sig) : undefined | ||
| const name = parsed?.name || opts.name | ||
| const receiverType = parsed?.receiverType || opts.receiverType | ||
| const argTypes = parsed?.argTypes | ||
| const returnType = parsed?.returnType || opts.returnType | ||
| const params = opts.params | ||
| let dec | ||
| try { | ||
| if (!name) throw new Error(`signature or name are required`) | ||
| if (!returnType) throw new Error(`must have a returnType`) | ||
| if (params) { | ||
| if (argTypes && params.length !== argTypes.length) { | ||
| throw new Error(`mismatched length in params and args in signature`) | ||
| } | ||
| } else if (!argTypes) throw new Error(`signature or params are required`) | ||
| dec = new FunctionDeclaration({ | ||
| name, | ||
| receiverType: receiverType ? this.getType(receiverType) : null, | ||
| returnType: this.getType(returnType), | ||
| handler: typeof handler === 'function' ? handler : opts.handler, | ||
| description: opts.description, | ||
| params: (argTypes || params).map((_, i) => | ||
| this.#normalizeParam(i, argTypes?.[i], params?.[i]) | ||
| ) | ||
| }) | ||
| } catch (e) { | ||
| if (typeof sig === 'string') e.message = `Invalid function declaration '${sig}': ${e.message}` | ||
| else if (name) e.message = `Invalid function declaration '${name}': ${e.message}` | ||
| else e.message = `Invalid function declaration: ${e.message}` | ||
| throw e | ||
| } | ||
| this.#checkOverlappingSignatures(dec) | ||
@@ -1190,3 +1308,3 @@ this.#functionDeclarations.set(dec.signature, dec) | ||
| getType(name) { | ||
| return this.#variables.get(name) | ||
| return this.#variables.get(name)?.type | ||
| } | ||
@@ -1193,0 +1311,0 @@ |
@@ -31,3 +31,3 @@ import {TypeError, EvaluationError} from './errors.js' | ||
| checkAccessOnType(ast, ctx, leftType, allowMissingField = false) { | ||
| if (leftType.kind === 'dyn') return leftType | ||
| if (leftType === this.dynType) return leftType | ||
@@ -39,6 +39,4 @@ const indexTypeName = ( | ||
| if (leftType.kind === 'list') { | ||
| if (indexTypeName !== 'int' && indexTypeName !== 'dyn') { | ||
| throw new this.Error(`List index must be int, got '${indexTypeName}'`, ast) | ||
| } | ||
| return leftType.valueType | ||
| if (indexTypeName === 'int' || indexTypeName === 'dyn') return leftType.valueType | ||
| throw new this.Error(`List index must be int, got '${indexTypeName}'`, ast) | ||
| } | ||
@@ -45,0 +43,0 @@ |
+6
-6
| { | ||
| "name": "@marcbachmann/cel-js", | ||
| "version": "7.3.1", | ||
| "version": "7.4.0", | ||
| "description": "A lightweight Common Expression Language (CEL) implementation in JavaScript with zero dependencies", | ||
@@ -72,12 +72,12 @@ "keywords": [ | ||
| "service": "drone", | ||
| "commit": "d2520eb4eda8d546e4003c2aab04af524af5d46b", | ||
| "build": "233", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/233", | ||
| "commit": "74bb594614b78ef4b8b47b1b4c344f6848e9fd02", | ||
| "build": "235", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/235", | ||
| "branch": "main", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/233", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/235", | ||
| "isPr": false, | ||
| "slug": "marcbachmann/cel-js", | ||
| "root": "/drone/src", | ||
| "date": "2026-02-09T10:56:44.061Z" | ||
| "date": "2026-02-11T23:27:43.430Z" | ||
| } | ||
| } |
+64
-1
@@ -89,3 +89,3 @@ # @marcbachmann/cel-js [](https://www.npmjs.com/package/@marcbachmann/cel-js) [](https://opensource.org/licenses/MIT) | ||
| Use `registerConstant(name, type, value)` to expose shared configuration without passing it through every evaluation context. | ||
| Use `registerConstant` to expose shared configuration without passing it through every evaluation context. | ||
@@ -101,2 +101,9 @@ ```javascript | ||
| Supported signatures: | ||
| ```javascript | ||
| env.registerConstant('minAge', 'int', 18n) | ||
| env.registerConstant({name: 'minAge', type: 'int', value: 18n, description: 'Minimum age'}) | ||
| ``` | ||
| #### Environment Options | ||
@@ -138,3 +145,59 @@ | ||
| - **`check(expression)`** - Validate expression types without evaluation | ||
| - **`getDefinitions()`** - Returns all registered variables and functions with their types, signatures, and descriptions | ||
| #### `registerVariable` signatures | ||
| ```javascript | ||
| env.registerVariable('user', 'map') | ||
| env.registerVariable('user', 'map', {description: 'The current user'}) | ||
| env.registerVariable('user', {type: 'map', description: 'The current user'}) | ||
| env.registerVariable({name: 'user', type: 'map', description: 'The current user'}) | ||
| ``` | ||
| The `type` can be a type string (e.g. `'int'`, `'map'`, `'list<string>'`) or a `TypeDeclaration` obtained from another environment. | ||
| #### `registerFunction` signatures | ||
| ```javascript | ||
| // Signature string + handler | ||
| env.registerFunction('greet(string): string', (name) => `Hello, ${name}!`) | ||
| env.registerFunction('greet(string): string', handler, {description: 'Greets someone'}) | ||
| env.registerFunction('greet(string): string', {handler, description: 'Greets someone'}) | ||
| // Single object with signature string | ||
| env.registerFunction({signature: 'add(int, int): int', handler, description: 'Adds two integers'}) | ||
| // Single object with signature string and named params | ||
| env.registerFunction({ | ||
| signature: 'formatDate(int, string): string', | ||
| handler, | ||
| description: 'Formats a timestamp', | ||
| params: [ | ||
| {name: 'timestamp', description: 'Unix timestamp in seconds'}, | ||
| {name: 'format', description: 'Date format string'} | ||
| ] | ||
| }) | ||
| // Single object without signature string | ||
| env.registerFunction({ | ||
| name: 'multiply', | ||
| returnType: 'int', | ||
| handler: (a, b) => a * b, | ||
| description: 'Multiplies two integers', | ||
| params: [ | ||
| {name: 'a', type: 'int', description: 'First number'}, | ||
| {name: 'b', type: 'int', description: 'Second number'} | ||
| ] | ||
| }) | ||
| // Receiver method (called as 'hello'.shout()) | ||
| env.registerFunction({ | ||
| name: 'shout', | ||
| receiverType: 'string', | ||
| returnType: 'string', | ||
| handler: (str) => str.toUpperCase() + '!', | ||
| params: [] | ||
| }) | ||
| ``` | ||
| #### `registerFunction` (sync & async) | ||
@@ -141,0 +204,0 @@ |
192444
3.22%4855
2.19%657
10.61%