Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 2 additions & 43 deletions packages/cel/src/func.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -239,42 +234,6 @@ export class OrderedDispatcher implements Dispatcher {
}
}

function isOfType<T extends CelType>(
val: CelValue,
type: T,
): val is CelValue<T> {
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<V = CelValue>(args: CelResult<V>[]) {
const errors: CelError[] = [];
const vals: V[] = [];
Expand Down
163 changes: 153 additions & 10 deletions packages/cel/src/type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<
Expand Down Expand Up @@ -125,3 +155,116 @@ void test("CelValue", () => {
| Message;
expectTypeOf<CelInput>().toEqualTypeOf<AllTypes>();
});

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 extends DescMessage>(
desc: Desc,
init?: MessageInitShape<Desc>,
) {
return reflect(AnySchema, anyPack(desc, create(desc, init)));
}

function valueAny(kind: MessageInitShape<typeof ValueSchema>["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}`;
}
}
Loading
Loading