Skip to content

Commit eb932db

Browse files
authored
Merge pull request #703 from murgatroid99/proto-loader_messages
Add message and enum type information to package definition output.
2 parents 38637b5 + 0bc3d0b commit eb932db

File tree

7 files changed

+251
-22
lines changed

7 files changed

+251
-22
lines changed

gulpfile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ gulp.task('native.test', 'Run tests of native code', (callback) => {
9999
});
100100

101101
gulp.task('test.only', 'Run tests without rebuilding anything',
102-
['js.core.test', 'native.test.only']);
102+
['js.core.test', 'native.test.only', 'protobuf.test']);
103103

104104
gulp.task('test', 'Run all tests', (callback) => {
105105
runSequence('build', 'test.only', 'internal.test.test', callback);

packages/proto-loader/gulpfile.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as fs from 'fs';
2222
import * as mocha from 'gulp-mocha';
2323
import * as path from 'path';
2424
import * as execa from 'execa';
25+
import * as semver from 'semver';
2526

2627
// gulp-help monkeypatches tasks to have an additional description parameter
2728
const gulp = help(_gulp);
@@ -54,7 +55,21 @@ gulp.task('clean', 'Deletes transpiled code.', ['install'],
5455
gulp.task('clean.all', 'Deletes all files added by targets', ['clean']);
5556

5657
/**
57-
* Transpiles TypeScript files in src/ to JavaScript according to the settings
58+
* Transpiles TypeScript files in src/ and test/ to JavaScript according to the settings
5859
* found in tsconfig.json.
5960
*/
60-
gulp.task('compile', 'Transpiles src/.', () => execNpmCommand('compile'));
61+
gulp.task('compile', 'Transpiles src/ and test/.', () => execNpmCommand('compile'));
62+
63+
/**
64+
* Transpiles src/ and test/, and then runs all tests.
65+
*/
66+
gulp.task('test', 'Runs all tests.', () => {
67+
if (semver.satisfies(process.version, ">=6")) {
68+
return gulp.src(`${outDir}/test/**/*.js`)
69+
.pipe(mocha({reporter: 'mocha-jenkins-reporter',
70+
require: ['ts-node/register']}));
71+
} else {
72+
console.log(`Skipping proto-loader tests for Node ${process.version}`);
73+
return Promise.resolve(null);
74+
}
75+
});

packages/proto-loader/src/index.ts

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,25 @@
1616
*
1717
*/
1818
import * as Protobuf from 'protobufjs';
19+
import * as descriptor from 'protobufjs/ext/descriptor';
1920
import * as fs from 'fs';
2021
import * as path from 'path';
2122
import camelCase = require('lodash.camelcase');
2223

24+
declare module 'protobufjs' {
25+
interface Type {
26+
toDescriptor(protoVersion: string): Protobuf.Message<descriptor.IDescriptorProto> & descriptor.IDescriptorProto;
27+
}
28+
29+
interface Root {
30+
toDescriptor(protoVersion: string): Protobuf.Message<descriptor.IFileDescriptorSet> & descriptor.IFileDescriptorSet;
31+
}
32+
33+
interface Enum {
34+
toDescriptor(protoVersion: string): Protobuf.Message<descriptor.IEnumDescriptorProto> & descriptor.IEnumDescriptorProto;
35+
}
36+
}
37+
2338
export interface Serialize<T> {
2439
(value: T): Buffer;
2540
}
@@ -28,6 +43,20 @@ export interface Deserialize<T> {
2843
(bytes: Buffer): T;
2944
}
3045

46+
export interface ProtobufTypeDefinition {
47+
format: string;
48+
type: object;
49+
fileDescriptorProtos: Buffer[];
50+
}
51+
52+
export interface MessageTypeDefinition extends ProtobufTypeDefinition {
53+
format: 'Protocol Buffer 3 DescriptorProto';
54+
}
55+
56+
export interface EnumTypeDefinition extends ProtobufTypeDefinition {
57+
format: 'Protocol Buffer 3 EnumDescriptorProto';
58+
}
59+
3160
export interface MethodDefinition<RequestType, ResponseType> {
3261
path: string;
3362
requestStream: boolean;
@@ -37,20 +66,33 @@ export interface MethodDefinition<RequestType, ResponseType> {
3766
requestDeserialize: Deserialize<RequestType>;
3867
responseDeserialize: Deserialize<ResponseType>;
3968
originalName?: string;
69+
requestType: MessageTypeDefinition;
70+
responseType: MessageTypeDefinition;
4071
}
4172

4273
export interface ServiceDefinition {
4374
[index: string]: MethodDefinition<object, object>;
4475
}
4576

77+
export type AnyDefinition = ServiceDefinition | MessageTypeDefinition | EnumTypeDefinition;
78+
4679
export interface PackageDefinition {
47-
[index: string]: ServiceDefinition;
80+
[index: string]: AnyDefinition;
4881
}
4982

5083
export type Options = Protobuf.IParseOptions & Protobuf.IConversionOptions & {
5184
includeDirs?: string[];
5285
};
5386

87+
const descriptorOptions: Protobuf.IConversionOptions = {
88+
longs: String,
89+
enums: String,
90+
bytes: String,
91+
defaults: true,
92+
oneofs: true,
93+
json: true
94+
};
95+
5496
function joinName(baseName: string, name: string): string {
5597
if (baseName === '') {
5698
return name;
@@ -59,19 +101,28 @@ function joinName(baseName: string, name: string): string {
59101
}
60102
}
61103

62-
function getAllServices(obj: Protobuf.NamespaceBase, parentName: string): Array<[string, Protobuf.Service]> {
104+
type HandledReflectionObject = Protobuf.Service | Protobuf.Type | Protobuf.Enum;
105+
106+
function isHandledReflectionObject(obj: Protobuf.ReflectionObject): obj is HandledReflectionObject {
107+
return obj instanceof Protobuf.Service || obj instanceof Protobuf.Type || obj instanceof Protobuf.Enum;
108+
}
109+
110+
function isNamespaceBase(obj: Protobuf.ReflectionObject): obj is Protobuf.NamespaceBase {
111+
return obj instanceof Protobuf.Namespace || obj instanceof Protobuf.Root;
112+
}
113+
114+
function getAllHandledReflectionObjects(obj: Protobuf.ReflectionObject, parentName: string): Array<[string, HandledReflectionObject]> {
63115
const objName = joinName(parentName, obj.name);
64-
if (obj.hasOwnProperty('methods')) {
65-
return [[objName, obj as Protobuf.Service]];
116+
if (isHandledReflectionObject(obj)) {
117+
return [[objName, obj]];
66118
} else {
67-
return obj.nestedArray.map((child) => {
68-
if (child.hasOwnProperty('nested')) {
69-
return getAllServices(child as Protobuf.NamespaceBase, objName);
70-
} else {
71-
return [];
72-
}
73-
}).reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
119+
if (isNamespaceBase(obj) && typeof obj.nested !== undefined) {
120+
return Object.keys(obj.nested!).map((name) => {
121+
return getAllHandledReflectionObjects(obj.nested![name], objName);
122+
}).reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
123+
}
74124
}
125+
return [];
75126
}
76127

77128
function createDeserializer(cls: Protobuf.Type, options: Options): Deserialize<object> {
@@ -88,16 +139,22 @@ function createSerializer(cls: Protobuf.Type): Serialize<object> {
88139
}
89140

90141
function createMethodDefinition(method: Protobuf.Method, serviceName: string, options: Options): MethodDefinition<object, object> {
142+
/* This is only ever called after the corresponding root.resolveAll(), so we
143+
* can assume that the resolved request and response types are non-null */
144+
const requestType: Protobuf.Type = method.resolvedRequestType!;
145+
const responseType: Protobuf.Type = method.resolvedResponseType!;
91146
return {
92147
path: '/' + serviceName + '/' + method.name,
93148
requestStream: !!method.requestStream,
94149
responseStream: !!method.responseStream,
95-
requestSerialize: createSerializer(method.resolvedRequestType as Protobuf.Type),
96-
requestDeserialize: createDeserializer(method.resolvedRequestType as Protobuf.Type, options),
97-
responseSerialize: createSerializer(method.resolvedResponseType as Protobuf.Type),
98-
responseDeserialize: createDeserializer(method.resolvedResponseType as Protobuf.Type, options),
150+
requestSerialize: createSerializer(requestType),
151+
requestDeserialize: createDeserializer(requestType, options),
152+
responseSerialize: createSerializer(responseType),
153+
responseDeserialize: createDeserializer(responseType, options),
99154
// TODO(murgatroid99): Find a better way to handle this
100-
originalName: camelCase(method.name)
155+
originalName: camelCase(method.name),
156+
requestType: createMessageDefinition(requestType),
157+
responseType: createMessageDefinition(responseType)
101158
};
102159
}
103160

@@ -109,10 +166,58 @@ function createServiceDefinition(service: Protobuf.Service, name: string, option
109166
return def;
110167
}
111168

169+
const fileDescriptorCache: Map<Protobuf.Root, Buffer[]> = new Map<Protobuf.Root, Buffer[]>();
170+
function getFileDescriptors(root: Protobuf.Root): Buffer[] {
171+
if (fileDescriptorCache.has(root)) {
172+
return fileDescriptorCache.get(root)!;
173+
} else {
174+
const descriptorList: descriptor.IFileDescriptorProto[] = root.toDescriptor('proto3').file;
175+
const bufferList: Buffer[] = descriptorList.map(value => Buffer.from(descriptor.FileDescriptorProto.encode(value).finish()));
176+
fileDescriptorCache.set(root, bufferList);
177+
return bufferList;
178+
}
179+
}
180+
181+
function createMessageDefinition(message: Protobuf.Type): MessageTypeDefinition {
182+
const messageDescriptor: protobuf.Message<descriptor.IDescriptorProto> = message.toDescriptor('proto3');
183+
return {
184+
format: 'Protocol Buffer 3 DescriptorProto',
185+
type: messageDescriptor.$type.toObject(messageDescriptor, descriptorOptions),
186+
fileDescriptorProtos: getFileDescriptors(message.root)
187+
};
188+
}
189+
190+
function createEnumDefinition(enumType: Protobuf.Enum): EnumTypeDefinition {
191+
const enumDescriptor: protobuf.Message<descriptor.IEnumDescriptorProto> = enumType.toDescriptor('proto3');
192+
return {
193+
format: 'Protocol Buffer 3 EnumDescriptorProto',
194+
type: enumDescriptor.$type.toObject(enumDescriptor, descriptorOptions),
195+
fileDescriptorProtos: getFileDescriptors(enumType.root)
196+
};
197+
}
198+
199+
/**
200+
* function createDefinition(obj: Protobuf.Service, name: string, options: Options): ServiceDefinition;
201+
* function createDefinition(obj: Protobuf.Type, name: string, options: Options): MessageTypeDefinition;
202+
* function createDefinition(obj: Protobuf.Enum, name: string, options: Options): EnumTypeDefinition;
203+
*/
204+
function createDefinition(obj: HandledReflectionObject, name: string, options: Options): AnyDefinition {
205+
if (obj instanceof Protobuf.Service) {
206+
return createServiceDefinition(obj, name, options);
207+
} else if (obj instanceof Protobuf.Type) {
208+
return createMessageDefinition(obj);
209+
} else if (obj instanceof Protobuf.Enum) {
210+
return createEnumDefinition(obj);
211+
} else {
212+
throw new Error('Type mismatch in reflection object handling');
213+
}
214+
}
215+
112216
function createPackageDefinition(root: Protobuf.Root, options: Options): PackageDefinition {
113217
const def: PackageDefinition = {};
114-
for (const [name, service] of getAllServices(root, '')) {
115-
def[name] = createServiceDefinition(service, name, options);
218+
root.resolveAll();
219+
for (const [name, obj] of getAllHandledReflectionObjects(root, '')) {
220+
def[name] = createDefinition(obj, name, options);
116221
}
117222
return def;
118223
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as assert from 'assert';
2+
3+
import * as proto_loader from '../src/index';
4+
5+
// Relative path from build output directory to test_protos directory
6+
const TEST_PROTO_DIR = `${__dirname}/../../test_protos/`;
7+
8+
type TypeDefinition = proto_loader.EnumTypeDefinition | proto_loader.MessageTypeDefinition;
9+
10+
function isTypeObject(obj: proto_loader.AnyDefinition): obj is TypeDefinition {
11+
return 'format' in obj;
12+
}
13+
14+
describe('Descriptor types', () => {
15+
it('Should be output for each enum', (done) => {
16+
proto_loader.load(`${TEST_PROTO_DIR}/enums.proto`).then((packageDefinition) => {
17+
assert('Enum1' in packageDefinition);
18+
assert(isTypeObject(packageDefinition.Enum1));
19+
// Need additional check because compiler doesn't understand asserts
20+
if(isTypeObject(packageDefinition.Enum1)) {
21+
const enum1Def: TypeDefinition = packageDefinition.Enum1;
22+
assert.strictEqual(enum1Def.format, 'Protocol Buffer 3 EnumDescriptorProto');
23+
}
24+
25+
assert('Enum2' in packageDefinition);
26+
assert(isTypeObject(packageDefinition.Enum2));
27+
// Need additional check because compiler doesn't understand asserts
28+
if(isTypeObject(packageDefinition.Enum2)) {
29+
const enum2Def: TypeDefinition = packageDefinition.Enum2;
30+
assert.strictEqual(enum2Def.format, 'Protocol Buffer 3 EnumDescriptorProto');
31+
}
32+
done();
33+
}, (error) => {done(error);});
34+
});
35+
it('Should be output for each message', (done) => {
36+
proto_loader.load(`${TEST_PROTO_DIR}/messages.proto`).then((packageDefinition) => {
37+
assert('LongValues' in packageDefinition);
38+
assert(isTypeObject(packageDefinition.LongValues));
39+
if(isTypeObject(packageDefinition.LongValues)) {
40+
const longValuesDef: TypeDefinition = packageDefinition.LongValues;
41+
assert.strictEqual(longValuesDef.format, 'Protocol Buffer 3 DescriptorProto');
42+
}
43+
44+
assert('SequenceValues' in packageDefinition);
45+
assert(isTypeObject(packageDefinition.SequenceValues));
46+
if(isTypeObject(packageDefinition.SequenceValues)) {
47+
const sequenceValuesDef: TypeDefinition = packageDefinition.SequenceValues;
48+
assert.strictEqual(sequenceValuesDef.format, 'Protocol Buffer 3 DescriptorProto');
49+
}
50+
done();
51+
}, (error) => {done(error);});
52+
});
53+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2019 gRPC authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
17+
enum Enum1 {
18+
DEFAULT = 0;
19+
VALUE1 = 1;
20+
VALUE2 = 2;
21+
}
22+
23+
enum Enum2 {
24+
DEFAULT = 0;
25+
ABC = 5;
26+
DEF = 10;
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2019 gRPC authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
17+
message LongValues {
18+
int64 int_64 = 1;
19+
uint64 uint_64 = 2;
20+
sint64 sint_64 = 3;
21+
fixed64 fixed_64 = 4;
22+
sfixed64 sfixed_64 = 5;
23+
}
24+
25+
message SequenceValues {
26+
bytes bytes_field = 1;
27+
repeated int32 repeated_field = 2;
28+
}

packages/proto-loader/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"outDir": "build"
66
},
77
"include": [
8-
"src/*.ts"
8+
"src/*.ts",
9+
"test/*.ts"
910
]
1011
}

0 commit comments

Comments
 (0)