@bufbuild/cel
Version:
A CEL evaluator for ECMAScript
600 lines (599 loc) • 20.7 kB
JavaScript
;
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.EvalFold = exports.EvalMap = exports.EvalList = exports.EvalObj = exports.EvalCall = exports.EvalAttr = exports.EvalConst = exports.EvalErr = exports.EvalHas = exports.Planner = void 0;
const access_js_1 = require("./access.js");
const activation_js_1 = require("./activation.js");
const opc = require("./gen/dev/cel/expr/operator_const.js");
const namespace_js_1 = require("./namespace.js");
const error_js_1 = require("./error.js");
const list_js_1 = require("./list.js");
const map_js_1 = require("./map.js");
const uint_js_1 = require("./uint.js");
const object_js_1 = require("./object.js");
const type_js_1 = require("./type.js");
class Planner {
constructor(functions, registry, namespace = namespace_js_1.Namespace.ROOT) {
this.functions = functions;
this.registry = registry;
this.namespace = namespace;
this.factory = new access_js_1.ConcreteAttributeFactory(this.registry, this.namespace);
}
plan(expr) {
const id = Number(expr.id);
switch (expr.exprKind.case) {
case "identExpr":
return new EvalAttr(this.factory.createMaybe(id, expr.exprKind.value.name), false);
case "constExpr":
return new EvalConst(id, this.constVal(expr.exprKind.value));
case "callExpr":
return this.planCall(id, expr.exprKind.value);
case "listExpr":
return this.planCreateList(id, expr.exprKind.value);
case "structExpr":
return this.planCreateStruct(id, expr.exprKind.value);
case "selectExpr":
return this.planSelect(id, expr.exprKind.value);
case "comprehensionExpr":
return this.planComprehension(id, expr.exprKind.value);
default:
return new EvalErr(id, "invalid expression");
}
}
planComprehension(id, value) {
if (value.accuInit === undefined ||
value.iterRange === undefined ||
value.loopCondition === undefined ||
value.loopStep === undefined ||
value.result === undefined) {
throw new Error("invalid comprehension");
}
const accu = this.plan(value.accuInit);
const iterRange = this.plan(value.iterRange);
const cond = this.plan(value.loopCondition);
const step = this.plan(value.loopStep);
const result = this.plan(value.result);
return new EvalFold(id, value.accuVar, value.iterVar, iterRange, accu, cond, step, result);
}
planSelect(id, expr) {
if (expr.operand === undefined) {
throw new Error("invalid select");
}
const operand = this.plan(expr.operand);
const attr = this.relativeAttr(id, operand, false);
const acc = this.factory.newAccess(id, expr.field, false);
if (acc instanceof access_js_1.ErrorAttr) {
throw new Error(`invalid select: ${acc.error.message}`);
}
if (expr.testOnly) {
return new EvalHas(id, attr, acc, expr.field);
}
attr.addAccess(acc);
return attr;
}
planCreateObj(id, expr) {
const typeName = this.resolveType(expr.messageName);
if (typeName === undefined) {
return new EvalErr(id, "unknown type: " + expr.messageName);
}
let optionals = undefined;
const keys = [];
const values = [];
for (let i = 0; i < expr.entries.length; i++) {
const entry = expr.entries[i];
if (entry.optionalEntry) {
if (optionals === undefined) {
optionals = new Array(expr.entries.length).fill(false);
}
optionals[i] = true;
}
switch (entry.keyKind.case) {
case "fieldKey":
keys.push(entry.keyKind.value);
break;
case "mapKey":
throw new Error("invalid entry");
default:
break;
}
if (entry.value === undefined) {
throw new Error("invalid entry");
}
values.push(this.plan(entry.value));
}
return new EvalObj(id, typeName, keys, values, optionals);
}
planCreateStruct(id, expr) {
if (expr.messageName !== "") {
return this.planCreateObj(id, expr);
}
let optionals = undefined;
const keys = [];
const values = [];
for (let i = 0; i < expr.entries.length; i++) {
const entry = expr.entries[i];
if (entry.optionalEntry) {
if (optionals === undefined) {
optionals = new Array(expr.entries.length).fill(false);
}
optionals[i] = true;
}
switch (entry.keyKind.case) {
case "fieldKey":
throw new Error("unimplemented");
case "mapKey":
keys.push(this.plan(entry.keyKind.value));
break;
default:
break;
}
if (entry.value === undefined) {
return new EvalErr(id, "map entry missing value");
}
values.push(this.plan(entry.value));
}
return new EvalMap(id, keys, values, optionals);
}
planCreateList(id, expr) {
const optionals = undefined;
if (expr.optionalIndices.length > 0) {
// set optionals to an array of booleans the same length as the list
const optionals = new Array(expr.elements.length).fill(false);
for (let i = 0; i < expr.optionalIndices.length; i++) {
const index = expr.optionalIndices[i];
if (index < 0 || index >= expr.elements.length) {
throw new Error("invalid optional index");
}
optionals[index] = true;
}
}
return new EvalList(id, expr.elements.map((arg) => this.plan(arg)), optionals);
}
planCall(id, call) {
// Check if the function is a qualified name.
if (call.target !== undefined) {
const qualName = toQualifiedName(call.target);
if (qualName !== undefined) {
const funcName = qualName + "." + call.function;
for (const candidate of this.namespace.resolveCandidateNames(funcName)) {
const func = this.functions.find(candidate);
if (func !== undefined) {
return new EvalCall(id, candidate, "", func, call.args.map((arg) => this.plan(arg)));
}
}
}
}
const args = call.target
? [this.plan(call.target), ...call.args.map((arg) => this.plan(arg))]
: call.args.map((arg) => this.plan(arg));
switch (call.function) {
case opc.INDEX:
return this.planCallIndex(call, args, false);
case opc.OPT_INDEX:
case opc.OPT_SELECT:
return this.planCallIndex(call, args, true);
case opc.CONDITIONAL:
return this.planCallConditional(id, call, args);
default:
break;
}
return new EvalCall(id, call.function, "", this.functions.find(call.function), args);
}
planCallConditional(id, _call, args) {
const cond = args[0];
const t = args[1];
const f = args[2];
const tAttr = this.relativeAttr(t.id, t, false);
const fAttr = this.relativeAttr(f.id, f, false);
return new EvalAttr(this.factory.createConditional(id, cond, tAttr, fAttr), false);
}
planCallIndex(_call, args, opt) {
const op = args[0];
const ind = args[1];
const attr = this.relativeAttr(op.id, op, false);
let acc;
if (ind instanceof EvalConst) {
acc = this.factory.newAccess(op.id, ind.value, opt);
}
else if (ind instanceof EvalAttr) {
acc = this.factory.newAccess(op.id, ind, opt);
}
else {
acc = this.relativeAttr(op.id, ind, opt);
}
attr.addAccess(acc);
return attr;
}
constVal(val) {
switch (val.constantKind.case) {
case "stringValue":
return val.constantKind.value;
case "bytesValue":
return val.constantKind.value;
case "doubleValue":
return val.constantKind.value;
case "boolValue":
return val.constantKind.value;
case "int64Value":
return val.constantKind.value;
case "uint64Value":
return (0, uint_js_1.celUint)(val.constantKind.value);
case "nullValue":
return null;
case undefined:
throw new Error("invalid constant");
default:
throw new Error(`unimplemented: ${val.constantKind.case}`);
}
}
relativeAttr(id, e, opt) {
if (e instanceof EvalAttr) {
return e;
}
return new EvalAttr(this.factory.createRelative(id, e), opt);
}
resolveType(name) {
for (const candidate of this.namespace.resolveCandidateNames(name)) {
if (this.isKnownType(candidate)) {
return candidate;
}
}
return undefined;
}
isKnownType(name) {
switch (name) {
case "google.protobuf.Value":
case "google.protobuf.Struct":
case "google.protobuf.ListValue":
case "google.protobuf.NullValue":
case "google.protobuf.BoolValue":
case "google.protobuf.UInt32Value":
case "google.protobuf.UInt64Value":
case "google.protobuf.Int32Value":
case "google.protobuf.Int64Value":
case "google.protobuf.FloatValue":
case "google.protobuf.DoubleValue":
case "google.protobuf.StringValue":
case "google.protobuf.BytesValue":
case "google.protobuf.Timestamp":
case "google.protobuf.Duration":
case "google.protobuf.Any":
return true;
default:
return this.registry.getMessage(name) !== undefined;
}
}
}
exports.Planner = Planner;
class EvalHas {
constructor(id, attr, access, field) {
this.id = id;
this.attr = attr;
this.access = access;
this.field = field;
}
eval(ctx) {
const raw = this.attr.resolve(ctx);
if (raw === undefined) {
return false;
}
if ((0, error_js_1.isCelError)(raw)) {
return raw;
}
return this.access.isPresent(ctx, raw);
}
}
exports.EvalHas = EvalHas;
class EvalErr {
constructor(id, msg) {
this.id = id;
this.msg = msg;
}
eval(_ctx) {
return (0, error_js_1.celError)(this.msg, this.id);
}
}
exports.EvalErr = EvalErr;
class EvalConst {
constructor(id, value) {
this.id = id;
this.value = value;
}
eval(_ctx) {
return this.value;
}
}
exports.EvalConst = EvalConst;
class EvalAttr {
constructor(attr, opt) {
this.attr = attr;
this.opt = opt;
this.id = attr.id;
}
access(vars, obj) {
return this.attr.access(vars, obj);
}
isPresent(vars, obj) {
return this.attr.isPresent(vars, obj);
}
accessIfPresent(vars, obj, presenceOnly) {
return this.attr.accessIfPresent(vars, obj, presenceOnly);
}
isOptional() {
return this.opt;
}
eval(ctx) {
const val = this.attr.resolve(ctx);
if (val === undefined) {
return (0, error_js_1.celError)("unresolved attribute", this.id);
}
if ((0, error_js_1.isCelError)(val)) {
return val;
}
return val;
}
resolve(vars) {
return this.attr.resolve(vars);
}
addAccess(acc) {
this.attr.addAccess(acc);
}
}
exports.EvalAttr = EvalAttr;
class EvalCall {
constructor(id, name, overload, call, args) {
this.id = id;
this.name = name;
this.overload = overload;
this.call = call;
this.args = args;
}
eval(ctx) {
if (this.call === undefined) {
return (0, error_js_1.celError)(`unbound function: ${this.name}`, this.id);
}
const argVals = this.args.map((x) => x.eval(ctx));
const result = this.call.dispatch(this.id, argVals);
if (result !== undefined) {
return result;
}
const vals = coerceToValues(argVals);
if ((0, error_js_1.isCelError)(vals)) {
return vals;
}
return (0, error_js_1.celError)(`found no matching overload for '${this.name}' applied to '(${vals
.map((x) => (0, type_js_1.celType)(x))
.map((x) => x.name)
.join(", ")})'`, this.id);
}
}
exports.EvalCall = EvalCall;
class EvalObj {
constructor(id, typeName, fields, values, optionals) {
this.id = id;
this.typeName = typeName;
this.fields = fields;
this.values = values;
this.optionals = optionals;
}
args() {
return this.values;
}
eval(ctx) {
const vals = coerceToValues(this.values.map((x) => x.eval(ctx)));
if ((0, error_js_1.isCelError)(vals)) {
return vals;
}
const obj = new Map();
for (let i = 0; i < vals.length; i++) {
if (obj.has(this.fields[i])) {
return (0, error_js_1.celError)(`map key conflict: ${this.fields[i]}`, this.id);
}
obj.set(this.fields[i], vals[i]);
}
try {
return (0, object_js_1.celObject)(this.typeName, obj);
}
catch (ex) {
if (ex instanceof Error) {
ex = ex.message;
}
return (0, error_js_1.celError)(`${ex}`, this.id);
}
}
}
exports.EvalObj = EvalObj;
class EvalList {
constructor(id, elems, _) {
this.id = id;
this.elems = elems;
}
eval(ctx) {
if (this.elems.length === 0) {
return list_js_1.EMPTY_LIST;
}
const first = this.elems[0].eval(ctx);
if ((0, error_js_1.isCelError)(first)) {
return first;
}
const elemVals = [first];
for (let i = 1; i < this.elems.length; i++) {
const elemVal = this.elems[i].eval(ctx);
if ((0, error_js_1.isCelError)(elemVal)) {
return elemVal;
}
elemVals.push(elemVal);
}
return (0, list_js_1.celList)(elemVals);
}
args() {
return this.elems;
}
}
exports.EvalList = EvalList;
class EvalMap {
constructor(id, keys, values, _) {
this.id = id;
this.keys = keys;
this.values = values;
}
args() {
return this.keys.concat(this.values);
}
eval(ctx) {
if (this.keys.length === 0) {
return map_js_1.EMPTY_MAP;
}
const entries = new Map();
const firstKey = this.mapKeyOrError(this.keys[0].eval(ctx));
if ((0, error_js_1.isCelError)(firstKey)) {
return firstKey;
}
const firstVal = this.values[0].eval(ctx);
if ((0, error_js_1.isCelError)(firstVal)) {
return firstVal;
}
if (typeof firstKey === "number" && !Number.isInteger(firstKey)) {
return unsupportedKeyType(this.id);
}
entries.set(firstKey, firstVal);
for (let i = 1; i < this.keys.length; i++) {
const key = this.mapKeyOrError(this.keys[i].eval(ctx));
if ((0, error_js_1.isCelError)(key)) {
return key;
}
const val = this.values[i].eval(ctx);
if ((0, error_js_1.isCelError)(val)) {
return val;
}
if (entries.has(key)) {
return (0, error_js_1.celError)(`map key conflict: ${key}`, this.id);
}
if (typeof key === "number" && !Number.isInteger(key)) {
return unsupportedKeyType(this.id);
}
entries.set(key, val);
}
return (0, map_js_1.celMap)(entries);
}
mapKeyOrError(key) {
switch (typeof key) {
case "boolean":
case "bigint":
case "string":
return key;
case "object":
if ((0, uint_js_1.isCelUint)(key)) {
return key;
}
return unsupportedKeyType(this.id);
case "number":
if (Number.isInteger(key)) {
return BigInt(key);
}
return unsupportedKeyType(this.id);
default:
return unsupportedKeyType(this.id);
}
}
}
exports.EvalMap = EvalMap;
class EvalFold {
constructor(id, accuVar, iterVar, iterRange, accu, cond, step, result) {
this.id = id;
this.accuVar = accuVar;
this.iterVar = iterVar;
this.iterRange = iterRange;
this.accu = accu;
this.cond = cond;
this.step = step;
this.result = result;
}
eval(ctx) {
const accuInit = this.accu.eval(ctx);
if ((0, error_js_1.isCelError)(accuInit)) {
return accuInit;
}
const accuCtx = new activation_js_1.VarActivation(this.accuVar, accuInit, ctx);
const iterRange = this.iterRange.eval(ctx);
if ((0, error_js_1.isCelError)(iterRange)) {
return iterRange;
}
let items = [];
if ((0, map_js_1.isCelMap)(iterRange)) {
items = Array.from(iterRange.keys());
}
else if ((0, list_js_1.isCelList)(iterRange)) {
items = Array.from(iterRange);
}
else {
return (0, error_js_1.celError)(`type mismatch: iterable vs ${(0, type_js_1.celType)(iterRange)}`, this.id);
}
// Fold the items.
for (const item of items) {
if ((0, error_js_1.isCelError)(item)) {
return item;
}
const iterCtx = new activation_js_1.VarActivation(this.iterVar, item, accuCtx);
const cond = this.cond.eval(iterCtx);
if ((0, error_js_1.isCelError)(cond)) {
return cond;
}
if (cond !== true) {
break;
}
// Update the result.
accuCtx.value = this.step.eval(iterCtx);
}
// Compute the result
return this.result.eval(accuCtx);
}
}
exports.EvalFold = EvalFold;
function toQualifiedName(expr) {
switch (expr.exprKind.case) {
case "identExpr":
return expr.exprKind.value.name;
case "selectExpr": {
if (expr.exprKind.value.testOnly ||
expr.exprKind.value.operand === undefined) {
return undefined;
}
const parent = toQualifiedName(expr.exprKind.value.operand);
if (parent === undefined) {
return undefined;
}
return parent + "." + expr.exprKind.value.field;
}
default:
return undefined;
}
}
function unsupportedKeyType(id) {
return (0, error_js_1.celError)(`unsupported key type`, id);
}
function coerceToValues(args) {
const errors = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if ((0, error_js_1.isCelError)(arg)) {
errors.push(arg);
}
}
if (errors.length > 0) {
return (0, error_js_1.celErrorMerge)(errors[0], ...errors.slice(1));
}
return args;
}