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

Add "dependencies" to DescFile #727

Merged
merged 1 commit into from
Feb 26, 2024
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
127 changes: 77 additions & 50 deletions packages/protobuf-test/src/descriptor-set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,56 +50,74 @@ import { join } from "node:path";
const fdsBytes = readFileSync("./descriptorset.binpb");

describe("DescriptorSet", () => {
const set = createDescriptorSet(fdsBytes);
test("proto2 syntax", () => {
const descFile = set.files.find((f) => f.name == "extra/proto2");
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(
(f) => f.name == "editions/edition2023-default-features",
);
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("file", () => {
test("proto2 syntax", () => {
const set = createDescriptorSet(fdsBytes);
const descFile = set.files.find((f) => f.name == "extra/proto2");
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 set = createDescriptorSet(fdsBytes);
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 set = createDescriptorSet(fdsBytes);
const descFile = set.files.find(
(f) => f.name == "editions/edition2023-default-features",
);
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,
}),
);
});
test("dependencies", async () => {
const fdsBin = await new UpstreamProtobuf().compileToDescriptorSet({
"a.proto": `syntax="proto3";
import "b.proto";
import "c.proto";`,
"b.proto": `syntax="proto3";`,
"c.proto": `syntax="proto3";`,
});
const set = createDescriptorSet(fdsBin);
const a = set.files[2];
expect(a.name).toBe("a");
expect(a.dependencies.length).toBe(2);
expect(a.dependencies.map((f) => f.name)).toStrictEqual(["b", "c"]);
});
});
describe("edition feature options", () => {
test("file options should apply to all elements", async () => {
Expand Down Expand Up @@ -423,6 +441,7 @@ describe("DescriptorSet", () => {
});
});
test("knows extension", () => {
const set = createDescriptorSet(fdsBytes);
const ext = set.extensions.get(
"protobuf_unittest.optional_int32_extension",
);
Expand All @@ -442,6 +461,7 @@ describe("DescriptorSet", () => {
);
});
test("knows nested extension", () => {
const set = createDescriptorSet(fdsBytes);
const ext = set.extensions.get(
"protobuf_unittest.TestNestedExtension.nested_string_extension",
);
Expand All @@ -465,6 +485,7 @@ describe("DescriptorSet", () => {
});
describe("declarationString()", () => {
test("for field with options", () => {
const set = createDescriptorSet(fdsBytes);
const message = set.messages.get(JsonNamesMessage.typeName);
expect(message).toBeDefined();
if (message !== undefined) {
Expand All @@ -475,6 +496,7 @@ describe("DescriptorSet", () => {
}
});
test("for field with labels", () => {
const set = createDescriptorSet(fdsBytes);
const message = set.messages.get(RepeatedScalarValuesMessage.typeName);
expect(message).toBeDefined();
if (message !== undefined) {
Expand All @@ -485,13 +507,15 @@ describe("DescriptorSet", () => {
}
});
test("for map field", () => {
const set = createDescriptorSet(fdsBytes);
const message = set.messages.get(MapsMessage.typeName);
const got = message?.fields
.find((f) => f.name === "int32_msg_field")
?.declarationString();
expect(got).toBe("map<int32, spec.MapsMessage> int32_msg_field = 10");
});
test("for enum value", () => {
const set = createDescriptorSet(fdsBytes);
const e = set.enums.get(proto3.getEnumType(SimpleEnum).typeName);
const got = e?.values
.find((v) => v.name === "SIMPLE_ZERO")
Expand All @@ -501,6 +525,7 @@ describe("DescriptorSet", () => {
});
describe("getComments()", () => {
describe("for file", () => {
const set = createDescriptorSet(fdsBytes);
const file = set.files.find((file) =>
file.messages.some(
(message) => message.typeName === MessageWithComments.typeName,
Expand Down Expand Up @@ -528,6 +553,7 @@ describe("DescriptorSet", () => {
});
});
test("for message", () => {
const set = createDescriptorSet(fdsBytes);
const message = set.messages.get(MessageWithComments.typeName);
const comments = message?.getComments();
expect(comments).toBeDefined();
Expand All @@ -541,6 +567,7 @@ describe("DescriptorSet", () => {
}
});
test("for field", () => {
const set = createDescriptorSet(fdsBytes);
const field = set.messages
.get(MessageWithComments.typeName)
?.fields.find((field) => field.name === "foo");
Expand Down
33 changes: 25 additions & 8 deletions packages/protobuf/src/create-descriptor-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export function createDescriptorSet(
input: FileDescriptorProto[] | FileDescriptorSet | Uint8Array,
options?: CreateDescriptorSetOptions,
): DescriptorSet {
const cart = {
const cart: Cart = {
files: [],
enums: new Map<string, DescEnum>(),
messages: new Map<string, DescMessage>(),
services: new Map<string, DescService>(),
Expand All @@ -82,7 +83,7 @@ export function createDescriptorSet(
? FileDescriptorSet.fromBinary(input).file
: input;
const resolverByEdition = new Map<Edition, FeatureResolverFn>();
const files = fileDescriptors.map((proto) => {
for (const proto of fileDescriptors) {
const edition =
proto.edition ?? parseFileSyntax(proto.syntax, proto.edition).edition;
let resolveFeatures = resolverByEdition.get(edition);
Expand All @@ -94,9 +95,9 @@ export function createDescriptorSet(
);
resolverByEdition.set(edition, resolveFeatures);
}
return newFile(proto, cart, resolveFeatures);
});
return { files, ...cart };
addFile(proto, cart, resolveFeatures);
}
return cart;
}

/**
Expand Down Expand Up @@ -129,6 +130,7 @@ interface CreateDescriptorSetOptions {
* use to resolve reference when creating descriptors.
*/
interface Cart {
files: DescFile[];
enums: Map<string, DescEnum>;
messages: Map<string, DescMessage>;
services: Map<string, DescService>;
Expand All @@ -139,18 +141,19 @@ interface Cart {
/**
* Create a descriptor for a file.
*/
function newFile(
function addFile(
proto: FileDescriptorProto,
cart: Cart,
resolveFeatures: FeatureResolverFn,
): DescFile {
): void {
assert(proto.name, `invalid FileDescriptorProto: missing name`);
const file: DescFile = {
kind: "file",
proto,
deprecated: proto.options?.deprecated ?? false,
...parseFileSyntax(proto.syntax, proto.edition),
name: proto.name.replace(/\.proto/, ""),
dependencies: findFileDependencies(proto, cart),
enums: [],
messages: [],
extensions: [],
Expand Down Expand Up @@ -192,7 +195,7 @@ function newFile(
addExtensions(message, cart, resolveFeatures);
}
cart.mapEntries.clear(); // map entries are local to the file, we can safely discard
return file;
cart.files.push(file);
}

/**
Expand Down Expand Up @@ -806,6 +809,20 @@ function parseFileSyntax(
};
}

/**
* Resolve dependencies of FileDescriptorProto to DescFile.
*/
function findFileDependencies(
proto: FileDescriptorProto,
cart: Cart,
): DescFile[] {
return proto.dependency.map((wantName) => {
const dep = cart.files.find((f) => f.proto.name === wantName);
assert(dep);
return dep;
});
}

/**
* Create a fully qualified name for a protobuf type or extension field.
*
Expand Down
20 changes: 12 additions & 8 deletions packages/protobuf/src/descriptor-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export type AnyDesc =
* Describes a protobuf source file.
*/
export interface DescFile {
kind: "file";
readonly kind: "file";
/**
* The syntax specified in the protobuf source.
*/
Expand All @@ -105,6 +105,10 @@ export interface DescFile {
* For a protobuf file `foo/bar.proto`, this is `foo/bar`.
*/
readonly name: string;
/**
* Files imported by this file.
*/
readonly dependencies: DescFile[];
/**
* Top-level enumerations declared in this file.
* Note that more enumerations might be declared within message declarations.
Expand Down Expand Up @@ -155,7 +159,7 @@ export interface DescFile {
* Describes an enumeration in a protobuf source file.
*/
export interface DescEnum {
kind: "enum";
readonly kind: "enum";
/**
* The fully qualified name of the enumeration. (We omit the leading dot.)
*/
Expand Down Expand Up @@ -252,7 +256,7 @@ export interface DescEnumValue {
* Describes a message declaration in a protobuf source file.
*/
export interface DescMessage {
kind: "message";
readonly kind: "message";
/**
* The fully qualified name of the message. (We omit the leading dot.)
*/
Expand Down Expand Up @@ -324,7 +328,7 @@ export interface DescMessage {
*/
export type DescField = DescFieldCommon &
(DescFieldScalar | DescFieldMessage | DescFieldEnum | DescFieldMap) & {
kind: "field";
readonly kind: "field";

/**
* The message this field is declared on.
Expand All @@ -337,7 +341,7 @@ export type DescField = DescFieldCommon &
*/
export type DescExtension = DescFieldCommon &
(DescFieldScalar | DescFieldMessage | DescFieldEnum | DescFieldMap) & {
kind: "extension";
readonly kind: "extension";

/**
* The fully qualified name of the extension.
Expand Down Expand Up @@ -637,7 +641,7 @@ interface DescFieldMapValueScalar {
* Describes a oneof group in a protobuf source file.
*/
export interface DescOneof {
kind: "oneof";
readonly kind: "oneof";
/**
* The name of the oneof group, as specified in the protobuf source.
*/
Expand Down Expand Up @@ -678,7 +682,7 @@ export interface DescOneof {
* Describes a service declaration in a protobuf source file.
*/
export interface DescService {
kind: "service";
readonly kind: "service";
/**
* The fully qualified name of the service. (We omit the leading dot.)
*/
Expand Down Expand Up @@ -721,7 +725,7 @@ export interface DescService {
* Describes an RPC declaration in a protobuf source file.
*/
export interface DescMethod {
kind: "rpc";
readonly kind: "rpc";
/**
* The name of the RPC, as specified in the protobuf source.
*/
Expand Down
Loading