UNPKG

@bufbuild/cel

Version:

A CEL evaluator for ECMAScript

527 lines (526 loc) 15.9 kB
// Copyright 2024-2025 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { EvalAttr } from "./planner.js"; import { isCelError, celError, } from "./error.js"; import { accessByIndex, accessByName, isSet } from "./field.js"; import { isCelUint } from "./uint.js"; import { CelScalar, celType, listType, mapType, objectType, } from "./type.js"; function attrAccess(factory, vars, obj, accAttr) { const val = accAttr.resolve(vars); if (val === undefined) { return undefined; } if (isCelError(val)) { return val; } const access = factory.newAccess(accAttr.id, val, accAttr.isOptional()); return access.access(vars, obj); } function attrIsPresent(factory, vars, obj, accAttr) { const val = accAttr.resolve(vars); if (val === undefined) { return false; } if (isCelError(val)) { return val; } const access = factory.newAccess(accAttr.id, val, accAttr.isOptional()); return access.isPresent(vars, obj); } function attrAccessIfPresent(factory, vars, obj, accAttr, presenceOnly) { const val = accAttr.resolve(vars); if (val === undefined) { return undefined; } if (isCelError(val)) { return val; } const access = factory.newAccess(accAttr.id, val, accAttr.isOptional()); return access.accessIfPresent(vars, obj, presenceOnly); } function applyAccesses(vars, obj, accesses) { if (accesses.length === 0) { return obj; } let cur = obj; for (const access of accesses) { const result = access.access(vars, cur); if (result === undefined) { return undefined; } if (isCelError(result)) { return result; } cur = result; } return cur; } class AbsoluteAttr { constructor(id, nsNames, accesses_, registry, factory) { this.id = id; this.nsNames = nsNames; this.accesses_ = accesses_; this.registry = registry; this.factory = factory; if (nsNames.length === 0) { throw new Error("No names provided"); } } isOptional() { return false; } addAccess(access) { this.accesses_.push(access); } candidateNames() { return this.nsNames; } accesses() { return this.accesses_; } access(vars, obj) { return attrAccess(this.factory, vars, obj, this); } isPresent(vars, obj) { return attrIsPresent(this.factory, vars, obj, this); } accessIfPresent(vars, obj, presenceOnly) { return attrAccessIfPresent(this.factory, vars, obj, this, presenceOnly); } resolve(vars) { for (const name of this.nsNames) { const raw = vars.resolve(name); if (raw !== undefined) { if (isCelError(raw)) { return raw; } return applyAccesses(vars, raw, this.accesses_); } const typ = this.findIdent(name); if (typ !== undefined && this.accesses_.length === 0) { return typ; } } return undefined; } findIdent(ident) { const desc = this.registry.getMessage(ident); if (desc) { return objectType(desc); } // Must be an enum if (ident.indexOf(".") > 1) { const lastDot = ident.lastIndexOf("."); const enumName = ident.substring(0, lastDot); const valueName = ident.substring(lastDot + 1); const descEnum = this.registry.getEnum(enumName); if (descEnum) { const enumValue = descEnum.values.find((v) => v.name === valueName); if (enumValue) { return BigInt(enumValue.number); } } } switch (ident) { case "int": return CelScalar.INT; case "uint": return CelScalar.UINT; case "double": return CelScalar.DOUBLE; case "bool": return CelScalar.BOOL; case "string": return CelScalar.STRING; case "bytes": return CelScalar.BYTES; case "list": return listType(CelScalar.DYN); case "map": return mapType(CelScalar.DYN, CelScalar.DYN); case "null_type": return CelScalar.NULL; case "type": return CelScalar.TYPE; default: return undefined; } } } class ConditionalAttr { constructor(id, cond, t, f, factory) { this.id = id; this.cond = cond; this.t = t; this.f = f; this.factory = factory; } isOptional() { return false; } addAccess(access) { this.t.addAccess(access); this.f.addAccess(access); } access(vars, obj) { return attrAccess(this.factory, vars, obj, this); } isPresent(vars, obj) { return attrIsPresent(this.factory, vars, obj, this); } accessIfPresent(vars, obj, presenceOnly) { return attrAccessIfPresent(this.factory, vars, obj, this, presenceOnly); } resolve(vars) { const cond = this.cond.eval(vars); switch (true) { case cond === true: return this.t.resolve(vars); case cond === false: return this.f.resolve(vars); case isCelError(cond): return cond; } return celError(`found no matching overload for _?_:_ applied to '(${celType(cond).name})'`, this.id); } } class MaybeAttr { constructor(id, attrs, factory) { this.id = id; this.attrs = attrs; this.factory = factory; } isOptional() { return false; } addAccess(access) { const augmentedNames = []; let str; if (access instanceof StringAccess) { str = access.name; } // Add the access to all existing attributes. for (const attr of this.attrs) { if (str !== undefined && attr.accesses().length === 0) { for (const name of attr.candidateNames()) { augmentedNames.push(name + "." + str); } } attr.addAccess(access); } if (augmentedNames.length > 0) { // Next, ensure the most specific variable / type reference is searched first. const newAttr = this.factory.createAbsolute(this.id, augmentedNames); // Insert it as the first attribute to search. this.attrs.unshift(newAttr); } } access(vars, obj) { return attrAccess(this.factory, vars, obj, this); } isPresent(vars, obj) { return attrIsPresent(this.factory, vars, obj, this); } accessIfPresent(vars, obj, presenceOnly) { return attrAccessIfPresent(this.factory, vars, obj, this, presenceOnly); } resolve(vars) { for (const attr of this.attrs) { const val = attr.resolve(vars); if (val !== undefined) { return val; } } return undefined; } } class RelativeAttr { constructor(id, operand, accesses_, factory) { this.id = id; this.operand = operand; this.accesses_ = accesses_; this.factory = factory; } isOptional() { return false; } addAccess(access) { this.accesses_.push(access); } access(vars, obj) { return attrAccess(this.factory, vars, obj, this); } isPresent(vars, obj) { return attrIsPresent(this.factory, vars, obj, this); } accessIfPresent(vars, obj, presenceOnly) { return attrAccessIfPresent(this.factory, vars, obj, this, presenceOnly); } resolve(vars) { const v = this.operand.eval(vars); if (isCelError(v)) { return v; } return applyAccesses(vars, v, this.accesses_); } } class StringAccess { constructor(id, name, celVal, optional) { this.id = id; this.name = name; this.celVal = celVal; this.optional = optional; } isOptional() { return this.optional; } access(_vars, obj) { const val = accessByName(obj, this.name); if (val === undefined && !this.optional) { return fieldNotFound(this.id, this.name); } return val; } isPresent(_vars, obj) { const set = isSet(obj, this.name); if (set === undefined) { return fieldNotFound(this.id, this.name); } return set; } accessIfPresent(_vars, obj, presenceOnly) { const val = accessByName(obj, this.name); if (val === undefined && !this.optional && !presenceOnly) { return fieldNotFound(this.id, this.name); } return val; } } class BoolAccess { constructor(id, index, optional) { this.id = id; this.index = index; this.optional = optional; } isOptional() { return this.optional; } access(_vars, obj) { if (obj === undefined) { return obj; } const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return fieldNotFound(this.id, this.index); } return raw; } isPresent(_vars, obj) { const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return fieldNotFound(this.id, this.index); } return true; } accessIfPresent(_vars, obj, _presenceOnly) { const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return fieldNotFound(this.id, this.index); } return raw; } } class NumAccess { constructor(id, index, celVal, optional) { this.id = id; this.index = index; this.celVal = celVal; this.optional = optional; } isOptional() { return this.optional; } access(_vars, obj) { if (obj === undefined) { return obj; } const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return indexOutOfBounds(this.id, this.index, -1); } return raw; } isPresent(_vars, obj) { const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return indexOutOfBounds(this.id, this.index, -1); } return true; } accessIfPresent(_vars, obj, _presenceOnly) { const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return indexOutOfBounds(this.id, this.index, -1); } return raw; } } class IntAccess { constructor(id, index, celVal, optional) { this.id = id; this.index = index; this.celVal = celVal; this.optional = optional; } access(_vars, obj) { if (obj === undefined) { return obj; } const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return indexOutOfBounds(this.id, Number(this.index), -1); } return raw; } isPresent(_vars, obj) { const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return indexOutOfBounds(this.id, Number(this.index), -1); } return true; } accessIfPresent(_vars, obj, _presenceOnly) { const raw = accessByIndex(obj, this.index); if (raw === undefined && !this.optional) { return indexOutOfBounds(this.id, Number(this.index), -1); } return raw; } isOptional() { return this.optional; } } export class ErrorAttr { constructor(id, error, opt) { this.id = id; this.error = error; this.opt = opt; } addAccess(_access) { // Do nothing } resolve(_vars) { return this.error; } isOptional() { return this.opt; } access(_vars, _obj) { return this.error; } isPresent(_vars, _obj) { return this.error; } accessIfPresent(_vars, _obj, _presenceOnly) { return this.error; } } class EvalAccess { constructor(id, key, factory, optional) { this.id = id; this.key = key; this.factory = factory; this.optional = optional; } access(vars, obj) { if (obj === undefined) { return obj; } const key = this.key.eval(vars); if (isCelError(key)) { return key; } const access = this.factory.newAccess(this.id, key, this.optional); return access.access(vars, obj); } isPresent(vars, obj) { if (obj === undefined) { return false; } const key = this.key.eval(vars); if (isCelError(key)) { return key; } const access = this.factory.newAccess(this.id, key, this.optional); return access.isPresent(vars, obj); } accessIfPresent(vars, obj, presenceOnly) { const key = this.key.eval(vars); if (isCelError(key)) { return key; } const access = this.factory.newAccess(this.id, key, this.optional); return access.accessIfPresent(vars, obj, presenceOnly); } isOptional() { return this.optional; } } export class ConcreteAttributeFactory { constructor(registry, container) { this.registry = registry; this.container = container; } createAbsolute(id, names) { return new AbsoluteAttr(id, names, [], this.registry, this); } createConditional(id, cond, t, f) { return new ConditionalAttr(id, cond, t, f, this); } createMaybe(id, name) { return new MaybeAttr(id, [this.createAbsolute(id, this.container.resolveCandidateNames(name))], this); } createRelative(id, operand) { return new RelativeAttr(id, operand, [], this); } newAccess(id, val, opt) { switch (typeof val) { case "boolean": return new BoolAccess(id, val, opt); case "number": return new NumAccess(id, val, val, opt); case "bigint": return new IntAccess(id, val, val, opt); case "string": return new StringAccess(id, val, val, opt); case "object": if (val instanceof EvalAttr) { return new EvalAccess(id, val, this, opt); } if (isCelUint(val)) { return new IntAccess(id, val.value, val, opt); } break; default: break; } return new ErrorAttr(id, celError("unsupported key type", id), opt); } } function fieldNotFound(id, name) { return celError(`field not found: ${String(name)}`, id); } function indexOutOfBounds(id, index, length) { return celError(`index ${index} out of bounds [0, ${length})`, id); }