UNPKG

@bufbuild/cel

Version:

A CEL evaluator for ECMAScript

343 lines (342 loc) 12.8 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 { 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); }