@bufbuild/cel
Version:
A CEL evaluator for ECMAScript
346 lines (345 loc) • 13.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.celObject = celObject;
const protobuf_1 = require("@bufbuild/protobuf");
const uint_js_1 = require("./uint.js");
const value_js_1 = require("./value.js");
const eval_js_1 = require("./eval.js");
const reflect_1 = require("@bufbuild/protobuf/reflect");
const list_js_1 = require("./list.js");
const map_js_1 = require("./map.js");
const wkt_1 = require("@bufbuild/protobuf/wkt");
const wire_1 = require("@bufbuild/protobuf/wire");
const type_js_1 = require("./type.js");
const wire_2 = require("@bufbuild/protobuf/wire");
/**
* Creates a new CelValue of the given type and with the given fields.
*
* Wrappers are converted to their corresponding scalars.
*/
function celObject(typeName, fields) {
const desc = (0, eval_js_1.getMsgDesc)(typeName);
const msg = (0, reflect_1.reflect)(desc);
for (const [k, v] of fields.entries()) {
const field = desc.fields.find((f) => f.name === k);
if (field === undefined) {
throw new Error(`Unknown field ${k} for ${typeName}`);
}
setMsgField(msg, field, v);
}
return (0, value_js_1.reflectMsgToCel)(msg);
}
function setMsgField(msg, field, v) {
switch (field.fieldKind) {
case "list":
if (!(0, list_js_1.isCelList)(v)) {
throw unexpectedTypeError(field, "list", v);
}
const list = (0, reflect_1.reflectList)(field, undefined, false);
copyList(list, v);
msg.set(field, list);
return;
case "map":
if (!(0, map_js_1.isCelMap)(v)) {
throw unexpectedTypeError(field, "map", v);
}
const map = (0, reflect_1.reflectMap)(field, undefined, false);
copyMap(map, v);
msg.set(field, map);
return;
case "enum":
msg.set(field, enumFromCel(field, v));
return;
case "message":
const msgVal = msgFromCel(field, v);
if (msgVal !== null) {
msg.set(field, msgVal);
}
return;
}
msg.set(field, scalarFromCel(field, field.scalar, v));
}
function copyList(list, cList) {
const field = list.field();
for (const v of cList) {
switch (field.listKind) {
case "enum":
list.add(enumFromCel(field, v));
break;
case "message":
list.add(msgFromCel(field, v));
break;
case "scalar":
list.add(scalarFromCel(field, field.scalar, v));
break;
}
}
}
function copyMap(map, cMap) {
const field = map.field();
for (const [k, v] of cMap.entries()) {
const key = scalarFromCel(field, field.mapKey, k);
switch (field.mapKind) {
case "enum":
map.set(key, enumFromCel(field, v));
break;
case "scalar":
map.set(key, scalarFromCel(field, field.scalar, v));
break;
case "message":
map.set(key, msgFromCel(field, v));
break;
}
}
}
function enumFromCel(field, v) {
if (typeof v !== "bigint") {
throw unexpectedTypeError(field, "int", v);
}
return intToInt32(v);
}
function msgFromCel(field, v) {
switch (field.message.typeName) {
case wkt_1.AnySchema.typeName:
if ((0, reflect_1.isReflectMessage)(v, wkt_1.AnySchema)) {
return v;
}
return (0, reflect_1.reflect)(wkt_1.AnySchema, anyFromCel(v));
case wkt_1.ValueSchema.typeName:
return (0, reflect_1.reflect)(wkt_1.ValueSchema, valueFromCel(v));
case wkt_1.StructSchema.typeName:
if (!(0, map_js_1.isCelMap)(v)) {
throw unexpectedTypeError(field, "map", v);
}
return (0, reflect_1.reflect)(wkt_1.StructSchema, structFromCel(v));
case wkt_1.ListValueSchema.typeName:
if (!(0, list_js_1.isCelList)(v)) {
throw unexpectedTypeError(field, "list", v);
}
return (0, reflect_1.reflect)(wkt_1.ListValueSchema, listValueFromCel(v));
}
if (v === null) {
return null;
}
if ((0, wkt_1.isWrapperDesc)(field.message)) {
const msg = (0, reflect_1.reflect)(field.message, undefined, false);
msg.set(field.message.fields[0], scalarFromCel(field.message.fields[0], field.message.fields[0].scalar, v));
return msg;
}
if (!(0, reflect_1.isReflectMessage)(v, field.message)) {
throw unexpectedTypeError(field, field.message.typeName, v);
}
return v;
}
/**
* Converts a CelValue to google.protobuf.Any.
*
* While the CEL spec doesn't say anything about converting
* CEL values to types, there a couple conformance tests around
* WKTs that expect this behavior. The go implementation also handles
* converting all CEL types except for type.
*/
function anyFromCel(v) {
switch (typeof v) {
case "string":
return (0, wkt_1.anyPack)(wkt_1.StringValueSchema, (0, protobuf_1.create)(wkt_1.StringValueSchema, { value: v }));
case "boolean":
return (0, wkt_1.anyPack)(wkt_1.BoolValueSchema, (0, protobuf_1.create)(wkt_1.BoolValueSchema, { value: v }));
case "bigint":
return (0, wkt_1.anyPack)(wkt_1.Int64ValueSchema, (0, protobuf_1.create)(wkt_1.Int64ValueSchema, { value: v }));
case "number":
return (0, wkt_1.anyPack)(wkt_1.DoubleValueSchema, (0, protobuf_1.create)(wkt_1.DoubleValueSchema, { value: v }));
default:
switch (true) {
case v instanceof Uint8Array:
return (0, wkt_1.anyPack)(wkt_1.BytesValueSchema, (0, protobuf_1.create)(wkt_1.BytesValueSchema, { value: v }));
case v == null:
return (0, wkt_1.anyPack)(wkt_1.ValueSchema, (0, protobuf_1.create)(wkt_1.ValueSchema, {
kind: { case: "nullValue", value: wkt_1.NullValue.NULL_VALUE },
}));
case (0, list_js_1.isCelList)(v):
return (0, wkt_1.anyPack)(wkt_1.ListValueSchema, listValueFromCel(v));
case (0, map_js_1.isCelMap)(v):
return (0, wkt_1.anyPack)(wkt_1.StructSchema, structFromCel(v));
case (0, uint_js_1.isCelUint)(v):
return (0, wkt_1.anyPack)(wkt_1.UInt64ValueSchema, (0, protobuf_1.create)(wkt_1.UInt64ValueSchema, { value: v.value }));
case (0, reflect_1.isReflectMessage)(v):
return (0, wkt_1.anyPack)(v.desc, v.message);
default:
// Only CelType is left.
throw new Error(`type cannot be converted ${wkt_1.AnySchema}`);
}
}
}
/**
* Converts a CelValue to google.protobuf.Value.
*
* CEL defines conversion to/from JSON. Since Value represents the JSON type
* in CEL and protobuf, we can use the same logic to convert them.
*
* Ref: https://github.com/google/cel-spec/blob/master/doc/langdef.md#json-data-conversion
*/
function valueFromCel(v) {
const value = (0, protobuf_1.create)(wkt_1.ValueSchema);
switch (typeof v) {
case "string":
value.kind = { case: "stringValue", value: v };
break;
case "boolean":
value.kind = { case: "boolValue", value: v };
break;
case "bigint":
if (v > BigInt(Number.MAX_SAFE_INTEGER) ||
v < BigInt(Number.MIN_SAFE_INTEGER)) {
value.kind = { case: "stringValue", value: v.toString() };
}
else {
value.kind = { case: "numberValue", value: Number(v) };
}
break;
case "number":
if (Number.isNaN(v)) {
value.kind = { case: "stringValue", value: "NaN" };
}
else if (v === Number.POSITIVE_INFINITY) {
value.kind = { case: "stringValue", value: "Infinity" };
}
else if (v === Number.NEGATIVE_INFINITY) {
value.kind = { case: "stringValue", value: "-Infinity" };
}
else {
value.kind = { case: "numberValue", value: Number(v) };
}
break;
default:
switch (true) {
case v instanceof Uint8Array:
value.kind = { case: "stringValue", value: (0, wire_1.base64Encode)(v) };
break;
case v == null:
value.kind = { case: "nullValue", value: wkt_1.NullValue.NULL_VALUE };
break;
case (0, list_js_1.isCelList)(v):
value.kind = { case: "listValue", value: listValueFromCel(v) };
break;
case (0, map_js_1.isCelMap)(v):
value.kind = { case: "structValue", value: structFromCel(v) };
break;
case (0, uint_js_1.isCelUint)(v):
if (v.value > BigInt(Number.MAX_SAFE_INTEGER)) {
value.kind = { case: "stringValue", value: v.value.toString() };
}
else {
value.kind = { case: "numberValue", value: Number(v.value) };
}
break;
case (0, reflect_1.isReflectMessage)(v):
// We can skip the intermediary step, but that will require
// us to reimplement all of toJson just with a different result type.
return (0, protobuf_1.fromJson)(wkt_1.ValueSchema, (0, protobuf_1.toJson)(v.desc, v.message));
default:
// Only CelType is left which is not supported.
throw new Error(`type cannot be converted ${wkt_1.ValueSchema}`);
}
}
return value;
}
function listValueFromCel(list) {
const listValue = (0, protobuf_1.create)(wkt_1.ListValueSchema);
for (const v of list) {
listValue.values.push(valueFromCel(v));
}
return listValue;
}
function structFromCel(map) {
const struct = (0, protobuf_1.create)(wkt_1.StructSchema);
for (const [k, v] of map.entries()) {
if (typeof k !== "string") {
throw new Error(`Invalid key type: ${typeof k} for google.protobuf.Struct, expected string`);
}
struct.fields[k] = valueFromCel(v);
}
return struct;
}
function scalarFromCel(field, type, v) {
if ((0, uint_js_1.isCelUint)(v)) {
v = v.value;
}
switch (type) {
case protobuf_1.ScalarType.UINT32:
case protobuf_1.ScalarType.FIXED32:
if (typeof v !== "bigint") {
throw unexpectedTypeError(field, "int", v);
}
return intToUint32(v);
case protobuf_1.ScalarType.INT32:
case protobuf_1.ScalarType.SINT32:
case protobuf_1.ScalarType.SFIXED32:
if (typeof v !== "bigint") {
throw unexpectedTypeError(field, "int", v);
}
return intToInt32(v);
case protobuf_1.ScalarType.UINT64:
case protobuf_1.ScalarType.FIXED64:
case protobuf_1.ScalarType.INT64:
case protobuf_1.ScalarType.SINT64:
case protobuf_1.ScalarType.SFIXED64:
if (typeof v !== "bigint") {
throw unexpectedTypeError(field, "int", v);
}
return v;
case protobuf_1.ScalarType.FLOAT:
if (typeof v !== "number") {
throw unexpectedTypeError(field, "double", v);
}
return Math.fround(v);
case protobuf_1.ScalarType.DOUBLE:
if (typeof v !== "number") {
throw unexpectedTypeError(field, "double", v);
}
return v;
case protobuf_1.ScalarType.STRING:
if (typeof v !== "string") {
throw unexpectedTypeError(field, "string", v);
}
return v;
case protobuf_1.ScalarType.BOOL:
if (typeof v !== "boolean") {
throw unexpectedTypeError(field, "bool", v);
}
return v;
case protobuf_1.ScalarType.BYTES:
if (!(v instanceof Uint8Array)) {
throw unexpectedTypeError(field, "bytes", v);
}
return v;
}
}
function unexpectedTypeError(field, expected, actValue) {
return new Error(`Expected ${expected} but got ${(0, type_js_1.celType)(actValue)} for ${field.parent}.${field.name}`);
}
function intToInt32(v) {
if (v < wire_2.INT32_MIN || v > wire_2.INT32_MAX) {
throw new Error("int32 out of range");
}
return Number(v);
}
function intToUint32(v) {
if (v < 0 || v > wire_2.UINT32_MAX) {
throw new Error("uint32 out of range");
}
return Number(v);
}