Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose experimental edition features in descriptor sets #627

Merged
merged 7 commits into from
Nov 28, 2023
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
210 changes: 202 additions & 8 deletions packages/protobuf-test/src/descriptor-set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@

import { describe, expect, test } from "@jest/globals";
import { readFileSync } from "fs";
import type { AnyDesc } from "@bufbuild/protobuf";
import {
createDescriptorSet,
Edition,
FeatureSet,
FeatureSet_EnumType,
FeatureSet_JsonFormat,
FeatureSet_MessageEncoding,
FeatureSet_RepeatedFieldEncoding,
FeatureSet_Utf8Validation,
FeatureSet_FieldPresence,
proto3,
ScalarType,
} from "@bufbuild/protobuf";
Expand All @@ -40,12 +48,32 @@ describe("DescriptorSet", () => {
expect(descFile).toBeDefined();
expect(descFile?.syntax).toBe("proto2");
expect(descFile?.edition).toBe(Edition.EDITION_PROTO2);
expect(descFile?.getFeatures()).toStrictEqual(
new FeatureSet({
fieldPresence: FeatureSet_FieldPresence.EXPLICIT,
enumType: FeatureSet_EnumType.CLOSED,
repeatedFieldEncoding: FeatureSet_RepeatedFieldEncoding.EXPANDED,
utf8Validation: FeatureSet_Utf8Validation.NONE,
messageEncoding: FeatureSet_MessageEncoding.LENGTH_PREFIXED,
jsonFormat: FeatureSet_JsonFormat.LEGACY_BEST_EFFORT,
}),
);
});
test("proto3 syntax", () => {
const descFile = set.files.find((f) => f.name == "extra/proto3");
expect(descFile).toBeDefined();
expect(descFile?.syntax).toBe("proto3");
expect(descFile?.edition).toBe(Edition.EDITION_PROTO3);
expect(descFile?.getFeatures()).toStrictEqual(
new FeatureSet({
fieldPresence: FeatureSet_FieldPresence.IMPLICIT,
enumType: FeatureSet_EnumType.OPEN,
repeatedFieldEncoding: FeatureSet_RepeatedFieldEncoding.PACKED,
utf8Validation: FeatureSet_Utf8Validation.VERIFY,
messageEncoding: FeatureSet_MessageEncoding.LENGTH_PREFIXED,
jsonFormat: FeatureSet_JsonFormat.ALLOW,
}),
);
});
test("edition 2023", () => {
const descFile = set.files.find(
Expand All @@ -54,6 +82,132 @@ describe("DescriptorSet", () => {
expect(descFile).toBeDefined();
expect(descFile?.syntax).toBe("editions");
expect(descFile?.edition).toBe(Edition.EDITION_2023);
expect(descFile?.getFeatures()).toStrictEqual(
new FeatureSet({
fieldPresence: FeatureSet_FieldPresence.EXPLICIT,
enumType: FeatureSet_EnumType.OPEN,
repeatedFieldEncoding: FeatureSet_RepeatedFieldEncoding.PACKED,
utf8Validation: FeatureSet_Utf8Validation.VERIFY,
messageEncoding: FeatureSet_MessageEncoding.LENGTH_PREFIXED,
jsonFormat: FeatureSet_JsonFormat.ALLOW,
}),
);
});
describe("edition feature options", () => {
test("file options should apply to all elements", async () => {
const bin = await new UpstreamProtobuf().compileToDescriptorSet(`
edition = "2023";
option features.field_presence = IMPLICIT;
option features.enum_type = CLOSED;
option features.repeated_field_encoding = EXPANDED;
option features.utf8_validation = NONE;
option features.message_encoding = DELIMITED;
option features.json_format = LEGACY_BEST_EFFORT;
message M {
message NestedMessage {
message DeepNestedMessage {}
enum DeepNestedEnum { A = 0; }
}
enum NestedEnum { A = 0; }
}
enum E { A = 0; }
service S {
rpc A(M) returns (M);
}`);
const { files, messages, enums, services } = createDescriptorSet(bin);
const wantFeatures = new FeatureSet({
fieldPresence: FeatureSet_FieldPresence.IMPLICIT,
enumType: FeatureSet_EnumType.CLOSED,
repeatedFieldEncoding: FeatureSet_RepeatedFieldEncoding.EXPANDED,
utf8Validation: FeatureSet_Utf8Validation.NONE,
messageEncoding: FeatureSet_MessageEncoding.DELIMITED,
jsonFormat: FeatureSet_JsonFormat.LEGACY_BEST_EFFORT,
});
expect(files[0].getFeatures()).toStrictEqual(wantFeatures);
expect(messages.get("M")?.getFeatures()).toStrictEqual(wantFeatures);
expect(messages.get("M.NestedMessage")?.getFeatures()).toStrictEqual(
wantFeatures,
);
expect(
messages.get("M.NestedMessage.DeepNestedMessage")?.getFeatures(),
).toStrictEqual(wantFeatures);
expect(enums.get("E")?.getFeatures()).toStrictEqual(wantFeatures);
expect(enums.get("M.NestedEnum")?.getFeatures()).toStrictEqual(
wantFeatures,
);
expect(
enums.get("M.NestedMessage.DeepNestedEnum")?.getFeatures(),
).toStrictEqual(wantFeatures);
expect(services.get("S")?.getFeatures()).toStrictEqual(wantFeatures);
expect(services.get("S")?.methods[0].getFeatures()).toStrictEqual(
wantFeatures,
);
});
test("message options should apply to nested elements", async () => {
const bin = await new UpstreamProtobuf().compileToDescriptorSet(`
edition="2023";
enum EnumJsonAllow { A = 0; }
message MessageJsonAllow {
int32 f = 1;
enum EnumJsonAllow { A = 0; }
message MessageJsonAllow {
int32 f = 1;
}
message MessageJsonLegacy {
option features.json_format = LEGACY_BEST_EFFORT;
int32 f = 1;
enum EnumJsonLegacy { A = 0; }
message MessageJsonLegacy {
int32 f = 1;
}
}
}`);
const { messages, enums } = createDescriptorSet(bin);
function expectJsonFormat(
jsonFormat: FeatureSet_JsonFormat,
desc: AnyDesc | undefined,
) {
expect(desc?.getFeatures().jsonFormat).toBe(jsonFormat);
if (desc?.kind == "message") {
for (const field of desc.fields) {
expect(field.getFeatures().jsonFormat).toBe(jsonFormat);
}
for (const oneof of desc.oneofs) {
expect(oneof.getFeatures().jsonFormat).toBe(jsonFormat);
}
}
if (desc?.kind == "enum") {
for (const value of desc.values) {
expect(value.getFeatures().jsonFormat).toBe(jsonFormat);
}
}
}
expectJsonFormat(FeatureSet_JsonFormat.ALLOW, enums.get("EnumJsonAllow"));
expectJsonFormat(
FeatureSet_JsonFormat.ALLOW,
messages.get("MessageJsonAllow"),
);
expectJsonFormat(
FeatureSet_JsonFormat.ALLOW,
enums.get("MessageJsonAllow.EnumJsonAllow"),
);
expectJsonFormat(
FeatureSet_JsonFormat.ALLOW,
messages.get("MessageJsonAllow.MessageJsonAllow"),
);
expectJsonFormat(
FeatureSet_JsonFormat.LEGACY_BEST_EFFORT,
messages.get("MessageJsonAllow.MessageJsonLegacy"),
);
expectJsonFormat(
FeatureSet_JsonFormat.LEGACY_BEST_EFFORT,
enums.get("MessageJsonAllow.MessageJsonLegacy.EnumJsonLegacy"),
);
expectJsonFormat(
FeatureSet_JsonFormat.LEGACY_BEST_EFFORT,
messages.get("MessageJsonAllow.MessageJsonLegacy.MessageJsonLegacy"),
);
});
});
describe("repeated field packing", () => {
test("proto2 is unpacked by default", async () => {
Expand Down Expand Up @@ -98,6 +252,47 @@ describe("DescriptorSet", () => {
expect(fields?.[3].packed).toBe(true);
expect(fields?.[4].packed).toBe(false);
});
test("edition2023 is packed by default", async () => {
const bin = await new UpstreamProtobuf().compileToDescriptorSet(`
edition="2023";
message M {
int32 not_packable = 1;
repeated bytes also_not_packable = 2;
repeated int32 default = 3;
repeated int32 explicitly_packed = 4 [features.repeated_field_encoding = PACKED];
repeated int32 explicitly_expanded = 5 [features.repeated_field_encoding = EXPANDED];
}
`);
const fields = createDescriptorSet(bin).messages.get("M")?.fields;
expect(fields?.[0].packedByDefault).toBe(true);
expect(fields?.[1].packedByDefault).toBe(false);
expect(fields?.[2].packedByDefault).toBe(true);
expect(fields?.[0].packed).toBe(true);
expect(fields?.[1].packed).toBe(false);
expect(fields?.[2].packed).toBe(true);
expect(fields?.[3].packed).toBe(true);
expect(fields?.[4].packed).toBe(false);
});
test("edition2023 with repeated_field_encoding file option", async () => {
const bin = await new UpstreamProtobuf().compileToDescriptorSet(`
edition="2023";
option features.repeated_field_encoding = EXPANDED;
message M {
int32 not_packable = 1;
repeated bytes also_not_packable = 2;
repeated int32 default = 3;
repeated int32 explicitly_packed = 4 [features.repeated_field_encoding = PACKED];
}
`);
const fields = createDescriptorSet(bin).messages.get("M")?.fields;
expect(fields?.[0].packedByDefault).toBe(true);
expect(fields?.[1].packedByDefault).toBe(false);
expect(fields?.[2].packedByDefault).toBe(true);
expect(fields?.[0].packed).toBe(false);
expect(fields?.[1].packed).toBe(false);
expect(fields?.[2].packed).toBe(false);
expect(fields?.[3].packed).toBe(true);
});
});
test("knows extension", () => {
const ext = set.extensions.get(
Expand Down Expand Up @@ -204,10 +399,8 @@ describe("DescriptorSet", () => {
}
});
});
});
describe("for message", () => {
const message = set.messages.get(MessageWithComments.typeName);
test("itself", () => {
test("for message", () => {
const message = set.messages.get(MessageWithComments.typeName);
const comments = message?.getComments();
expect(comments).toBeDefined();
if (comments) {
Expand All @@ -219,10 +412,11 @@ describe("DescriptorSet", () => {
expect(comments.trailing).toBeUndefined();
}
});
test("field", () => {
const comments = message?.fields
.find((field) => field.name === "foo")
?.getComments();
test("for field", () => {
const field = set.messages
.get(MessageWithComments.typeName)
?.fields.find((field) => field.name === "foo");
const comments = field?.getComments();
expect(comments).toBeDefined();
if (comments) {
expect(comments.leadingDetached).toStrictEqual([
Expand Down
2 changes: 1 addition & 1 deletion packages/protobuf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build:cjs": "../../node_modules/typescript/bin/tsc --project tsconfig.json --module commonjs --outDir ./dist/cjs --declaration --declarationDir ./dist/cjs && echo >./dist/cjs/package.json '{\"type\":\"commonjs\"}'",
"build:esm": "../../node_modules/typescript/bin/tsc --project tsconfig.json --module ES2015 --verbatimModuleSyntax --outDir ./dist/esm --declaration --declarationDir ./dist/esm",
"build:proxy": "node ../../scripts/gen-esm-proxy.mjs .",
"bootstrap:featureset-defaults": "upstream-inject-feature-defaults src/private/feature-set-defaults.ts",
"bootstrap:featureset-defaults": "upstream-inject-feature-defaults src/private/feature-set.ts",
"prebootstrap:wkt": "rm -rf .tmp && mkdir -p .tmp/google/protobuf && cp -rp src/google/protobuf/* .tmp/google/protobuf",
"bootstrap:wkt": "protoc --es_out=src --es_opt=bootstrap_wkt=true,ts_nocheck=false,target=ts --proto_path $(upstream-include wkt) $(upstream-files wkt) && license-header src/google/protobuf",
"postbootstrap:wkt": "diff >/dev/null -r .tmp/google/protobuf src/google/protobuf && cp -rp .tmp/google/protobuf/* src/google/protobuf || true",
Expand Down
Loading