@bufbuild/cel
Version:
A CEL evaluator for ECMAScript
343 lines (342 loc) • 12.8 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.
import { create, fromJson, ScalarType, toJson, } from "@bufbuild/protobuf";
import { isCelUint } from "./uint.js";
import { reflectMsgToCel } from "./value.js";
import { getMsgDesc } from "./eval.js";
import { isReflectMessage, reflect, reflectList, reflectMap, } from "@bufbuild/protobuf/reflect";
import { isCelList } from "./list.js";
import { isCelMap } from "./map.js";
import { anyPack, AnySchema, BoolValueSchema, BytesValueSchema, DoubleValueSchema, Int64ValueSchema, isWrapperDesc, ListValueSchema, NullValue, StringValueSchema, StructSchema, UInt64ValueSchema, ValueSchema, } from "@bufbuild/protobuf/wkt";
import { base64Encode } from "@bufbuild/protobuf/wire";
import { celType } from "./type.js";
import { INT32_MAX, INT32_MIN, UINT32_MAX } from "@bufbuild/protobuf/wire";
/**
* Creates a new CelValue of the given type and with the given fields.
*
* Wrappers are converted to their corresponding scalars.
*/
export function celObject(typeName, fields) {
const desc = getMsgDesc(typeName);
const msg = 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 reflectMsgToCel(msg);
}
function setMsgField(msg, field, v) {
switch (field.fieldKind) {
case "list":
if (!isCelList(v)) {
throw unexpectedTypeError(field, "list", v);
}
const list = reflectList(field, undefined, false);
copyList(list, v);
msg.set(field, list);
return;
case "map":
if (!isCelMap(v)) {
throw unexpectedTypeError(field, "map", v);
}
const map = 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 AnySchema.typeName:
if (isReflectMessage(v, AnySchema)) {
return v;
}
return reflect(AnySchema, anyFromCel(v));
case ValueSchema.typeName:
return reflect(ValueSchema, valueFromCel(v));
case StructSchema.typeName:
if (!isCelMap(v)) {
throw unexpectedTypeError(field, "map", v);
}
return reflect(StructSchema, structFromCel(v));
case ListValueSchema.typeName:
if (!isCelList(v)) {
throw unexpectedTypeError(field, "list", v);
}
return reflect(ListValueSchema, listValueFromCel(v));
}
if (v === null) {
return null;
}
if (isWrapperDesc(field.message)) {
const msg = 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 (!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 anyPack(StringValueSchema, create(StringValueSchema, { value: v }));
case "boolean":
return anyPack(BoolValueSchema, create(BoolValueSchema, { value: v }));
case "bigint":
return anyPack(Int64ValueSchema, create(Int64ValueSchema, { value: v }));
case "number":
return anyPack(DoubleValueSchema, create(DoubleValueSchema, { value: v }));
default:
switch (true) {
case v instanceof Uint8Array:
return anyPack(BytesValueSchema, create(BytesValueSchema, { value: v }));
case v == null:
return anyPack(ValueSchema, create(ValueSchema, {
kind: { case: "nullValue", value: NullValue.NULL_VALUE },
}));
case isCelList(v):
return anyPack(ListValueSchema, listValueFromCel(v));
case isCelMap(v):
return anyPack(StructSchema, structFromCel(v));
case isCelUint(v):
return anyPack(UInt64ValueSchema, create(UInt64ValueSchema, { value: v.value }));
case isReflectMessage(v):
return anyPack(v.desc, v.message);
default:
// Only CelType is left.
throw new Error(`type cannot be converted ${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 = create(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: base64Encode(v) };
break;
case v == null:
value.kind = { case: "nullValue", value: NullValue.NULL_VALUE };
break;
case isCelList(v):
value.kind = { case: "listValue", value: listValueFromCel(v) };
break;
case isCelMap(v):
value.kind = { case: "structValue", value: structFromCel(v) };
break;
case 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 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 fromJson(ValueSchema, toJson(v.desc, v.message));
default:
// Only CelType is left which is not supported.
throw new Error(`type cannot be converted ${ValueSchema}`);
}
}
return value;
}
function listValueFromCel(list) {
const listValue = create(ListValueSchema);
for (const v of list) {
listValue.values.push(valueFromCel(v));
}
return listValue;
}
function structFromCel(map) {
const struct = create(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 (isCelUint(v)) {
v = v.value;
}
switch (type) {
case ScalarType.UINT32:
case ScalarType.FIXED32:
if (typeof v !== "bigint") {
throw unexpectedTypeError(field, "int", v);
}
return intToUint32(v);
case ScalarType.INT32:
case ScalarType.SINT32:
case ScalarType.SFIXED32:
if (typeof v !== "bigint") {
throw unexpectedTypeError(field, "int", v);
}
return intToInt32(v);
case ScalarType.UINT64:
case ScalarType.FIXED64:
case ScalarType.INT64:
case ScalarType.SINT64:
case ScalarType.SFIXED64:
if (typeof v !== "bigint") {
throw unexpectedTypeError(field, "int", v);
}
return v;
case ScalarType.FLOAT:
if (typeof v !== "number") {
throw unexpectedTypeError(field, "double", v);
}
return Math.fround(v);
case ScalarType.DOUBLE:
if (typeof v !== "number") {
throw unexpectedTypeError(field, "double", v);
}
return v;
case ScalarType.STRING:
if (typeof v !== "string") {
throw unexpectedTypeError(field, "string", v);
}
return v;
case ScalarType.BOOL:
if (typeof v !== "boolean") {
throw unexpectedTypeError(field, "bool", v);
}
return v;
case 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 ${celType(actValue)} for ${field.parent}.${field.name}`);
}
function intToInt32(v) {
if (v < INT32_MIN || v > INT32_MAX) {
throw new Error("int32 out of range");
}
return Number(v);
}
function intToUint32(v) {
if (v < 0 || v > UINT32_MAX) {
throw new Error("uint32 out of range");
}
return Number(v);
}