Skip to content

Commit

Permalink
fix: support int64 conversion between the pf message and JSON object (#…
Browse files Browse the repository at this point in the history
…1028)

* fix: support uint64 conversion between the pf message and JSON object

* fix: make enums properly work in request

Co-authored-by: Alexander Fenster <fenster@google.com>
  • Loading branch information
summer-ji-eng and alexander-fenster authored Jun 17, 2021
1 parent 03b5a4e commit b46f57d
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 42 deletions.
95 changes: 56 additions & 39 deletions src/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,15 @@ export class GrpcClient {
return root;
}

private getServiceMethods(service: protobuf.Service) {
const methods = Object.keys(service.methods);

const methodsLowerCamelCase = methods.map(method => {
return method[0].toLowerCase() + method.substring(1);
});
private static getServiceMethods(service: protobuf.Service) {
const methods: {[name: string]: protobuf.Method} = {};
for (const [methodName, methodObject] of Object.entries(service.methods)) {
const methodNameLowerCamelCase =
methodName[0].toLowerCase() + methodName.substring(1);
methods[methodNameLowerCamelCase] = methodObject;
}

return methodsLowerCamelCase;
return methods;
}

/**
Expand Down Expand Up @@ -285,7 +286,7 @@ export class GrpcClient {
requestData: Uint8Array,
callback: protobuf.RPCImplCallback
) {
return [method, requestData, callback];
return [requestData, callback];
}

// decoder for google.rpc.Status messages
Expand All @@ -307,34 +308,44 @@ export class GrpcClient {
false,
false
) as unknown as FallbackServiceStub;
const methods = this.getServiceMethods(service);

const newServiceStub = service.create(
const methods = GrpcClient.getServiceMethods(service);

// grpcCompatibleServiceStub methods accept four parameters:
// request, options, metadata, and callback - similar to
// the stub returned by grpc.ts
const grpcCompatibleServiceStub = service.create(
serviceClientImpl,
false,
false
) as unknown as FallbackServiceStub;
for (const methodName of methods) {
newServiceStub[methodName] = (
for (const [methodName, methodObject] of Object.entries(methods)) {
grpcCompatibleServiceStub[methodName] = (
req: {},
options: {[name: string]: string},
metadata: {},
callback: Function
) => {
const [method, requestData, serviceCallback] = serviceStub[
methodName
].apply(serviceStub, [
req,
(err: Error | null, response: {}) => {
if (!err) {
// converts a protobuf message instance to a plain JavaScript object with enum conversion options specified
response = method.resolvedResponseType.toObject(response, {
enums: String,
});
}
callback(err, response);
},
]);
const [requestData, serviceCallback] = serviceStub[methodName].apply(
serviceStub,
[
methodObject.resolvedRequestType!.fromObject(req),
(err: Error | null, response: protobuf.Message<{}>) => {
if (!err) {
// converts a protobuf message instance to a plain JavaScript object
// with enum and long conversion options specified
const responseObject =
methodObject.resolvedResponseType!.toObject(response, {
enums: String,
longs: String,
});
callback(null, responseObject);
} else {
callback(err);
}
},
]
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let cancelController: AbortController, cancelSignal: any;
if (isBrowser() || typeof AbortController !== 'undefined') {
Expand Down Expand Up @@ -380,13 +391,13 @@ export class GrpcClient {
}

const protoNamespaces: string[] = [];
let currNamespace = method.parent;
let currNamespace = methodObject.parent!;
while (currNamespace.name !== '') {
protoNamespaces.unshift(currNamespace.name);
currNamespace = currNamespace.parent;
currNamespace = currNamespace.parent!;
}
const protoServiceName = protoNamespaces.join('.');
const rpcName = method.name;
const rpcName = methodObject.name;

let url: string;
let data: string;
Expand All @@ -397,21 +408,26 @@ export class GrpcClient {
if (this.fallback === 'rest') {
// REGAPIC: JSON over HTTP/1 with gRPC trancoding
headers['Content-Type'] = 'application/json';
const decodedRequest = method.resolvedRequestType.decode(requestData);
const requestJSON = method.resolvedRequestType.toObject(
const decodedRequest =
methodObject.resolvedRequestType!.decode(requestData);
const requestJSON = methodObject.resolvedRequestType!.toObject(
// TODO: use toJSON instead of toObject
decodedRequest
decodedRequest,
{
enums: String,
longs: String,
}
);
const transcoded = transcode(
requestJSON,
method.parsedOptions,
method.resolvedRequestType.fields
methodObject.parsedOptions,
methodObject.resolvedRequestType!.fields
);
if (!transcoded) {
throw new Error(
`Cannot build HTTP request for ${JSON.stringify(
requestJSON
)}, method: ${method.name}`
)}, method: ${methodObject.name}`
);
}
httpMethod = transcoded.httpMethod;
Expand Down Expand Up @@ -467,9 +483,10 @@ export class GrpcClient {
);
throw error;
}
const message = method.resolvedResponseType.fromObject(response);
const encoded = method.resolvedResponseType
.encode(message)
const message =
methodObject.resolvedResponseType!.fromObject(response);
const encoded = methodObject
.resolvedResponseType!.encode(message)
.finish();
serviceCallback(null, encoded);
} else {
Expand Down Expand Up @@ -501,7 +518,7 @@ export class GrpcClient {
};
};
}
return newServiceStub;
return grpcCompatibleServiceStub;
}
}

Expand Down
13 changes: 12 additions & 1 deletion test/fixtures/google/example/library/v1/library.proto
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ service LibraryService {

// Gets a book. Returns NOT_FOUND if the book does not exist.
rpc GetBook(GetBookRequest) returns (Book) {
option (google.api.http) = { get: "/v1/{name=shelves/*/books/*}" };
option (google.api.http) = {
get: "/v1/{name=shelves/*/books/*}"
additional_bindings: {
get: "/v1/{name=shelves/*/book_id/*}"
}
};
}

// Lists books in a shelf. The order is unspecified but deterministic. Newly
Expand Down Expand Up @@ -120,6 +125,9 @@ message Book {

// Value indicating whether the book has been read.
bool read = 4;

// The id of the book.
int64 book_id = 5;
}

// A Shelf contains a collection of books with a theme.
Expand Down Expand Up @@ -203,6 +211,9 @@ message CreateBookRequest {
message GetBookRequest {
// The name of the book to retrieve.
string name = 1;

// The the id of the book.
int64 book_id = 2;
}

// Request message for LibraryService.ListBooks.
Expand Down
16 changes: 14 additions & 2 deletions test/fixtures/library.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,16 @@
"requestType": "GetBookRequest",
"responseType": "Book",
"options": {
"(google.api.http).get": "/v1/{name=shelves/*/books/*}"
"(google.api.http).get": "/v1/{name=shelves/*/books/*}",
"(google.api.http).additional_bindings.get": "/v1/{name=shelves/*/book_id/*}"
},
"parsedOptions": [
{
"(google.api.http)": {
"get": "/v1/{name=shelves/*/books/*}"
"get": "/v1/{name=shelves/*/books/*}",
"additional_bindings": {
"get": "/v1/{name=shelves/*/book_id/*}"
}
}
}
]
Expand Down Expand Up @@ -199,6 +203,10 @@
"read": {
"type": "bool",
"id": 4
},
"bookId": {
"type": "int64",
"id": 5
}
}
},
Expand Down Expand Up @@ -296,6 +304,10 @@
"name": {
"type": "string",
"id": 1
},
"bookId": {
"type": "int64",
"id": 2
}
}
},
Expand Down
114 changes: 114 additions & 0 deletions test/unit/regapic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,118 @@ describe('regapic', () => {
);
});
});

describe('should support long data type conversion in proto message', () => {
it('large number long data type conversion in proto message response', done => {
const requestObject = {name: 'shelves/shelf-name/books/book-name'};
const responseObject = {
name: 'book-name',
author: 'book-author',
title: 'book-title',
read: true,
bookId: 9007199254740992,
};
// incomplete types for nodeFetch, so...
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sinon.stub(nodeFetch, 'Promise' as any).returns(
Promise.resolve({
ok: true,
arrayBuffer: () => {
return Promise.resolve(Buffer.from(JSON.stringify(responseObject)));
},
})
);
gaxGrpc.createStub(libraryService, stubOptions).then(libStub => {
libStub.getBook(
requestObject,
{},
{},
(
err: {},
result: {name: {}; author: {}; title: {}; read: false; bookId: {}}
) => {
assert.strictEqual(err, null);
assert.strictEqual('book-name', result.name);
assert.strictEqual('9007199254740992', result.bookId);
done();
}
);
});
});

it('small number long data type conversion in proto message response', done => {
const requestObject = {name: 'shelves/shelf-name/books/book-name'};
const responseObject = {
name: 'book-name',
author: 'book-author',
title: 'book-title',
read: true,
bookId: 42,
};
// incomplete types for nodeFetch, so...
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sinon.stub(nodeFetch, 'Promise' as any).returns(
Promise.resolve({
ok: true,
arrayBuffer: () => {
return Promise.resolve(Buffer.from(JSON.stringify(responseObject)));
},
})
);
gaxGrpc.createStub(libraryService, stubOptions).then(libStub => {
libStub.getBook(
requestObject,
{},
{},
(
err: {},
result: {name: {}; author: {}; title: {}; read: false; bookId: {}}
) => {
assert.strictEqual(err, null);
assert.strictEqual('book-name', result.name);
assert.strictEqual('42', result.bookId);
done();
}
);
});
});

it('long data type conversion in proto message request', done => {
const bookId = 9007199254740992;
const requestObject = {name: `shelves/shelf-name/book_id/${bookId}`};
const responseObject = {
name: 'book-name',
author: 'book-author',
title: 'book-title',
read: true,
bookId: bookId,
};
// incomplete types for nodeFetch, so...
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sinon.stub(nodeFetch, 'Promise' as any).returns(
Promise.resolve({
ok: true,
arrayBuffer: () => {
return Promise.resolve(Buffer.from(JSON.stringify(responseObject)));
},
})
);
gaxGrpc.createStub(libraryService, stubOptions).then(libStub => {
libStub.getBook(
requestObject,
{},
{},
(
err: {},
result: {name: {}; author: {}; title: {}; read: false; bookId: {}}
) => {
assert.strictEqual(err, null);
assert.strictEqual('book-name', result.name);
assert.strictEqual(bookId.toString(), result.bookId);
done();
}
);
});
});
});
});

0 comments on commit b46f57d

Please sign in to comment.