@marcbachmann/cel-js
Advanced tools
+2
-2
@@ -152,3 +152,3 @@ import {createRegistry, RootContext} from './registry.js' | ||
| debugOperandType(value, checkedType) { | ||
| if (checkedType?.hasNoDynTypes()) return checkedType | ||
| if (checkedType?.hasDynType === false) return checkedType | ||
| return this.debugRuntimeType(value, checkedType).wrappedType | ||
@@ -158,3 +158,3 @@ } | ||
| debugRuntimeType(value, checkedType) { | ||
| if (checkedType?.hasNoDynTypes()) return checkedType | ||
| if (checkedType?.hasDynType === false) return checkedType | ||
@@ -161,0 +161,0 @@ const runtimeType = this.debugType(value) |
+2
-2
@@ -390,4 +390,4 @@ import {EvaluationError} from './errors.js' | ||
| const GPD = 'google.protobuf.Duration' | ||
| const TimestampType = registry.registerType(TS, Date).getObjectType(TS).typeType | ||
| const DurationType = registry.registerType(GPD, Duration).getObjectType(GPD).typeType | ||
| const TimestampType = registry.registerType(TS, Date).typeType | ||
| const DurationType = registry.registerType(GPD, Duration).typeType | ||
| registry.registerConstant('google', 'map<string, map<string, type>>', { | ||
@@ -394,0 +394,0 @@ protobuf: {Duration: DurationType, Timestamp: TimestampType} |
+29
-34
| import {EvaluationError, ParseError} from './errors.js' | ||
| import {ASTNode} from './parser.js' | ||
| import {OPERATORS as OPS} from './operators.js' | ||
@@ -11,6 +10,2 @@ const identity = (x) => x | ||
| function ast(callAst, op, args) { | ||
| return new ASTNode(callAst.input, callAst.pos, op, args) | ||
| } | ||
| function createMapExpander(hasFilter) { | ||
@@ -24,14 +19,14 @@ const functionDesc = hasFilter ? 'map(var, filter, transform)' : 'map(var, transform)' | ||
| let step = ast(transform, OPS.accuPush, transform) | ||
| let step = transform.clone(OPS.accuPush, transform) | ||
| if (predicate) { | ||
| const accuValue = ast(predicate, OPS.accuValue) | ||
| step = ast(predicate, OPS.ternary, [predicate.setMeta('label', label), step, accuValue]) | ||
| const accuValue = predicate.clone(OPS.accuValue) | ||
| step = predicate.clone(OPS.ternary, [predicate.setMeta('label', label), step, accuValue]) | ||
| } | ||
| return { | ||
| callAst: ast(callAst, OPS.comprehension, { | ||
| callAst: callAst.clone(OPS.comprehension, { | ||
| errorsAreFatal: true, | ||
| iterable: receiver, | ||
| iterVarName: assertIdentifier(iterVar, invalidMsg), | ||
| init: ast(callAst, OPS.list, []), | ||
| init: callAst.clone(OPS.list, []), | ||
| step, | ||
@@ -51,13 +46,13 @@ result: identity | ||
| const iterVarName = assertIdentifier(args[0], invalidMsg) | ||
| const accuValue = ast(callAst, OPS.accuValue) | ||
| const accuValue = callAst.clone(OPS.accuValue) | ||
| const predicate = args[1].setMeta('label', label) | ||
| const appendItem = ast(callAst, OPS.accuPush, ast(callAst, OPS.id, iterVarName)) | ||
| const step = ast(predicate, OPS.ternary, [predicate, appendItem, accuValue]) | ||
| const appendItem = callAst.clone(OPS.accuPush, callAst.clone(OPS.id, iterVarName)) | ||
| const step = predicate.clone(OPS.ternary, [predicate, appendItem, accuValue]) | ||
| return { | ||
| callAst: ast(callAst, OPS.comprehension, { | ||
| callAst: callAst.clone(OPS.comprehension, { | ||
| errorsAreFatal: true, | ||
| iterable: receiver, | ||
| iterVarName, | ||
| init: ast(callAst, OPS.list, []), | ||
| init: callAst.clone(OPS.list, []), | ||
| step, | ||
@@ -78,3 +73,3 @@ result: identity | ||
| return { | ||
| callAst: ast(callAst, OPS.comprehension, { | ||
| callAst: callAst.clone(OPS.comprehension, { | ||
| kind: 'quantifier', | ||
@@ -140,8 +135,8 @@ errorsAreFatal: opts.errorsAreFatal || false, | ||
| return { | ||
| init: ast(callAst, OPS.value, true), | ||
| init: callAst.clone(OPS.value, true), | ||
| condition: identity, | ||
| step: ast(predicate, OPS.ternary, [ | ||
| step: predicate.clone(OPS.ternary, [ | ||
| predicate, | ||
| ast(predicate, OPS.value, true), | ||
| ast(predicate, OPS.value, false) | ||
| predicate.clone(OPS.value, true), | ||
| predicate.clone(OPS.value, false) | ||
| ]) | ||
@@ -162,8 +157,8 @@ } | ||
| return { | ||
| init: ast(callAst, OPS.value, false), | ||
| init: callAst.clone(OPS.value, false), | ||
| condition: opts.condition, | ||
| step: ast(predicate, OPS.ternary, [ | ||
| step: predicate.clone(OPS.ternary, [ | ||
| predicate, | ||
| ast(predicate, OPS.value, true), | ||
| ast(predicate, OPS.value, false) | ||
| predicate.clone(OPS.value, true), | ||
| predicate.clone(OPS.value, false) | ||
| ]) | ||
@@ -184,6 +179,6 @@ } | ||
| transform({ast: callAst, predicate, opts}) { | ||
| const accuValue = ast(callAst, OPS.accuValue) | ||
| const accuValue = callAst.clone(OPS.accuValue) | ||
| return { | ||
| init: ast(callAst, OPS.value, 0), | ||
| step: ast(predicate, OPS.ternary, [predicate, ast(callAst, OPS.accuInc), accuValue]), | ||
| init: callAst.clone(OPS.value, 0), | ||
| step: predicate.clone(OPS.ternary, [predicate, callAst.clone(OPS.accuInc), accuValue]), | ||
| result: opts.result | ||
@@ -199,5 +194,5 @@ } | ||
| function bindOptionalEvaluate(ev, macro, ctx, boundValue) { | ||
| const res = ev.eval(macro.exp, (ctx = macro.bindCtx.reuse(ctx).setIterValue(boundValue))) | ||
| if (res instanceof Promise && ctx === macro.bindCtx) ctx.async = true | ||
| function bindOptionalEvaluate(ev, exp, bindCtx, ctx, boundValue) { | ||
| const res = ev.eval(exp, (ctx = bindCtx.reuse(ctx).setIterValue(boundValue, ev, exp))) | ||
| if (res instanceof Promise && ctx === bindCtx) ctx.async = true | ||
| return res | ||
@@ -216,6 +211,6 @@ } | ||
| function bindEvaluate(ev, macro, ctx) { | ||
| const v = ev.eval(macro.val, ctx) | ||
| if (v instanceof Promise) return v.then((_v) => bindOptionalEvaluate(ev, macro, ctx, _v)) | ||
| return bindOptionalEvaluate(ev, macro, ctx, v) | ||
| function bindEvaluate(ev, {val, exp, bindCtx}, ctx) { | ||
| const v = ev.eval(val, ctx) | ||
| if (v instanceof Promise) return v.then((_v) => bindOptionalEvaluate(ev, exp, bindCtx, ctx, _v)) | ||
| return bindOptionalEvaluate(ev, exp, bindCtx, ctx, v) | ||
| } | ||
@@ -222,0 +217,0 @@ |
+24
-29
| import {celTypes} from './registry.js' | ||
| import {objKeys, isArray} from './globals.js' | ||
| const dynType = celTypes.dyn | ||
@@ -202,3 +203,3 @@ export class Base { | ||
| if (!(left.hasDyn() || right.hasDyn())) { | ||
| if (!(left.hasDynType || right.hasDynType)) { | ||
| ast.staticHandler = chk.registry.findBinaryOverload(op, left, right) | ||
@@ -234,3 +235,3 @@ } | ||
| const types = (ast.argTypes ??= new Array(argLen)) | ||
| const types = ast.argTypes | ||
| let i = argLen | ||
@@ -260,6 +261,6 @@ while (i--) types[i] = ev.debugOperandType(args[i], argAst[i].checkedType) | ||
| let i = args.length | ||
| const types = (ast.argTypes ??= new Array(i)) | ||
| const types = ast.argTypes | ||
| while (i--) types[i] = ev.debugOperandType(args[i], argAst[i].checkedType) | ||
| const receiverType = ev.debugRuntimeType(receiver, receiverAst.checkedType || ev.dynType) | ||
| const receiverType = ev.debugRuntimeType(receiver, receiverAst.checkedType || dynType) | ||
| const decl = candidates.findMatch(types, receiverType) | ||
@@ -333,3 +334,3 @@ if (decl) return decl.handler.call(ev, receiver, ...args) | ||
| if (condition && !condition(accu)) break | ||
| accu = ev.eval(step, ctx.setIterValue(items[i++])) | ||
| accu = ev.eval(step, ctx.setIterValue(items[i++], ev, step)) | ||
| if (accu instanceof Promise) return continueLoop(ev, ctx, args, items, accu, i) | ||
@@ -348,3 +349,3 @@ } | ||
| if (condition && !condition(accu)) return args.result(accu) | ||
| accu = ev.eval(step, ctx.setIterValue(items[i++])) | ||
| accu = ev.eval(step, ctx.setIterValue(items[i++], ev, step)) | ||
| if (accu instanceof Promise) accu = await accu | ||
@@ -361,3 +362,3 @@ } | ||
| if (!condition(accu)) return args.result(accu) | ||
| stp = ev.tryEval(step, ctx.setIterValue(items[i++])) | ||
| stp = ev.tryEval(step, ctx.setIterValue(items[i++], ev, step)) | ||
| if (stp instanceof Promise) return continueQuantifier(ev, ctx, args, items, accu, i, error, stp) | ||
@@ -385,3 +386,3 @@ if (stp instanceof Error && (error ??= stp)) continue | ||
| if (!condition(accu)) return args.result(accu) | ||
| stp = ev.tryEval(step, ctx.setIterValue(items[i++])) | ||
| stp = ev.tryEval(step, ctx.setIterValue(items[i++], ev, step)) | ||
| if (stp instanceof Promise) stp = await stp | ||
@@ -402,4 +403,4 @@ if (stp instanceof Error && (error ??= stp)) continue | ||
| function fieldAccess(ev, ast, left, right) { | ||
| // const leftType = ast.args[0].checkedType | ||
| // if (leftType.name !== 'dyn') return leftType.field(left, right, ast, ev) | ||
| const leftType = ast.args[0].checkedType | ||
| if (leftType !== dynType) return leftType.field(left, right, ast, ev) | ||
| return ev.debugType(left).field(left, right, ast, ev) | ||
@@ -419,19 +420,13 @@ } | ||
| check(chk, ast, ctx) { | ||
| const varType = ctx.getType(ast.args) | ||
| if (varType !== undefined) return varType | ||
| throw new chk.Error(`Unknown variable: ${ast.args}`, ast) | ||
| const variable = ctx.getVariable(ast.args) | ||
| if (!variable) throw new chk.Error(`Unknown variable: ${ast.args}`, ast) | ||
| if (variable.constant) { | ||
| const alternate = ast.clone(OPERATORS.value, variable.value) | ||
| ast.setMeta('alternate', alternate) | ||
| return chk.check(alternate, ctx) | ||
| } | ||
| return variable.type | ||
| }, | ||
| evaluate(ev, ast, ctx) { | ||
| const type = ast.checkedType || ctx.getType(ast.args) | ||
| const value = type && ctx.getValue(ast.args) | ||
| if (value === undefined) throw new ev.Error(`Unknown variable: ${ast.args}`, ast) | ||
| const valueType = ev.debugType(value) | ||
| switch (type) { | ||
| case valueType: | ||
| case celTypes.dyn: | ||
| return value | ||
| default: | ||
| if (type.matches(valueType)) return value | ||
| } | ||
| throw new ev.Error(`Variable '${ast.args}' is not of type '${type}', got '${valueType}'`, ast) | ||
| return ctx.getCheckedValue(ev, ast) | ||
| } | ||
@@ -490,3 +485,3 @@ }, | ||
| const argTypes = args.map((a) => chk.check(a, ctx)) | ||
| const argTypes = (ast.argTypes = args.map((a) => chk.check(a, ctx))) | ||
| const decl = candidates.findMatch(argTypes) | ||
@@ -501,3 +496,3 @@ | ||
| if (!argTypes.some((t) => t.hasDyn())) ast.staticHandler = decl | ||
| if (!argTypes.some((t) => t.hasDynType)) ast.staticHandler = decl | ||
| return decl.returnType | ||
@@ -521,3 +516,3 @@ }, | ||
| const argTypes = args.map((a) => chk.check(a, ctx)) | ||
| const argTypes = (ast.argTypes = args.map((a) => chk.check(a, ctx))) | ||
| if (receiverType.kind === 'dyn' && candidates.returnType) return candidates.returnType | ||
@@ -535,3 +530,3 @@ const decl = candidates.findMatch(argTypes, receiverType) | ||
| if (!receiverType.hasPlaceholder() && !argTypes.some((t) => t.hasDyn())) { | ||
| if (!receiverType.hasPlaceholderType && !argTypes.some((t) => t.hasDynType)) { | ||
| ast.staticHandler = decl | ||
@@ -538,0 +533,0 @@ } |
+3
-1
@@ -55,3 +55,5 @@ import {EvaluationError} from './errors.js' | ||
| export function toggleOptionalTypes(registry, enable) { | ||
| registry.constants.set('optional', enable ? optionalNamespace : undefined) | ||
| const optionalConstant = enable ? optionalNamespace : undefined | ||
| registry.variables.set('optional', undefined) | ||
| registry.registerConstant('optional', 'OptionalNamespace', optionalConstant) | ||
| } | ||
@@ -58,0 +60,0 @@ |
+6
-7
@@ -192,3 +192,5 @@ import {UnsignedInt, Duration} from './functions.js' | ||
| switch (typeof a) { | ||
| case 'undefined': | ||
| case 'string': | ||
| case 'boolean': | ||
| return false | ||
@@ -203,11 +205,8 @@ case 'bigint': | ||
| return false | ||
| case 'boolean': | ||
| return false | ||
| case 'object': | ||
| if (typeof b !== 'object' || a === null || b === null) return false | ||
| if (typeof b !== 'object') return false | ||
| const leftType = ev.debugType(a) | ||
| const rightType = ev.debugType(b) | ||
| if (leftType !== rightType) return false | ||
| const leftType = ev.objectTypesByConstructor.get(a.constructor)?.type | ||
| const rightType = ev.objectTypesByConstructor.get(b.constructor)?.type | ||
| if (!leftType || leftType !== rightType) return false | ||
| const overload = ev.registry.findBinaryOverload('==', leftType, rightType) | ||
@@ -214,0 +213,0 @@ if (!overload) return false |
+4
-0
@@ -85,2 +85,6 @@ import {UnsignedInt} from './functions.js' | ||
| clone(op, args) { | ||
| return new ASTNode(this.#meta.input, this.#meta.pos, op, args) | ||
| } | ||
| get meta() { | ||
@@ -87,0 +91,0 @@ return this.#meta |
+29
-7
@@ -96,2 +96,12 @@ /** | ||
| /** | ||
| * Schema definition for inline typed object registration. | ||
| * Maps field names to type strings or nested schemas. | ||
| * When used with `registerVariable`, internally calls `registerType` to create | ||
| * a named type with runtime conversion support. | ||
| */ | ||
| export interface ObjectSchema { | ||
| [field: string]: string | ObjectSchema | ||
| } | ||
| /** | ||
| * Registry for managing function overloads, operator overloads, and type mappings. | ||
@@ -130,10 +140,13 @@ */ | ||
| * Register a custom type with its constructor and optional field definitions. | ||
| * When `ctor` is omitted but `fields` is provided, an internal wrapper class is auto-generated | ||
| * and a default `convert` function is created to wrap plain objects at runtime. | ||
| * @param typename - The name of the type | ||
| * @param definition - Either a constructor function or an object with ctor and fields | ||
| * @param withoutDynRegistration - If true, skip automatic dyn() and type() function registration | ||
| * @param definition - Either a constructor function or an object with ctor/fields/convert | ||
| */ | ||
| registerType( | ||
| typename: string, | ||
| definition: Function | {ctor: Function; fields?: Record<string, any>}, | ||
| withoutDynRegistration?: boolean | ||
| definition: | ||
| | Function | ||
| | {ctor: Function; fields?: Record<string, string>; convert?: (value: any) => any} | ||
| | {fields: Record<string, string>; convert?: (value: any) => any} | ||
| ): void | ||
@@ -150,6 +163,15 @@ | ||
| * Register a variable with its type, throwing if it already exists. | ||
| * When an ObjectSchema is provided via `{schema: ...}`, a type is auto-registered | ||
| * via `registerType` with runtime conversion support. | ||
| * @param name - The variable name | ||
| * @param type - The variable type name or declaration | ||
| * @param type - The variable type name, declaration, or options with schema | ||
| */ | ||
| registerVariable(name: string, type: string | TypeDeclaration): this | ||
| registerVariable( | ||
| name: string, | ||
| type: | ||
| | string | ||
| | TypeDeclaration | ||
| | {type: string; description?: string} | ||
| | {schema: ObjectSchema; description?: string} | ||
| ): this | ||
@@ -253,3 +275,3 @@ /** | ||
| /** Set the iteration variable value */ | ||
| setIterValue(iterValue: any): this | ||
| setIterValue(iterValue: any, evaluator: any, ast: any): this | ||
@@ -256,0 +278,0 @@ /** Resolve a value by name, falling back to parent scopes. */ |
+251
-118
@@ -70,3 +70,4 @@ import {EvaluationError} from './errors.js' | ||
| has(key) { | ||
| return this.#entries.has(key) || (this.#parent ? this.#parent.has(key) : false) | ||
| if (this.#entries.has(key)) return !!this.#entries.get(key) | ||
| return this.#parent ? this.#parent.has(key) : false | ||
| } | ||
@@ -94,3 +95,5 @@ | ||
| get(name) { | ||
| return super.get(name) ?? (RESERVED.has(name) ? undefined : dynVariableDeclaration) | ||
| return ( | ||
| super.get(name) ?? (RESERVED.has(name) ? undefined : new VariableDeclaration(name, dynType)) | ||
| ) | ||
| } | ||
@@ -106,16 +109,21 @@ } | ||
| #matchesCache = new WeakMap() | ||
| #hasDynTypes = null | ||
| #hasPlaceholderTypes = null | ||
| constructor({kind, type, name, keyType, valueType, values}) { | ||
| constructor({kind, type, name, keyType, valueType}) { | ||
| this.kind = kind | ||
| this.type = type | ||
| this.name = name | ||
| this.keyType = keyType | ||
| this.valueType = valueType | ||
| if (keyType) this.keyType = keyType | ||
| if (valueType) this.valueType = valueType | ||
| if (values) this.values = values | ||
| this.unwrappedType = kind === 'dyn' && valueType ? valueType.unwrappedType : this | ||
| this.wrappedType = kind === 'dyn' ? this : _createDynType(this.unwrappedType) | ||
| this.hasDynType = | ||
| this.kind === 'dyn' || this.valueType?.hasDynType || this.keyType?.hasDynType || false | ||
| this.hasPlaceholderType = | ||
| this.kind === 'param' || | ||
| this.keyType?.hasPlaceholderType || | ||
| this.valueType?.hasPlaceholderType || | ||
| false | ||
| if (kind === 'list') this.fieldLazy = this.#getListField | ||
@@ -126,20 +134,13 @@ else if (kind === 'map') this.fieldLazy = this.#getMapField | ||
| this.#hasDynTypes = | ||
| this.kind === 'dyn' || this.valueType?.hasDyn() || this.keyType?.hasDyn() || false | ||
| this.#hasPlaceholderTypes = | ||
| this.kind === 'param' || | ||
| this.keyType?.hasPlaceholder() || | ||
| this.valueType?.hasPlaceholder() || | ||
| false | ||
| objFreeze(this) | ||
| } | ||
| /** @deprecated Please use .hasDynType */ | ||
| hasDyn() { | ||
| return this.#hasDynTypes | ||
| return this.hasDynType | ||
| } | ||
| /** @deprecated Please use .hasDynType === false */ | ||
| hasNoDynTypes() { | ||
| return this.#hasDynTypes === false | ||
| return this.hasDynType === false | ||
| } | ||
@@ -155,4 +156,5 @@ | ||
| /** @deprecated Please use .hasPlaceholderType */ | ||
| hasPlaceholder() { | ||
| return this.#hasPlaceholderTypes | ||
| return this.hasPlaceholderType | ||
| } | ||
@@ -165,3 +167,4 @@ | ||
| if (t1.kind !== t2.kind) return null | ||
| if (!(t1.hasPlaceholder() || t2.hasPlaceholder() || t1.hasDyn() || t2.hasDyn())) return null | ||
| if (!(t1.hasPlaceholderType || t2.hasPlaceholderType || t1.hasDynType || t2.hasDynType)) | ||
| return null | ||
@@ -182,3 +185,3 @@ const valueType = t1.valueType.unify(r, t2.valueType) | ||
| templated(r, bind) { | ||
| if (!this.hasPlaceholder()) return this | ||
| if (!this.hasPlaceholderType) return this | ||
@@ -219,13 +222,14 @@ switch (this.kind) { | ||
| #getMessageField(obj, key, ast, ev) { | ||
| const message = ev.objectTypesByConstructor.get(obj.constructor) | ||
| if (!message || !(obj instanceof message.ctor)) return | ||
| if (!message.fields) return hasOwn(obj, key) ? obj[key] : undefined | ||
| const message = obj ? ev.objectTypesByConstructor.get(obj.constructor) : undefined | ||
| if (!message) return | ||
| const type = message.fields[key] | ||
| if (!type) return | ||
| const type = message.fields ? message.fields[key] : dynType | ||
| if (!type) return undefined | ||
| const value = obj[key] | ||
| const value = obj instanceof Map ? obj.get(key) : obj[key] | ||
| if (value === undefined) return | ||
| const valueType = ev.debugType(value) | ||
| switch (type) { | ||
| case celTypes.dyn: | ||
| case dynType: | ||
| case valueType: | ||
@@ -240,22 +244,39 @@ return value | ||
| #getMapField(obj, key, ast, ev) { | ||
| let value | ||
| if (obj instanceof Map) value = obj.get(key) | ||
| else value = hasOwn(obj, key) ? obj[key] : undefined | ||
| // eslint-disable-next-line no-nested-ternary | ||
| const value = obj instanceof Map ? obj.get(key) : obj && hasOwn(obj, key) ? obj[key] : undefined | ||
| if (value === undefined) return | ||
| const valueType = this.valueType | ||
| const type = ev.debugType(value) | ||
| if (valueType.matches(type)) return value | ||
| if (this.valueType.matches(type)) return value | ||
| throw new EvaluationError(`Field '${key}' is not of type '${valueType}', got '${type}'`, ast) | ||
| throw new EvaluationError( | ||
| `Field '${key}' is not of type '${this.valueType}', got '${type}'`, | ||
| ast | ||
| ) | ||
| } | ||
| #getListElementAtIndex(list, pos) { | ||
| switch (list?.constructor) { | ||
| case Array: | ||
| return list[pos] | ||
| case Set: { | ||
| let i = 0 | ||
| for (const item of list) { | ||
| if (i++ !== pos) continue | ||
| return item | ||
| } | ||
| } | ||
| } | ||
| } | ||
| #getListField(obj, key, ast, ev) { | ||
| if (!(typeof key === 'number' || typeof key === 'bigint')) return | ||
| if (typeof key === 'bigint') key = Number(key) | ||
| else if (typeof key !== 'number') return | ||
| const value = obj[key] | ||
| const value = this.#getListElementAtIndex(obj, key) | ||
| if (value === undefined) { | ||
| if (!obj) return | ||
| throw new EvaluationError( | ||
| `No such key: index out of bounds, index ${key} ${ | ||
| key < 0 ? '< 0' : `>= size ${obj.length}` | ||
| key < 0 ? '< 0' : `>= size ${obj.length || obj.size}` | ||
| }`, | ||
@@ -276,2 +297,3 @@ ast | ||
| fieldLazy() {} | ||
| field(obj, key, ast, ev) { | ||
@@ -326,6 +348,8 @@ const v = this.fieldLazy(obj, key, ast, ev) | ||
| export class VariableDeclaration { | ||
| constructor({name, type, description}) { | ||
| constructor(name, type, description, value) { | ||
| this.name = name | ||
| this.type = type | ||
| this.description = description ?? null | ||
| this.constant = value !== undefined | ||
| this.value = value | ||
| objFreeze(this) | ||
@@ -336,3 +360,2 @@ } | ||
| export class FunctionDeclaration { | ||
| #hasPlaceholderTypes | ||
| constructor({name, receiverType, returnType, handler, description, params}) { | ||
@@ -354,6 +377,6 @@ if (typeof name !== 'string') throw new Error('name must be a string') | ||
| this.#hasPlaceholderTypes = | ||
| this.returnType.hasPlaceholder() || | ||
| this.receiverType?.hasPlaceholder() || | ||
| this.argTypes.some((t) => t.hasPlaceholder()) || | ||
| this.hasPlaceholderType = | ||
| this.returnType.hasPlaceholderType || | ||
| this.receiverType?.hasPlaceholderType || | ||
| this.argTypes.some((t) => t.hasPlaceholderType) || | ||
| false | ||
@@ -365,3 +388,3 @@ | ||
| hasPlaceholder() { | ||
| return this.#hasPlaceholderTypes | ||
| return this.hasPlaceholderType | ||
| } | ||
@@ -378,3 +401,2 @@ | ||
| export class OperatorDeclaration { | ||
| #hasPlaceholderTypes | ||
| constructor({operator, leftType, rightType, handler, returnType}) { | ||
@@ -390,4 +412,4 @@ this.operator = operator | ||
| this.#hasPlaceholderTypes = | ||
| this.leftType.hasPlaceholder() || this.rightType?.hasPlaceholder() || false | ||
| this.hasPlaceholderType = | ||
| this.leftType.hasPlaceholderType || this.rightType?.hasPlaceholderType || false | ||
@@ -398,3 +420,3 @@ objFreeze(this) | ||
| hasPlaceholder() { | ||
| return this.#hasPlaceholderTypes | ||
| return this.hasPlaceholderType | ||
| } | ||
@@ -454,7 +476,5 @@ | ||
| const dynType = _createDynType() | ||
| const dynVariableDeclaration = new VariableDeclaration({name: 'dyn', type: dynType}) | ||
| const astType = _createPrimitiveType('ast') | ||
| const listType = _createListType(dynType) | ||
| const mapType = _createMapType(dynType, dynType) | ||
| export const celTypes = { | ||
@@ -511,3 +531,3 @@ string: _createPrimitiveType('string'), | ||
| #matchesFunction(fn, argTypes, receiverType) { | ||
| if (fn.hasPlaceholder()) return this.#matchWithPlaceholders(fn, argTypes, receiverType) | ||
| if (fn.hasPlaceholderType) return this.#matchWithPlaceholders(fn, argTypes, receiverType) | ||
| if (receiverType && fn.receiverType && !receiverType.matches(fn.receiverType)) return | ||
@@ -561,6 +581,2 @@ return fn.matchesArgs(argTypes) | ||
| const objTypesDecls = [ | ||
| [undefined, 'map', TYPES.map, celTypes.map], | ||
| [Object, 'map', TYPES.map, celTypes.map], | ||
| [Map, 'map', TYPES.map, celTypes.map], | ||
| [Array, 'list', TYPES.list, celTypes.list], | ||
| [UnsignedInt, 'uint', TYPES.uint, celTypes.uint], | ||
@@ -576,3 +592,4 @@ [Type, 'type', TYPES.type, celTypes.type], | ||
| const invalidVar = 'Invalid variable declaration:' | ||
| const invalidVar = (postfix) => new Error(`Invalid variable declaration: ${postfix}`) | ||
| const invalidType = (postfix) => new Error(`Invalid type declaration: ${postfix}`) | ||
@@ -622,2 +639,3 @@ export class Registry { | ||
| let description = opts?.description | ||
| let value | ||
| if ( | ||
@@ -629,29 +647,42 @@ typeof name === 'string' && | ||
| description = type.description | ||
| type = type.type | ||
| value = type.value | ||
| if (type.schema) type = this.registerType({name: `$${name}`, schema: type.schema}).type | ||
| else type = type.type | ||
| } else if (typeof name === 'object') { | ||
| type = name.type | ||
| if (name.schema) type = this.registerType({name: `$${name.name}`, schema: name.schema}).type | ||
| else type = name.type | ||
| description = name.description | ||
| value = name.value | ||
| 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 name !== 'string' || !name) throw invalidVar(`name must be a string`) | ||
| if (RESERVED.has(name)) throw invalidVar(`'${name}' is a reserved name`) | ||
| if (this.variables.has(name)) throw 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`) | ||
| else if (!(type instanceof TypeDeclaration)) throw invalidVar(`type is required`) | ||
| this.variables.set(name, new VariableDeclaration({name, type, description})) | ||
| this.variables.set(name, new VariableDeclaration(name, type, description, value)) | ||
| return this | ||
| } | ||
| #registerSchemaAsType(name, schema) { | ||
| const fields = Object.create(null) | ||
| for (const key of objKeys(schema)) { | ||
| const def = schema[key] | ||
| if (typeof def === 'object' && def) { | ||
| fields[key] = this.registerType({name: `${name}.${key}`, schema: def}).type.name | ||
| } else if (typeof def === 'string') { | ||
| fields[key] = def | ||
| } else { | ||
| throw new Error(`Invalid field definition for '${name}.${key}'`) | ||
| } | ||
| } | ||
| return fields | ||
| } | ||
| registerConstant(name, type, value) { | ||
| if (typeof name === 'object') { | ||
| value = name.value | ||
| this.registerVariable(name) | ||
| name = name.name | ||
| } else { | ||
| this.registerVariable(name, type) | ||
| } | ||
| this.constants.set(name, value) | ||
| if (typeof name === 'object') this.registerVariable(name) | ||
| else this.registerVariable({name, type, value}) | ||
| return this | ||
@@ -721,31 +752,33 @@ } | ||
| if (typeof name !== 'string' || name.length < 2 || RESERVED.has(name)) { | ||
| throw new Error(`Message type name invalid: ${name}`) | ||
| throw invalidType(`name '${name}' is not valid`) | ||
| } | ||
| if (this.objectTypes.has(name)) throw new Error(`Message type already registered: ${name}`) | ||
| if (this.objectTypes.has(name)) throw invalidType(`type '${name}' already registered`) | ||
| const type = this.#parseTypeString(name, false) | ||
| if (type.kind !== 'message') throw new Error(`Message type invalid: ${name}`) | ||
| if (type.kind !== 'message') throw invalidType(`type '${name}' is not valid`) | ||
| const ctor = typeof _d === 'function' ? _d : _d?.ctor | ||
| if (typeof ctor !== 'function') throw new Error(`Message type constructor invalid: '${name}'`) | ||
| const decl = Object.freeze({ | ||
| const decl = { | ||
| name, | ||
| typeType: new Type(name), | ||
| type, | ||
| ctor, | ||
| fields: this.#normalizeFields(name, _d?.fields) | ||
| }) | ||
| ctor: typeof _d === 'function' ? _d : _d?.ctor, | ||
| convert: typeof _d === 'function' ? undefined : _d?.convert, | ||
| fields: | ||
| typeof _d?.schema === 'object' | ||
| ? this.#normalizeFields(name, this.#registerSchemaAsType(name, _d.schema)) | ||
| : this.#normalizeFields(name, typeof _d === 'function' ? undefined : _d?.fields) | ||
| } | ||
| this.objectTypes.set(name, decl) | ||
| this.objectTypesByConstructor.set(ctor, decl) | ||
| if (typeof decl.ctor !== 'function') { | ||
| if (!decl.fields) throw invalidType(`type '${name}' requires a constructor or fields`) | ||
| Object.assign(decl, this.#createDefaultConvert(name, decl.fields)) | ||
| } | ||
| this.objectTypes.set(name, Object.freeze(decl)) | ||
| this.objectTypesByConstructor.set(decl.ctor, decl) | ||
| this.registerFunctionOverload(`type(${name}): type`, () => decl.typeType) | ||
| return this | ||
| return decl | ||
| } | ||
| getObjectType(name) { | ||
| return this.objectTypes.get(name) | ||
| } | ||
| /** @returns {TypeDeclaration} */ | ||
@@ -893,3 +926,3 @@ #parseTypeString(typeStr, requireKnownTypes = true) { | ||
| } | ||
| return celTypes.dyn | ||
| return dynType | ||
| } | ||
@@ -905,3 +938,3 @@ | ||
| #matchesOverload(decl, actualLeft, actualRight) { | ||
| const bindings = decl.hasPlaceholder() ? new Map() : null | ||
| const bindings = decl.hasPlaceholderType ? new Map() : null | ||
| const leftType = this.matchTypeWithPlaceholders(decl.leftType, actualLeft, bindings) | ||
@@ -916,3 +949,3 @@ if (!leftType) return | ||
| return decl.hasPlaceholder() | ||
| return decl.hasPlaceholderType | ||
| ? { | ||
@@ -928,3 +961,3 @@ handler: decl.handler, | ||
| matchTypeWithPlaceholders(declared, actual, bindings) { | ||
| if (!declared.hasPlaceholder()) return actual.matches(declared) ? actual : null | ||
| if (!declared.hasPlaceholderType) return actual.matches(declared) ? actual : null | ||
@@ -946,3 +979,3 @@ const treatAsDyn = actual.kind === 'dyn' | ||
| #collectPlaceholderBindings(declared, actual, bindings, fromDyn = false) { | ||
| if (!declared.hasPlaceholder()) return true | ||
| if (!declared.hasPlaceholderType) return true | ||
| if (!actual) return false | ||
@@ -955,3 +988,3 @@ | ||
| case 'param': { | ||
| const candidateType = treatAsDyn ? celTypes.dyn : actual | ||
| const candidateType = treatAsDyn ? dynType : actual | ||
| return this.#bindPlaceholder(declared.name, candidateType, bindings) | ||
@@ -1027,2 +1060,50 @@ } | ||
| #createDefaultConvert(name, fields) { | ||
| const keys = objKeys(fields) | ||
| const verifiedProto = Object.create(null) | ||
| for (const k of keys) { | ||
| const type = fields[k] | ||
| if (type.kind !== 'message') verifiedProto[k] = true | ||
| else verifiedProto[k] = this.objectTypes.get(type.name) | ||
| } | ||
| const Ctor = { | ||
| [name]: class extends Map { | ||
| #raw | ||
| constructor(v) { | ||
| super() | ||
| this.#raw = v | ||
| } | ||
| [Symbol.iterator]() { | ||
| if (this.size !== keys.length) for (const k of keys) this.get(k) | ||
| return super[Symbol.iterator]() | ||
| } | ||
| get(field) { | ||
| const v = super.get(field) | ||
| if (v !== undefined) return v | ||
| const dec = verifiedProto[field] | ||
| if (dec === undefined || this.has(field)) return | ||
| const value = this.#raw instanceof Map ? this.#raw.get(field) : this.#raw[field] | ||
| if (dec === true || value === undefined) return (super.set(field, value), value) | ||
| super.set(field, dec.convert ? dec.convert(value) : value) | ||
| return super.get(field) | ||
| } | ||
| } | ||
| }[name] | ||
| return { | ||
| ctor: Ctor, | ||
| convert(v) { | ||
| if (!v) return | ||
| if (v.constructor === Ctor) return v | ||
| return new Ctor(v) | ||
| } | ||
| } | ||
| } | ||
| clone(opts) { | ||
@@ -1227,3 +1308,3 @@ return new Registry({ | ||
| const dec = new OperatorDeclaration({operator: op, leftType, rightType, returnType, handler}) | ||
| if (dec.hasPlaceholder() && !(rightType.hasPlaceholder() && leftType.hasPlaceholder())) { | ||
| if (dec.hasPlaceholderType && !(rightType.hasPlaceholderType && leftType.hasPlaceholderType)) { | ||
| throw new Error( | ||
@@ -1305,32 +1386,51 @@ `Operator overload with placeholders must use them in both left and right types: ${sig}` | ||
| #variables | ||
| #constants | ||
| #context | ||
| #contextObj | ||
| #contextMap | ||
| #convertCache | ||
| constructor(registry, context) { | ||
| this.#variables = registry.variables | ||
| this.#constants = registry.constants | ||
| if (context === undefined || context === null) return | ||
| if (typeof context !== 'object') throw new EvaluationError('Context must be an object') | ||
| this.#context = context | ||
| if (context instanceof Map) this.getValue = this.#getValueFromMap | ||
| else this.getValue = this.#getValueFromObject | ||
| if (context instanceof Map) this.#contextMap = context | ||
| else this.#contextObj = context | ||
| } | ||
| #getValueFromObject(key) { | ||
| const v = this.#context[key] | ||
| if (v !== undefined) return v | ||
| return this.#constants.get(key) | ||
| getType(name) { | ||
| return this.#variables.get(name)?.type | ||
| } | ||
| #getValueFromMap(key) { | ||
| const v = this.#context.get(key) | ||
| if (v !== undefined) return v | ||
| return this.#variables.get(key) | ||
| getValue(key) { | ||
| return ( | ||
| this.#convertCache?.get(key) || | ||
| (this.#contextMap ? this.#contextMap.get(key) : this.#contextObj?.[key]) | ||
| ) | ||
| } | ||
| getType(name) { | ||
| return this.#variables.get(name)?.type | ||
| getVariable(name) { | ||
| return this.#variables.get(name) | ||
| } | ||
| getValue(name) { | ||
| return this.#constants.get(name) | ||
| getCheckedValue(ev, ast) { | ||
| const value = this.getValue(ast.args) | ||
| if (value === undefined) throw new ev.Error(`Unknown variable: ${ast.args}`, ast) | ||
| const valueType = ev.debugType(value) | ||
| switch (ast.checkedType) { | ||
| case valueType: | ||
| case dynType: | ||
| return value | ||
| default: | ||
| if (ast.checkedType.matches(valueType)) return value | ||
| // Convert plain objects to typed instances when a convert function is registered | ||
| if (ast.checkedType.kind === 'message' && valueType.kind === 'map') { | ||
| const converted = ev.objectTypes.get(ast.checkedType.name)?.convert?.(value) | ||
| if (converted) | ||
| return ((this.#convertCache ??= new Map()).set(ast.args, converted), converted) | ||
| } | ||
| } | ||
| throw new ev.Error( | ||
| `Variable '${ast.args}' is not of type '${ast.checkedType}', got '${valueType}'`, | ||
| ast | ||
| ) | ||
| } | ||
@@ -1359,3 +1459,3 @@ | ||
| reuse(parent) { | ||
| if (!this.async) return (this.#parent = parent) && this | ||
| if (!this.async) return ((this.#parent = parent), this) | ||
| const ctx = new OverlayContext(parent, this.iterVar, this.iterType) | ||
@@ -1366,4 +1466,22 @@ ctx.accuType = this.accuType | ||
| setIterValue(v) { | ||
| return ((this.iterValue = v), this) | ||
| setIterValue(v, ev, ast) { | ||
| const valueType = ev.debugType(v) | ||
| switch (this.iterType) { | ||
| case valueType: | ||
| case dynType: | ||
| return ((this.iterValue = v), this) | ||
| default: | ||
| if (this.iterType.matches(valueType)) return ((this.iterValue = v), this) | ||
| // Convert plain objects to typed instances when a convert function is registered | ||
| if (this.iterType.kind === 'message' && valueType.kind === 'map') { | ||
| const converted = ev.objectTypes.get(this.iterType.name)?.convert?.(v) | ||
| if (converted) return ((this.iterValue = converted), this) | ||
| } | ||
| } | ||
| throw new ev.Error( | ||
| `Variable '${this.iterVar}' is not of type '${this.iterType}', got '${valueType}'`, | ||
| ast | ||
| ) | ||
| } | ||
@@ -1383,2 +1501,17 @@ | ||
| getCheckedValue(ev, ast) { | ||
| if (this.iterVar === ast.args) return this.iterValue | ||
| return this.#parent.getCheckedValue(ev, ast) | ||
| } | ||
| getVariable(name) { | ||
| if (this.iterVar === name) return new VariableDeclaration(name, this.iterType) | ||
| return this.#parent.getVariable(name) | ||
| } | ||
| setConverted(name, value) { | ||
| return this.iterVar === name ? (this.iterValue = value) : this.#parent.setConverted(name, value) | ||
| } | ||
| // getVariable makes this obsolete | ||
| getType(key) { | ||
@@ -1385,0 +1518,0 @@ return this.iterVar === key ? this.iterType : this.#parent.getType(key) |
@@ -54,4 +54,7 @@ import {TypeError, EvaluationError} from './errors.js' | ||
| if (customType.fields) { | ||
| const keyName = ast.op === '.' || ast.op === '.?' ? ast.args[1] : undefined | ||
| if (keyName) { | ||
| let keyName | ||
| if (ast.op === '.' || ast.op === '.?') keyName = ast.args[1] | ||
| else if (ast.args[1].op === 'value') keyName = ast.args[1].args | ||
| if (typeof keyName === 'string') { | ||
| const fieldType = customType.fields[keyName] | ||
@@ -72,3 +75,3 @@ if (fieldType) return fieldType | ||
| formatType(type) { | ||
| if (!type.hasPlaceholder()) return type.name | ||
| if (!type.hasPlaceholderType) return type.name | ||
| return type.templated(this.registry, toDynTypeBinding).name | ||
@@ -75,0 +78,0 @@ } |
+6
-6
| { | ||
| "name": "@marcbachmann/cel-js", | ||
| "version": "7.4.0", | ||
| "version": "7.5.0", | ||
| "description": "A lightweight Common Expression Language (CEL) implementation in JavaScript with zero dependencies", | ||
@@ -72,12 +72,12 @@ "keywords": [ | ||
| "service": "drone", | ||
| "commit": "74bb594614b78ef4b8b47b1b4c344f6848e9fd02", | ||
| "build": "235", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/235", | ||
| "commit": "0a47f533eb9b3772529f1a829b2bfbe957b654bb", | ||
| "build": "241", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/241", | ||
| "branch": "main", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/235", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/241", | ||
| "isPr": false, | ||
| "slug": "marcbachmann/cel-js", | ||
| "root": "/drone/src", | ||
| "date": "2026-02-11T23:27:43.430Z" | ||
| "date": "2026-02-16T17:48:36.155Z" | ||
| } | ||
| } |
+52
-4
@@ -73,3 +73,9 @@ # @marcbachmann/cel-js [](https://www.npmjs.com/package/@marcbachmann/cel-js) [](https://opensource.org/licenses/MIT) | ||
| const env = new Environment() | ||
| .registerVariable('user', 'map') | ||
| .registerVariable('skipAgeCheck', 'bool') | ||
| .registerVariable('user', { | ||
| schema: { | ||
| email: 'string', | ||
| age: 'int', | ||
| } | ||
| }) | ||
| .registerConstant('minAge', 'int', 18n) | ||
@@ -80,4 +86,4 @@ .registerFunction('isAdult(int): bool', age => age >= 18n) | ||
| // Type-checked evaluation with constant | ||
| env.evaluate('isAdult(user.age) && user.age >= minAge', { | ||
| user: {age: 25n} | ||
| env.evaluate('isAdult(user.age) && (user.age >= minAge || skipAgeCheck)', { | ||
| user: {age: 25n}, skipAgeCheck: true | ||
| }) | ||
@@ -140,3 +146,3 @@ | ||
| - **`registerConstant(name, type, value)`** - Provide immutable values without passing them in context | ||
| - **`clone()`** - Create a fast, isolated copy that stops the parent from registering more entries | ||
| - **`clone()`** - Create an isolated copy. Call that stops the parent from registering more entries. | ||
| - **`hasVariable(name)`** - Check if variable is registered | ||
@@ -155,2 +161,17 @@ - **`parse(expression)`** - Parse expression for reuse | ||
| env.registerVariable({name: 'user', type: 'map', description: 'The current user'}) | ||
| // Passing a schema property will implicitly create a custom type and | ||
| // convert objects/maps to a new class instance. Those values then behave similar like | ||
| // explicit types that are created using a constructor. | ||
| env.registerVariable({ | ||
| name: 'user', | ||
| schema: { | ||
| email: 'string', | ||
| age: 'int', | ||
| profile: { | ||
| tags: 'list<string>', | ||
| avatar: 'string' | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
@@ -160,2 +181,29 @@ | ||
| #### `registerType` signatures | ||
| ```javascript | ||
| // Name + constructor class | ||
| // when fields are not provided, own properties are accessible automatically | ||
| env.registerType('Vector', Vector) | ||
| // Name + object with constructor and field types | ||
| env.registerType('Vector', {ctor: Vector, fields: {x: 'double', y: 'double'}}) | ||
| // Name + object with fields only (auto-generates a wrapper class and convert function) | ||
| env.registerType('Vector', {fields: {x: 'double', y: 'double'}}) | ||
| // Name + object with nested schema (registers nested types automatically) | ||
| env.registerType('Vector', {schema: {x: 'double', y: 'double'}}) | ||
| // Single object with name and schema | ||
| env.registerType({name: 'Vector', schema: {x: 'double', y: 'double'}}) | ||
| // Single object with constructor (name inferred from constructor) | ||
| env.registerType({ctor: Vector, fields: {x: 'double', y: 'double'}}) | ||
| ``` | ||
| When `fields` or `schema` is provided without a `ctor`, an internal wrapper class is auto-generated and plain objects are automatically converted at runtime. A custom `convert` function can be passed to override this default conversion. | ||
| When using the `schema` declaration, we're creating a new Map instance for the specific type when retrieving the values by variable from a context object. | ||
| #### `registerFunction` signatures | ||
@@ -162,0 +210,0 @@ |
199291
3.56%4991
2.8%705
7.31%