From d2eee0a69c6a3bf7e0dd84d5a64535fc3175848e Mon Sep 17 00:00:00 2001 From: Sri Krishna Date: Tue, 13 Jan 2026 10:55:27 +0530 Subject: [PATCH 1/2] Expand `celType` to account for unwrapped values --- packages/cel/src/func.ts | 45 +-------------- packages/cel/src/type.ts | 116 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 45 deletions(-) diff --git a/packages/cel/src/func.ts b/packages/cel/src/func.ts index 53b932d..fee51bd 100644 --- a/packages/cel/src/func.ts +++ b/packages/cel/src/func.ts @@ -12,14 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { isCelList } from "./list.js"; import { type CelType, type CelValueTuple, - CelScalar, - isCelType, type CelValue, type CelInput, + isTypeOf, } from "./type.js"; import { type CelResult, @@ -28,9 +26,6 @@ import { celErrorMerge, celError, } from "./error.js"; -import { isCelMap } from "./map.js"; -import { isCelUint } from "./uint.js"; -import { isReflectMessage } from "@bufbuild/protobuf/reflect"; import { unwrapAny, toCel } from "./value.js"; const privateFuncSymbol = Symbol.for("@bufbuild/cel/func"); @@ -129,7 +124,7 @@ class Func implements CelFunc { const checkedVals = []; for (let i = 0; i < vals.length; i++) { const celValue = unwrapAny(vals[i]); - if (!isOfType(celValue, overload.parameters[i])) { + if (!isTypeOf(celValue, overload.parameters[i])) { break; } checkedVals.push(celValue); @@ -239,42 +234,6 @@ export class OrderedDispatcher implements Dispatcher { } } -function isOfType( - val: CelValue, - type: T, -): val is CelValue { - switch (type.kind) { - case "list": - return isCelList(val); - case "map": - return isCelMap(val); - case "object": - return isReflectMessage(val, type.desc); - case "type": - return isCelType(val); - case "scalar": - switch (type) { - case CelScalar.DYN: - return true; - case CelScalar.INT: - return typeof val === "bigint"; - case CelScalar.UINT: - return isCelUint(val); - case CelScalar.BOOL: - return typeof val === "boolean"; - case CelScalar.DOUBLE: - return typeof val === "number"; - case CelScalar.NULL: - return val === null; - case CelScalar.STRING: - return typeof val === "string"; - case CelScalar.BYTES: - return val instanceof Uint8Array; - } - } - return false; -} - function unwrapResults(args: CelResult[]) { const errors: CelError[] = []; const vals: V[] = []; diff --git a/packages/cel/src/type.ts b/packages/cel/src/type.ts index fdb1f04..046800a 100644 --- a/packages/cel/src/type.ts +++ b/packages/cel/src/type.ts @@ -22,7 +22,27 @@ import { type ReflectMap, type ReflectMessage, } from "@bufbuild/protobuf/reflect"; -import { TimestampSchema, DurationSchema } from "@bufbuild/protobuf/wkt"; +import { + TimestampSchema, + DurationSchema, + AnySchema, + type Any, + DoubleValueSchema, + FloatValueSchema, + UInt64ValueSchema, + UInt32ValueSchema, + Int32ValueSchema, + Int64ValueSchema, + StringValueSchema, + BytesValueSchema, + BoolValueSchema, + StructSchema, + ListValueSchema, + NullValueSchema, + ValueSchema, + anyUnpack, + type Value, +} from "@bufbuild/protobuf/wkt"; const privateSymbol = Symbol.for("@bufbuild/cel/type"); @@ -283,7 +303,7 @@ export function celType(v: CelValue): CelType { case v instanceof Uint8Array: return CelScalar.BYTES; case isReflectMessage(v): - return objectType(v.desc); + return celTypeOfMessage(v); case isCelList(v): return listType(CelScalar.DYN); case isCelMap(v): @@ -308,6 +328,98 @@ export function isCelType(v: unknown): v is CelType { return typeof v === "object" && v !== null && isObjectCelType(v); } +/** + * Returns true if v satisfies type t. + * + * For lists and maps + */ +export function isTypeOf(val: CelValue, type: CelType): boolean { + if (type === CelScalar.DYN) { + return true; + } + const valType = celType(val); + if (valType.kind !== type.kind) { + return false; + } + if (type.kind === "scalar") { + return valType === type; + } + if (type.kind === "object") { + return valType.name === type.name; + } + return true; +} + export function isObjectCelType(v: NonNullable): v is CelType { return privateSymbol in v; } + +function celTypeOfMessage(v: ReflectMessage): CelType { + let typeName = v.desc.typeName; + if (isReflectMessageAny(v)) { + typeName = typeUrlToName(v.message.typeUrl); + if (typeName === ValueSchema.typeName) { + const value = anyUnpack(v.message, ValueSchema); + return value ? valueToType(value) : CelScalar.NULL; + } + } + switch (typeName) { + case FloatValueSchema.typeName: + case DoubleValueSchema.typeName: + return CelScalar.DOUBLE; + case UInt64ValueSchema.typeName: + case UInt32ValueSchema.typeName: + return CelScalar.UINT; + case Int32ValueSchema.typeName: + case Int64ValueSchema.typeName: + return CelScalar.INT; + case StringValueSchema.typeName: + return CelScalar.STRING; + case BytesValueSchema.typeName: + return CelScalar.BYTES; + case BoolValueSchema.typeName: + return CelScalar.BOOL; + case StructSchema.typeName: + return mapType(CelScalar.STRING, CelScalar.DYN); + case ListValueSchema.typeName: + return listType(CelScalar.DYN); + case NullValueSchema.typeName: + return CelScalar.NULL; + case ValueSchema.typeName: + return valueToType(v.message as Value); + } + return objectType(v.desc); +} + +function valueToType(v: Value): CelType { + switch (v.kind.case) { + case "boolValue": + return CelScalar.BOOL; + case "listValue": + return listType(CelScalar.DYN); + case "nullValue": + case undefined: + return CelScalar.NULL; + case "numberValue": + return CelScalar.DOUBLE; + case "stringValue": + return CelScalar.STRING; + case "structValue": + return mapType(CelScalar.STRING, CelScalar.DYN); + } +} + +function isReflectMessageAny( + v: ReflectMessage, +): v is ReflectMessage & { message: Any } { + return v.desc.typeName === AnySchema.typeName; +} + +function typeUrlToName(url: string): string { + const slash = url.lastIndexOf("/"); + const name = slash >= 0 ? url.substring(slash + 1) : url; + if (!name.length) { + throw new Error(`invalid type url: ${url}`); + } + return name; +} From 46d561e1c79544dfbe7548ee12e48242493b1cbd Mon Sep 17 00:00:00 2001 From: Sri Krishna Date: Wed, 14 Jan 2026 22:51:12 +0530 Subject: [PATCH 2/2] Fix message; Add tests --- packages/cel/src/type.test.ts | 163 +++++++++++++++++++++++++++++++--- packages/cel/src/type.ts | 33 ++++--- 2 files changed, 176 insertions(+), 20 deletions(-) diff --git a/packages/cel/src/type.test.ts b/packages/cel/src/type.test.ts index fa028bf..00a77ac 100644 --- a/packages/cel/src/type.test.ts +++ b/packages/cel/src/type.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { test } from "node:test"; +import { test, suite } from "node:test"; import { CelScalar, listType, @@ -22,18 +22,48 @@ import { type CelType, type CelInput, objectType, + celType, } from "./type.js"; -import { TimestampSchema, type Timestamp } from "@bufbuild/protobuf/wkt"; +import { + TimestampSchema, + type Timestamp, + anyPack, + Int32ValueSchema, + AnySchema, + Int64ValueSchema, + UInt32ValueSchema, + UInt64ValueSchema, + StringValueSchema, + BoolValueSchema, + BytesValueSchema, + DoubleValueSchema, + DurationSchema, + StructSchema, + ListValueSchema, + ValueSchema, + NullValue, + FloatValueSchema, +} from "@bufbuild/protobuf/wkt"; import { expectTypeOf } from "expect-type"; -import type { CelList } from "./list.js"; -import type { CelMap } from "./map.js"; -import type { CelUint } from "./uint.js"; -import type { - ReflectList, - ReflectMap, - ReflectMessage, +import { celList, type CelList, isCelList } from "./list.js"; +import { celMap, type CelMap, isCelMap } from "./map.js"; +import { celUint, type CelUint, isCelUint } from "./uint.js"; +import { + isReflectMessage, + reflect, + type ReflectList, + type ReflectMap, + type ReflectMessage, } from "@bufbuild/protobuf/reflect"; -import type { Message } from "@bufbuild/protobuf"; +import { + create, + type Message, + type DescMessage, + isMessage, + type MessageInitShape, +} from "@bufbuild/protobuf"; +import * as assert from "node:assert/strict"; +import { TestAllTypesSchema } from "@bufbuild/cel-spec/cel/expr/conformance/proto3/test_all_types_pb.js"; void test("CelTupleValue", () => { expectTypeOf< @@ -125,3 +155,116 @@ void test("CelValue", () => { | Message; expectTypeOf().toEqualTypeOf(); }); + +void suite("celType()", () => { + const pairs: [CelValue, CelType][] = [ + // Scalars + ["str", CelScalar.STRING], + [true, CelScalar.BOOL], + [false, CelScalar.BOOL], + [new Uint8Array([0]), CelScalar.BYTES], + // Numerical + [1.2, CelScalar.DOUBLE], + [1n, CelScalar.INT], + [celUint(1n), CelScalar.UINT], + // Nulls + [null, CelScalar.NULL], + // Messages + [ + reflect( + TestAllTypesSchema, + create(TestAllTypesSchema, { singleInt32: 1 }), + ), + objectType(TestAllTypesSchema), + ], + // Lists + [celList([1, 2, 3]), listType(CelScalar.DYN)], + // Maps + [ + celMap( + new Map([ + [1n, "1"], + [2n, "2"], + ]), + ), + mapType(CelScalar.DYN, CelScalar.DYN), + ], + // Any + [reflectAny(Int32ValueSchema), CelScalar.INT], + [reflectAny(Int64ValueSchema), CelScalar.INT], + [reflectAny(UInt32ValueSchema), CelScalar.UINT], + [reflectAny(UInt64ValueSchema), CelScalar.UINT], + [reflectAny(StringValueSchema), CelScalar.STRING], + [reflectAny(BoolValueSchema), CelScalar.BOOL], + [reflectAny(BytesValueSchema), CelScalar.BYTES], + [reflectAny(DoubleValueSchema), CelScalar.DOUBLE], + [reflectAny(FloatValueSchema), CelScalar.DOUBLE], + [reflectAny(TimestampSchema), objectType(TimestampSchema.typeName)], + [reflectAny(DurationSchema), objectType(DurationSchema.typeName)], + [reflectAny(TestAllTypesSchema), objectType(TestAllTypesSchema.typeName)], + [reflectAny(StructSchema), mapType(CelScalar.STRING, CelScalar.DYN)], + [reflectAny(ListValueSchema), listType(CelScalar.DYN)], + [valueAny({ case: "boolValue", value: false }), CelScalar.BOOL], + [valueAny({ case: "stringValue", value: "" }), CelScalar.STRING], + [valueAny({ case: "listValue", value: {} }), listType(CelScalar.DYN)], + [reflectAny(ValueSchema), CelScalar.NULL], + [ + valueAny({ case: "nullValue", value: NullValue.NULL_VALUE }), + CelScalar.NULL, + ], + [valueAny({ case: "numberValue", value: 1 }), CelScalar.DOUBLE], + ]; + for (const [val, typ] of pairs) { + void test(`${debugStr(val)} is ${typ}`, () => { + assertTypeEqual(celType(val), typ); + }); + } +}); + +function assertTypeEqual(act: CelType, exp: CelType, message?: string) { + message ??= `${act} != ${exp}`; + if (act.kind === "scalar") { + return assert.strictEqual(act, exp, message); + } + if (act.kind === "list" && exp.kind === "list") { + return assertTypeEqual(act.element, exp.element, message); + } + if (act.kind === "map" && exp.kind === "map") { + assertTypeEqual(act.key, exp.key, message); + return assertTypeEqual(act.value, exp.value, message); + } + if (act.kind === "object" && exp.kind === "object") { + assert.strictEqual(act.name, exp.name, message); + return assert.strictEqual(act.desc, exp.desc, message); + } + if (act.kind === "type" && exp.kind === "type") { + return; + } + assert.fail(message); +} + +function reflectAny( + desc: Desc, + init?: MessageInitShape, +) { + return reflect(AnySchema, anyPack(desc, create(desc, init))); +} + +function valueAny(kind: MessageInitShape["kind"]) { + return reflectAny(ValueSchema, { kind: kind }); +} + +function debugStr(val: CelValue): string { + switch (true) { + case isCelUint(val): + return `${val.value}u`; + case isReflectMessage(val): + return `${val.desc.typeName}${isMessage(val.message, AnySchema) ? `(${val.message.typeUrl.slice("type.googleapis.com/".length)})` : ""}`; + case isCelList(val): + return `[${new Array(...val).map(debugStr).join(",")}]`; + case isCelMap(val): + return `{${new Array(...val.entries()).map(([k, v]) => `${debugStr(k)}:${debugStr(v)}`).join(",")}}`; + default: + return `${val}`; + } +} diff --git a/packages/cel/src/type.ts b/packages/cel/src/type.ts index 046800a..dfa4bc8 100644 --- a/packages/cel/src/type.ts +++ b/packages/cel/src/type.ts @@ -38,7 +38,6 @@ import { BoolValueSchema, StructSchema, ListValueSchema, - NullValueSchema, ValueSchema, anyUnpack, type Value, @@ -115,8 +114,8 @@ export interface CelTypeType export interface CelObjectType extends celTypeShared { readonly kind: "object"; - readonly desc: Desc; - readonly name: Desc["typeName"]; + readonly desc: Desc | undefined; + readonly name: string; } interface celTypeShared { @@ -192,16 +191,28 @@ export function typeType(type: T): CelTypeType { /** * Creates a new CelObjectType. */ +export function objectType(typeName: string): CelObjectType; export function objectType( desc: Desc, -): CelObjectType { +): CelObjectType; +export function objectType( + descOrTypeName: Desc | string, +): CelObjectType | CelObjectType { + let name: string, desc: DescMessage | undefined; + if (typeof descOrTypeName === "string") { + name = descOrTypeName; + desc = undefined; + } else { + name = descOrTypeName.typeName; + desc = descOrTypeName; + } return { [privateSymbol]: {}, kind: "object", desc, - name: desc.typeName, + name, toString() { - return desc.name; + return name; }, }; } @@ -331,7 +342,9 @@ export function isCelType(v: unknown): v is CelType { /** * Returns true if v satisfies type t. * - * For lists and maps + * - If the type is dyn, it will always return true. + * - If the type is a list, the element type will not be matched. + * - If the type is a map, key/value types will not be matched. */ export function isTypeOf(val: CelValue, type: CelType): boolean { if (type === CelScalar.DYN) { @@ -383,12 +396,12 @@ function celTypeOfMessage(v: ReflectMessage): CelType { return mapType(CelScalar.STRING, CelScalar.DYN); case ListValueSchema.typeName: return listType(CelScalar.DYN); - case NullValueSchema.typeName: - return CelScalar.NULL; case ValueSchema.typeName: return valueToType(v.message as Value); } - return objectType(v.desc); + return typeName === v.desc.typeName + ? objectType(v.desc) + : objectType(typeName); } function valueToType(v: Value): CelType {