diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 7958e5854..5782aae44 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -57,6 +57,7 @@ "@types/node": "^10.17.26", "@types/yargs": "^17.0.24", "clang-format": "^1.2.2", + "google-proto-files": "^3.0.2", "gts": "^3.1.0", "rimraf": "^3.0.2", "ts-node": "^10.9.2", diff --git a/packages/proto-loader/src/index.ts b/packages/proto-loader/src/index.ts index d607668a9..59e68df93 100644 --- a/packages/proto-loader/src/index.ts +++ b/packages/proto-loader/src/index.ts @@ -115,6 +115,34 @@ export interface EnumTypeDefinition extends ProtobufTypeDefinition { format: 'Protocol Buffer 3 EnumDescriptorProto'; } +export enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 'IDEMPOTENCY_UNKNOWN', + NO_SIDE_EFFECTS = 'NO_SIDE_EFFECTS', + IDEMPOTENT = 'IDEMPOTENT' +} + +export interface NamePart { + name_part: string; + is_extension: boolean; +} + +export interface UninterpretedOption { + name?: NamePart[]; + identifier_value?: string; + positive_int_value?: number; + negative_int_value?: number; + double_value?: number; + string_value?: string; + aggregate_value?: string; +} + +export interface MethodOptions { + deprecated: boolean; + idempotency_level: IdempotencyLevel; + uninterpreted_option: UninterpretedOption[]; + [k: string]: unknown; +} + export interface MethodDefinition { path: string; requestStream: boolean; @@ -126,6 +154,7 @@ export interface MethodDefinition { }; } +function mapMethodOptions(options: Partial[] | undefined): MethodOptions { + return (options || []).reduce((obj: MethodOptions, item: Partial) => { + for (const [key, value] of Object.entries(item)) { + switch (key) { + case 'uninterpreted_option' : + obj.uninterpreted_option.push(item.uninterpreted_option as UninterpretedOption); + break; + default: + obj[key] = value + } + } + return obj + }, + { + deprecated: false, + idempotency_level: IdempotencyLevel.IDEMPOTENCY_UNKNOWN, + uninterpreted_option: [], + } + ) as MethodOptions; +} + function createMethodDefinition( method: Protobuf.Method, serviceName: string, @@ -242,6 +292,7 @@ function createMethodDefinition( originalName: camelCase(method.name), requestType: createMessageDefinition(requestType, fileDescriptors), responseType: createMessageDefinition(responseType, fileDescriptors), + options: mapMethodOptions(method.parsedOptions), }; } diff --git a/packages/proto-loader/test/descriptor_type_test.ts b/packages/proto-loader/test/descriptor_type_test.ts index 180681c12..7a2ed3939 100644 --- a/packages/proto-loader/test/descriptor_type_test.ts +++ b/packages/proto-loader/test/descriptor_type_test.ts @@ -20,6 +20,7 @@ import { rpcFileDescriptorSet } from '../test_protos/rpc.desc'; import { readFileSync } from 'fs'; import * as proto_loader from '../src/index'; +import { dirname } from 'path'; // Relative path from build output directory to test_protos directory const TEST_PROTO_DIR = `${__dirname}/../../test_protos/`; @@ -128,4 +129,53 @@ describe('Descriptor types', () => { // This will throw if the file descriptor object cannot be parsed proto_loader.loadFileDescriptorSetFromObject(rpcFileDescriptorSet); }); + + it('Can parse method options into object correctly', () => { + const includeDirs = [ + dirname(require.resolve('google-proto-files/package.json')) + ]; + const packageDefinition = proto_loader.loadSync(`${TEST_PROTO_DIR}/method_options.proto`, { includeDirs }); + assert('Hello' in packageDefinition); + const service = packageDefinition.Hello as proto_loader.ServiceDefinition + assert.deepStrictEqual(service.Hello.options, { + deprecated: true, + idempotency_level: 'NO_SIDE_EFFECTS', + uninterpreted_option: [{ + name: { + name_part: 'foo', + is_extension: false, + }, + identifier_value: 'bar', + positive_int_value: 9007199254740991, + negative_int_value: -9007199254740991, + double_value: 1.2345, + string_value: 'foobar', + aggregate_value: 'foobar' + }], + '(google.api.http)': { + post: '/hello', + body: '*', + response_body: '*', + additional_bindings: {} + }, + '(google.api.method_signature)': 'bar' + }) + assert.deepStrictEqual(service.HelloWithoutOptions.options, { + deprecated: false, + idempotency_level: 'IDEMPOTENCY_UNKNOWN', + uninterpreted_option: [] + }) + assert.deepStrictEqual(service.HelloWithSomeOptions.options, { + deprecated: true, + idempotency_level: 'IDEMPOTENCY_UNKNOWN', + uninterpreted_option: [], + '(google.api.http)': { + get: '/hello', + additional_bindings: { + body: '*', + get: '/hello-world' + } + }, + }) + }) }); diff --git a/packages/proto-loader/test_protos/method_options.proto b/packages/proto-loader/test_protos/method_options.proto new file mode 100644 index 000000000..97c4fd3aa --- /dev/null +++ b/packages/proto-loader/test_protos/method_options.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/httpbody.proto"; + +message Empty {} + +message MethodSignature { + repeated string method_signature = 1; +} + +service Hello { + rpc Hello (Empty) returns (Empty) { + option deprecated = true; + option idempotency_level = NO_SIDE_EFFECTS; + option uninterpreted_option = { + name: { + name_part: 'foo' + is_extension: false + } + identifier_value: 'bar' + positive_int_value: 9007199254740991 + negative_int_value: -9007199254740991 + double_value: 1.2345 + string_value: 'foobar' + aggregate_value: 'foobar' + }; + option (google.api.http) = { + post: "/hello" + body: "*" + response_body: "*" + additional_bindings: {} + }; + option (google.api.method_signature) = 'bar'; + } + rpc HelloWithoutOptions (Empty) returns (Empty) {} + rpc HelloWithSomeOptions (Empty) returns (Empty) { + option deprecated = true; + option (google.api.http) = { + get: "/hello" + additional_bindings: { + get: "/hello-world" + body: "*" + } + }; + } +}