diff --git a/packages/protobuf-test/src/wkt/any.test.ts b/packages/protobuf-test/src/wkt/any.test.ts index d2749000d..f373dd0ab 100644 --- a/packages/protobuf-test/src/wkt/any.test.ts +++ b/packages/protobuf-test/src/wkt/any.test.ts @@ -13,86 +13,190 @@ // limitations under the License. import { describe, expect, test } from "@jest/globals"; +import { + create, + createRegistry, + type Message, + toBinary, +} from "@bufbuild/protobuf"; import { AnySchema, anyIs, anyPack, anyUnpack, ValueSchema, + type FieldMask, + anyUnpackTo, + FieldMaskSchema, + DurationSchema, } from "@bufbuild/protobuf/wkt"; -import type { Value } from "@bufbuild/protobuf/wkt"; -import { create, createRegistry } from "@bufbuild/protobuf"; -describe("google.protobuf.Any", () => { - test(`is correctly identifies by message and type name`, () => { - const val = create(ValueSchema, { - kind: { case: "numberValue", value: 1 }, +describe("anyIs", () => { + test(`matches standard type URL`, () => { + const any = create(AnySchema, { + typeUrl: "type.googleapis.com/google.protobuf.Value", }); - const any = anyPack(ValueSchema, val); - expect(anyIs(any, ValueSchema)).toBe(true); - expect(anyIs(any, ValueSchema.typeName)).toBe(true); - - // The typeUrl set in the Any doesn't have to start with a URL prefix - expect(anyIs(any, "type.googleapis.com/google.protobuf.Value")).toBe(false); }); - - test(`matches type name with leading slash`, () => { + test(`matches short type URL`, () => { const any = create(AnySchema, { typeUrl: "/google.protobuf.Value" }); expect(anyIs(any, ValueSchema)).toBe(true); }); - - test(`is returns false for an empty Any`, () => { + test(`matches custom type URL`, () => { + const any = create(AnySchema, { + typeUrl: "example.com/google.protobuf.Value", + }); + expect(anyIs(any, ValueSchema)).toBe(true); + }); + test("accepts type name string", () => { + const any = create(AnySchema, { typeUrl: "/google.protobuf.Value" }); + expect(anyIs(any, "google.protobuf.Value")).toBe(true); + expect(anyIs(any, "google.protobuf.Duration")).toBe(false); + }); + test("accepts empty type name string", () => { + const any = create(AnySchema, { typeUrl: "/google.protobuf.Value" }); + expect(anyIs(any, "")).toBe(false); + expect(anyIs(create(AnySchema), "")).toBe(false); + }); + test("returns false for an empty Any", () => { const any = create(AnySchema); - expect(anyIs(any, ValueSchema)).toBe(false); expect(anyIs(any, "google.protobuf.Value")).toBe(false); expect(anyIs(any, "")).toBe(false); }); - - test(`unpack correctly unpacks a message in the registry`, () => { - const typeRegistry = createRegistry(ValueSchema); - const val = create(ValueSchema, { - kind: { case: "numberValue", value: 1 }, + test("returns false for different type", () => { + const any = create(AnySchema, { + typeUrl: "type.googleapis.com/google.protobuf.Value", }); - const any = anyPack(ValueSchema, val); - - const unpacked = anyUnpack(any, typeRegistry) as Value; - - expect(unpacked).toBeDefined(); - expect(unpacked.kind.case).toBe("numberValue"); - expect(unpacked.kind.value).toBe(1); + expect(anyIs(any, DurationSchema)).toBe(false); + expect(anyIs(any, "google.protobuf.Duration")).toBe(false); }); +}); - test(`unpack correctly unpacks a message with a leading slash type url in the registry`, () => { - const typeRegistry = createRegistry(ValueSchema); - const val = create(ValueSchema, { - kind: { case: "numberValue", value: 1 }, +describe("anyUnpack()", () => { + describe("with a schema", () => { + test("returns undefined if the Any is empty", () => { + const any = create(AnySchema, { + typeUrl: "", + value: new Uint8Array(), + }); + const unpacked: FieldMask | undefined = anyUnpack(any, FieldMaskSchema); + expect(unpacked).toBeUndefined(); + }); + test("returns undefined if the Any contains a different type", () => { + const any = create(AnySchema, { + typeUrl: "type.googleapis.com/google.protobuf.Duration", + value: toBinary( + DurationSchema, + create(DurationSchema, { + seconds: BigInt(100), + }), + ), + }); + const unpacked: FieldMask | undefined = anyUnpack(any, FieldMaskSchema); + expect(unpacked).toBeUndefined(); + }); + test("returns unpacked", () => { + const val = create(FieldMaskSchema, { + paths: ["foo"], + }); + const any = create(AnySchema, { + typeUrl: "type.googleapis.com/google.protobuf.FieldMask", + value: toBinary(FieldMaskSchema, val), + }); + const unpacked: FieldMask | undefined = anyUnpack(any, FieldMaskSchema); + expect(unpacked).toBeDefined(); + expect(unpacked?.paths).toStrictEqual(["foo"]); }); - const { value } = anyPack(ValueSchema, val); - const any = create(AnySchema, { typeUrl: "/google.protobuf.Value", value }); - - const unpacked = anyUnpack(any, typeRegistry) as Value; - - expect(unpacked).toBeDefined(); - expect(unpacked.kind.case).toBe("numberValue"); - expect(unpacked.kind.value).toBe(1); }); - - test(`unpack returns undefined if message not in the registry`, () => { - const typeRegistry = createRegistry(); - const val = create(ValueSchema, { - kind: { case: "numberValue", value: 1 }, + describe("with a registry", () => { + test("returns undefined if the Any is empty", () => { + const any = create(AnySchema); + const unpacked: Message | undefined = anyUnpack(any, createRegistry()); + expect(unpacked).toBeUndefined(); + }); + test(`returns undefined if message not in the registry`, () => { + const registry = createRegistry(); + const val = create(ValueSchema, { + kind: { case: "numberValue", value: 1 }, + }); + const any = anyPack(ValueSchema, val); + const unpacked = anyUnpack(any, registry); + expect(unpacked).toBeUndefined(); + }); + test(`returns unpacked`, () => { + const typeRegistry = createRegistry(ValueSchema); + const val = create(ValueSchema, { + kind: { case: "numberValue", value: 1 }, + }); + const any = anyPack(ValueSchema, val); + const unpacked: Message | undefined = anyUnpack(any, typeRegistry); + expect(unpacked).toStrictEqual(val); }); - const any = anyPack(ValueSchema, val); - const unpacked = anyUnpack(any, typeRegistry); - expect(unpacked).toBeUndefined(); }); +}); - test(`unpack returns undefined with an empty Any`, () => { - const typeRegistry = createRegistry(ValueSchema); +describe("anyUnpackTo()", () => { + test("returns undefined if the Any is empty", () => { const any = create(AnySchema); - const unpacked = anyUnpack(any, typeRegistry); + const unpacked: FieldMask | undefined = anyUnpackTo( + any, + FieldMaskSchema, + create(FieldMaskSchema), + ); + expect(unpacked).toBeUndefined(); + }); + test("returns undefined if the Any contains a different type", () => { + const any = create(AnySchema, { + typeUrl: "type.googleapis.com/google.protobuf.Duration", + value: toBinary( + DurationSchema, + create(DurationSchema, { + seconds: BigInt(100), + }), + ), + }); + const unpacked: FieldMask | undefined = anyUnpackTo( + any, + FieldMaskSchema, + create(FieldMaskSchema), + ); expect(unpacked).toBeUndefined(); }); + test("returns unpacked", () => { + const val = create(FieldMaskSchema, { + paths: ["foo"], + }); + const any = create(AnySchema, { + typeUrl: "type.googleapis.com/google.protobuf.FieldMask", + value: toBinary(FieldMaskSchema, val), + }); + const unpacked: FieldMask | undefined = anyUnpackTo( + any, + FieldMaskSchema, + create(FieldMaskSchema), + ); + expect(unpacked).toBeDefined(); + expect(unpacked?.paths).toStrictEqual(["foo"]); + }); + test("merges into target", () => { + const val = create(FieldMaskSchema, { + paths: ["foo"], + }); + const any = create(AnySchema, { + typeUrl: "type.googleapis.com/google.protobuf.FieldMask", + value: toBinary(FieldMaskSchema, val), + }); + const target = create(FieldMaskSchema, { + paths: ["bar"], + }); + const unpacked: FieldMask | undefined = anyUnpackTo( + any, + FieldMaskSchema, + target, + ); + expect(unpacked).toBeDefined(); + expect(unpacked?.paths).toStrictEqual(["bar", "foo"]); + expect(target.paths).toStrictEqual(["bar", "foo"]); + }); }); diff --git a/packages/protobuf/src/wkt/any.ts b/packages/protobuf/src/wkt/any.ts index af66a962f..800a39c71 100644 --- a/packages/protobuf/src/wkt/any.ts +++ b/packages/protobuf/src/wkt/any.ts @@ -105,7 +105,7 @@ export function anyUnpack( registryOrMessageDesc.kind == "message" ? registryOrMessageDesc : registryOrMessageDesc.getMessage(typeUrlToName(any.typeUrl)); - if (!desc) { + if (!desc || !anyIs(any, desc)) { return undefined; } return fromBinary(desc, any.value); @@ -119,7 +119,7 @@ export function anyUnpackTo( schema: Desc, message: MessageShape, ) { - if (any.typeUrl === "") { + if (!anyIs(any, schema)) { return undefined; } return mergeFromBinary(schema, message, any.value);