Skip to content

Commit

Permalink
feat: support regapic LRO (#1276)
Browse files Browse the repository at this point in the history
* feat: support regapic LRO

* address operationClient comments
  • Loading branch information
summer-ji-eng authored Jun 7, 2022
1 parent ff5eef3 commit b9beedb
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 7 deletions.
8 changes: 7 additions & 1 deletion src/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {isNodeJS} from './featureDetection';
import {generateServiceStub} from './fallbackServiceStub';
import {StreamType} from './streamingCalls/streaming';
import * as objectHash from 'object-hash';
import {google} from '../protos/http';

export {FallbackServiceError};
export {PathTemplate} from './pathTemplate';
Expand Down Expand Up @@ -83,6 +84,7 @@ export class GrpcClient {
fallback: boolean | 'rest' | 'proto';
grpcVersion: string;
private static protoCache = new Map<string, protobuf.Root>();
httpRules?: Array<google.api.IHttpRule>;

/**
* In rare cases users might need to deallocate all memory consumed by loaded protos.
Expand Down Expand Up @@ -121,6 +123,7 @@ export class GrpcClient {
}
this.fallback = options.fallback !== 'rest' ? 'proto' : 'rest';
this.grpcVersion = require('../../package.json').version;
this.httpRules = (options as GrpcClientOptions).httpRules;
}

/**
Expand Down Expand Up @@ -329,8 +332,11 @@ export class GrpcClient {
*/
export function lro(options: GrpcClientOptions) {
options = Object.assign({scopes: []}, options);
if (options.protoJson) {
options = Object.assign(options, {fallback: 'rest'});
}
const gaxGrpc = new GrpcClient(options);
return new OperationsClientBuilder(gaxGrpc);
return new OperationsClientBuilder(gaxGrpc, options.protoJson);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ INCLUDE_DIRS.push(googleProtoFilesDir);
// COMMON_PROTO_FILES logic is here for protobufjs loads (see
// GoogleProtoFilesRoot below)
import * as commonProtoFiles from './protosList.json';
import {google} from '../protos/http';
// use the correct path separator for the OS we are running on
const COMMON_PROTO_FILES: string[] = commonProtoFiles.map(file =>
file.replace(/[/\\]/g, path.sep)
Expand All @@ -46,6 +47,8 @@ const COMMON_PROTO_FILES: string[] = commonProtoFiles.map(file =>
export interface GrpcClientOptions extends GoogleAuthOptions {
auth?: GoogleAuth;
grpc?: GrpcModule;
protoJson?: protobuf.Root;
httpRules?: Array<google.api.IHttpRule>;
}

export interface MetadataValue {
Expand Down Expand Up @@ -112,6 +115,7 @@ export class GrpcClient {
grpcVersion: string;
fallback: boolean | 'rest' | 'proto';
private static protoCache = new Map<string, grpc.GrpcObject>();
httpRules?: Array<google.api.IHttpRule>;

/**
* Key for proto cache map. We are doing our best to make sure we respect
Expand Down
18 changes: 13 additions & 5 deletions src/operationsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import * as protos from '../protos/operations';
import configData = require('./operations_client_config.json');
import {Transform} from 'stream';
import {CancellablePromise} from './call';
import protoJson = require('../protos/operations.json');
import operationProtoJson = require('../protos/operations.json');
import {overrideHttpRules} from './transcoding';

export const SERVICE_ADDRESS = 'longrunning.googleapis.com';
const version = require('../../package.json').version;
Expand Down Expand Up @@ -567,9 +568,16 @@ export class OperationsClientBuilder {
* Builds a new Operations Client
* @param gaxGrpc {GrpcClient}
*/
constructor(gaxGrpc: GrpcClient | FallbackGrpcClient) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const operationsProtos = gaxGrpc.loadProtoJSON(protoJson);
constructor(
gaxGrpc: GrpcClient | FallbackGrpcClient,
protoJson?: protobuf.Root
) {
if (protoJson && gaxGrpc.httpRules) {
// overwrite the http rules if provide in service yaml.
overrideHttpRules(gaxGrpc.httpRules, protoJson);
}
const operationsProtos =
protoJson ?? gaxGrpc.loadProtoJSON(operationProtoJson);

/**
* Build a new instance of {@link OperationsClient}.
Expand All @@ -582,7 +590,7 @@ export class OperationsClientBuilder {
*/
this.operationsClient = opts => {
if (gaxGrpc.fallback) {
opts.fallback = true;
opts.fallback = gaxGrpc.fallback;
}
return new OperationsClient(gaxGrpc, operationsProtos, opts);
};
Expand Down
44 changes: 44 additions & 0 deletions src/transcoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,47 @@ export function transcode(
}
return undefined;
}

// Override the protobuf json's the http rules.
export function overrideHttpRules(
httpRules: Array<google.api.IHttpRule>,
protoJson: protobuf.Root
) {
for (const rule of httpRules) {
if (!rule.selector) {
continue;
}
const rpc = protoJson.lookup(rule.selector) as protobuf.Method;
// Not support override on non-exist RPC or a RPC without an annotation.
// We could reconsider if we have the use case later.
if (!rpc || !rpc.parsedOptions) {
continue;
}
for (const item of rpc.parsedOptions) {
if (!(httpOptionName in item)) {
continue;
}
const httpOptions = item[httpOptionName];
for (const httpMethod in httpOptions) {
if (httpMethod in rule) {
if (httpMethod === 'additional_bindings') {
continue;
}
httpOptions[httpMethod] =
rule[httpMethod as keyof google.api.IHttpRule];
}
if (rule.additional_bindings) {
httpOptions['additional_bindings'] = !httpOptions[
'additional_bindings'
]
? []
: Array.isArray(httpOptions['additional_bindings'])
? httpOptions['additional_bindings']
: [httpOptions['additional_bindings']];
// Make the additional_binding to be an array if it is not.
httpOptions['additional_bindings'].push(...rule.additional_bindings);
}
}
}
}
}
1 change: 0 additions & 1 deletion test/unit/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {StreamArrayParser} from '../../src/streamArrayParser';
import path = require('path');
import protobuf = require('protobufjs');
import {GoogleError} from '../../src';
import {CodeChallengeMethod} from 'google-auth-library';
import {Metadata} from '@grpc/grpc-js';

function createApiCallStreaming(
Expand Down
138 changes: 138 additions & 0 deletions test/unit/transcoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ import {
match,
buildQueryStringComponents,
requestChangeCaseAndCleanup,
overrideHttpRules,
} from '../../src/transcoding';
import * as assert from 'assert';
import {camelToSnakeCase, snakeToCamelCase} from '../../src/util';
import * as protobuf from 'protobufjs';
import {testMessageJson} from '../fixtures/fallbackOptional';
import {echoProtoJson} from '../fixtures/echoProtoJson';
import {google} from '../../protos/http';

describe('gRPC to HTTP transcoding', () => {
const parsedOptions: ParsedOptionsType = [
Expand Down Expand Up @@ -697,3 +700,138 @@ describe('validate proto3 field with default value', () => {
}
});
});

describe('override the HTTP rules in protoJson', () => {
const httpOptionName = '(google.api.http)';

it('override multiple http rules', () => {
const httpRules: Array<google.api.IHttpRule> = [
{
selector: 'google.showcase.v1beta1.Messaging.GetRoom',
get: '/v1beta1/{name**}',
},
{
selector: 'google.showcase.v1beta1.Messaging.ListRooms',
get: '/fake/value',
},
];
const root = protobuf.Root.fromJSON(echoProtoJson);
overrideHttpRules(httpRules, root);
for (const rule of httpRules) {
const modifiedRpc = root.lookup(rule.selector!) as protobuf.Method;
assert(modifiedRpc);
assert(modifiedRpc.parsedOptions);
for (const item of modifiedRpc!.parsedOptions) {
if (!(httpOptionName in item)) {
continue;
}
const httpOptions = item[httpOptionName];
assert.deepStrictEqual(httpOptions.get, rule.get);
}
}
});

it("override additional bindings for a rpc doesn't has additional bindings", () => {
const httpRules: Array<google.api.IHttpRule> = [
{
selector: 'google.showcase.v1beta1.Messaging.GetRoom',
get: 'v1beta1/room/{name=**}',
additional_bindings: [{get: 'v1beta1/room/{name}'}],
},
];
const root = protobuf.Root.fromJSON(echoProtoJson);
overrideHttpRules(httpRules, root);
for (const rule of httpRules) {
const modifiedRpc = root.lookup(rule.selector!) as protobuf.Method;
assert(modifiedRpc);
assert(modifiedRpc.parsedOptions);
for (const item of modifiedRpc!.parsedOptions) {
if (!(httpOptionName in item)) {
continue;
}
const httpOptions = item[httpOptionName];
assert.deepStrictEqual(httpOptions.get, rule.get);
assert.deepStrictEqual(
httpOptions.additional_bindings,
rule.additional_bindings
);
}
}
});

it('append additional bindings for a rpc has additional bindings', () => {
const httpRules: Array<google.api.IHttpRule> = [
{
selector: 'google.showcase.v1beta1.Messaging.GetBlurb',
get: 'v1beta1/fake/value',
additional_bindings: [
{get: 'v1beta1/fake/value'},
] as Array<google.api.IHttpRule>,
},
];
const root = protobuf.Root.fromJSON(echoProtoJson);
const originRpc = root.lookup(httpRules[0].selector!) as protobuf.Method;
const originAdditionalBindings = () => {
for (const item of originRpc!.parsedOptions) {
if (!(httpOptionName in item)) {
continue;
}
const httpOptions = item[httpOptionName] as google.api.IHttpRule;
return [httpOptions.additional_bindings];
}
return null;
};
assert(originAdditionalBindings());
const expectedAditionalBindings = originAdditionalBindings()!.concat(
httpRules[0].additional_bindings
);
overrideHttpRules(httpRules, root);
for (const rule of httpRules) {
const modifiedRpc = root.lookup(rule.selector!) as protobuf.Method;
assert(modifiedRpc);
assert(modifiedRpc.parsedOptions);
for (const item of modifiedRpc!.parsedOptions) {
if (!(httpOptionName in item)) {
continue;
}
const httpOptions = item[httpOptionName];
assert.deepStrictEqual(httpOptions.get, rule.get);
assert.deepStrictEqual(
httpOptions.additional_bindings,
expectedAditionalBindings
);
}
}
});

it("can't override a non-exist rpc", () => {
const httpRules: Array<google.api.IHttpRule> = [
{
selector: 'not.a.valid.rpc',
get: 'v1/operations/{name=**}',
},
];
const root = protobuf.Root.fromJSON(echoProtoJson);
overrideHttpRules(httpRules, root);
for (const rule of httpRules) {
const modifiedRpc = root.lookup(rule.selector!) as protobuf.Method;
assert.equal(modifiedRpc, null);
}
});

it('not support a rpc has no parsedOption', () => {
const httpRules: Array<google.api.IHttpRule> = [
{
selector: 'google.showcase.v1beta1.Messaging.Connect',
get: 'fake/url',
},
];
const root = protobuf.Root.fromJSON(echoProtoJson);
overrideHttpRules(httpRules, root);
for (const rule of httpRules) {
const modifiedRpc = root.lookup(rule.selector!) as protobuf.Method;
assert(modifiedRpc);
assert.equal(modifiedRpc.parsedOptions, null);
}
});
});

0 comments on commit b9beedb

Please sign in to comment.