diff --git a/packages/proto-loader/README.md b/packages/proto-loader/README.md index 481b8629..07f28907 100644 --- a/packages/proto-loader/README.md +++ b/packages/proto-loader/README.md @@ -38,6 +38,7 @@ The options parameter is an object that can have the following optional properti | `arrays` | `true` or `false` | Set empty arrays for missing array values even if `defaults` is `false` Defaults to `false`. | `objects` | `true` or `false` | Set empty objects for missing object values even if `defaults` is `false` Defaults to `false`. | `oneofs` | `true` or `false` | Set virtual oneof properties to the present field's name. Defaults to `false`. +| `json` | `true` or `false` | Represent `Infinity` and `NaN` as strings in `float` fields, and automatically decode `google.protobuf.Any` values. Defaults to `false` | `includeDirs` | An array of strings | A list of search paths for imported `.proto` files. The following options object closely approximates the existing behavior of `grpc.load`: diff --git a/packages/proto-loader/bin/proto-loader-gen-types.ts b/packages/proto-loader/bin/proto-loader-gen-types.ts index 6342d2bd..7c70cd20 100644 --- a/packages/proto-loader/bin/proto-loader-gen-types.ts +++ b/packages/proto-loader/bin/proto-loader-gen-types.ts @@ -96,6 +96,15 @@ function getImportLine(dependency: Protobuf.Type | Protobuf.Enum, from?: Protobu } function generatePermissiveMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type) { + if (messageType.fullName === '.google.protobuf.Any') { + /* This describes the behavior of the Protobuf.js Any wrapper fromObject + * replacement function */ + formatter.writeLine('export type Any__Output = AnyExtension | {'); + formatter.writeLine(' type_url: string;'); + formatter.writeLine(' value: Buffer | Uint8Array | string;'); + formatter.writeLine('}'); + return; + } formatter.writeLine(`export interface ${messageType.name} {`); formatter.indent(); for (const field of messageType.fieldsArray) { @@ -104,6 +113,8 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp switch (field.type) { case 'double': case 'float': + type = 'number | string'; + break; case 'int32': case 'uint32': case 'sint32': @@ -149,6 +160,23 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp } function generateRestrictedMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type, options: Protobuf.IConversionOptions) { + if (messageType.fullName === '.google.protobuf.Any' && options.json) { + /* This describes the behavior of the Protobuf.js Any wrapper toObject + * replacement function */ + formatter.writeLine('export type Any__Output = AnyExtension | {'); + formatter.writeLine(' type_url: string;'); + let type: string; + if (options.bytes === Array) { + type = 'Uint8Array'; + } else if (options.bytes === String) { + type = 'string'; + } else { + type = 'Buffer'; + } + formatter.writeLine(` value: ${type};`); + formatter.writeLine('}'); + return; + } formatter.writeLine(`export interface ${messageType.name}__Output {`); formatter.indent(); for (const field of messageType.fieldsArray) { @@ -158,6 +186,12 @@ function generateRestrictedMessageInterface(formatter: TextFormatter, messageTyp switch (field.type) { case 'double': case 'float': + if (options.json) { + type = 'number | string'; + } else { + type = 'number'; + } + break; case 'int32': case 'uint32': case 'sint32': @@ -244,6 +278,9 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob if (usesLong) { formatter.writeLine("import { Long } from '@grpc/proto-loader';"); } + if (messageType.fullName === '.google.protobuf.Any') { + formatter.writeLine("import { AnyExtension } from '@grpc/proto-loader';") + } formatter.writeLine(''); generatePermissiveMessageInterface(formatter, messageType); @@ -524,7 +561,7 @@ function runScript() { .string(['includeDirs', 'grpcLib']) .normalize(['includeDirs', 'outDir']) .array('includeDirs') - .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs']) + .boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json']) // .choices('longs', ['String', 'Number']) // .choices('enums', ['String']) // .choices('bytes', ['Array', 'String']) @@ -559,13 +596,14 @@ function runScript() { outDir: 'O' }).describe({ keepCase: 'Preserve the case of field names', - longs: 'The type that should be used to output 64 bit integer values', - enums: 'The type that should be used to output enum fields', - bytes: 'The type that should be used to output bytes fields', + longs: 'The type that should be used to output 64 bit integer values. Can be String, Number', + enums: 'The type that should be used to output enum fields. Can be String', + bytes: 'The type that should be used to output bytes fields. Can be String, Array', defaults: 'Output default values for omitted fields', arrays: 'Output default values for omitted repeated fields even if --defaults is not set', objects: 'Output default values for omitted message fields even if --defaults is not set', oneofs: 'Output virtual oneof fields set to the present field\'s name', + json: 'Represent Infinity and NaN as strings in float fields. Also decode google.protobuf.Any automatically', includeDirs: 'Directories to search for included files', outDir: 'Directory in which to output files', grpcLib: 'The gRPC implementation library that these types will be used with' diff --git a/packages/proto-loader/package.json b/packages/proto-loader/package.json index 2bf93d42..b84b7977 100644 --- a/packages/proto-loader/package.json +++ b/packages/proto-loader/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/proto-loader", - "version": "0.6.0-pre2", + "version": "0.6.0-pre3", "author": "Google Inc.", "contributors": [ { diff --git a/packages/proto-loader/src/index.ts b/packages/proto-loader/src/index.ts index 45b6f150..2e850760 100644 --- a/packages/proto-loader/src/index.ts +++ b/packages/proto-loader/src/index.ts @@ -22,6 +22,39 @@ import * as descriptor from 'protobufjs/ext/descriptor'; export { Long } from 'long'; +/** + * This type exists for use with code generated by the proto-loader-gen-types + * tool. This type should be used with another interface, e.g. + * MessageType & AnyExtension for an object that is converted to or from a + * google.protobuf.Any message. + * For example, when processing an Any message: + * + * ```ts + * if (isAnyExtension(message)) { + * switch (message['@type']) { + * case TYPE1_URL: + * handleType1(message as AnyExtension & Type1); + * break; + * case TYPE2_URL: + * handleType2(message as AnyExtension & Type2); + * break; + * // ... + * } + * } + * ``` + */ +export interface AnyExtension { + /** + * The fully qualified name of the message type that this object represents, + * possibly including a URL prefix. + */ + '@type': string; +} + +export function isAnyExtension(obj: object): obj is AnyExtension { + return ('@type' in obj) && (typeof (obj as AnyExtension)['@type'] === 'string'); +} + import camelCase = require('lodash.camelcase'); declare module 'protobufjs' { @@ -331,6 +364,8 @@ function addIncludePathResolver(root: Protobuf.Root, includePaths: string[]) { * `defaults` is `false`. Defaults to `false`. * @param options.oneofs Set virtual oneof properties to the present field's * name + * @param options.json Represent Infinity and NaN as strings in float fields, + * and automatically decode google.protobuf.Any values. * @param options.includeDirs Paths to search for imported `.proto` files. */ export function load(