diff --git a/README.md b/README.md index ccec8767..b5ae07f0 100644 --- a/README.md +++ b/README.md @@ -172,3 +172,12 @@ client.getUser(req, (err, user) => { /* ... */ }); For a sample of the generated protos and service definitions, see [examples](https://github.com/improbable-eng/ts-protoc-gen/tree/master/examples). To generate the examples from protos, please run `./generate.sh` + +## Gotchas +By default the google-protobuf library will use the JavaScript number type to store 64bit float and integer values; this can lead to overflow problems as you exceed JavaScript's `Number.MAX_VALUE`. To work around this, you should consider using the `jstype` annotation on any 64bit fields, ie: + +```proto +message Example { + uint64 bigInt = 1 [jstype = JS_STRING]; +} +``` \ No newline at end of file diff --git a/examples/generated/proto/examplecom/annotations_pb.d.ts b/examples/generated/proto/examplecom/annotations_pb.d.ts new file mode 100644 index 00000000..2c0f3e2d --- /dev/null +++ b/examples/generated/proto/examplecom/annotations_pb.d.ts @@ -0,0 +1,41 @@ +// package: examplecom +// file: proto/examplecom/annotations.proto + +import * as jspb from "google-protobuf"; + +export class AnnotatedMessage extends jspb.Message { + getMyunit64(): string; + setMyunit64(value: string): void; + + getMyint64(): string; + setMyint64(value: string): void; + + getMyfixed64(): string; + setMyfixed64(value: string): void; + + getMysint64(): string; + setMysint64(value: string): void; + + getMysfixed64(): string; + setMysfixed64(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): AnnotatedMessage.AsObject; + static toObject(includeInstance: boolean, msg: AnnotatedMessage): AnnotatedMessage.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: AnnotatedMessage, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): AnnotatedMessage; + static deserializeBinaryFromReader(message: AnnotatedMessage, reader: jspb.BinaryReader): AnnotatedMessage; +} + +export namespace AnnotatedMessage { + export type AsObject = { + myunit64: string, + myint64: string, + myfixed64: string, + mysint64: string, + mysfixed64: string, + } +} + diff --git a/examples/generated/proto/examplecom/annotations_pb.js b/examples/generated/proto/examplecom/annotations_pb.js new file mode 100644 index 00000000..c0698227 --- /dev/null +++ b/examples/generated/proto/examplecom/annotations_pb.js @@ -0,0 +1,265 @@ +/** + * @fileoverview + * @enhanceable + * @suppress {messageConventions} JS Compiler reports an error if a variable or + * field starts with 'MSG_' and isn't a translatable message. + * @public + */ +// GENERATED CODE -- DO NOT EDIT! + +var jspb = require('google-protobuf'); +var goog = jspb; +var global = Function('return this')(); + +goog.exportSymbol('proto.examplecom.AnnotatedMessage', null, global); + +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.examplecom.AnnotatedMessage = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.examplecom.AnnotatedMessage, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.examplecom.AnnotatedMessage.displayName = 'proto.examplecom.AnnotatedMessage'; +} + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.examplecom.AnnotatedMessage.prototype.toObject = function(opt_includeInstance) { + return proto.examplecom.AnnotatedMessage.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.examplecom.AnnotatedMessage} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.examplecom.AnnotatedMessage.toObject = function(includeInstance, msg) { + var f, obj = { + myunit64: jspb.Message.getFieldWithDefault(msg, 1, "0"), + myint64: jspb.Message.getFieldWithDefault(msg, 2, "0"), + myfixed64: jspb.Message.getFieldWithDefault(msg, 3, "0"), + mysint64: jspb.Message.getFieldWithDefault(msg, 4, "0"), + mysfixed64: jspb.Message.getFieldWithDefault(msg, 5, "0") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.examplecom.AnnotatedMessage} + */ +proto.examplecom.AnnotatedMessage.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.examplecom.AnnotatedMessage; + return proto.examplecom.AnnotatedMessage.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.examplecom.AnnotatedMessage} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.examplecom.AnnotatedMessage} + */ +proto.examplecom.AnnotatedMessage.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setMyunit64(value); + break; + case 2: + var value = /** @type {string} */ (reader.readInt64String()); + msg.setMyint64(value); + break; + case 3: + var value = /** @type {string} */ (reader.readFixed64String()); + msg.setMyfixed64(value); + break; + case 4: + var value = /** @type {string} */ (reader.readSint64String()); + msg.setMysint64(value); + break; + case 5: + var value = /** @type {string} */ (reader.readSfixed64String()); + msg.setMysfixed64(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.examplecom.AnnotatedMessage.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.examplecom.AnnotatedMessage.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.examplecom.AnnotatedMessage} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.examplecom.AnnotatedMessage.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getMyunit64(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 1, + f + ); + } + f = message.getMyint64(); + if (parseInt(f, 10) !== 0) { + writer.writeInt64String( + 2, + f + ); + } + f = message.getMyfixed64(); + if (parseInt(f, 10) !== 0) { + writer.writeFixed64String( + 3, + f + ); + } + f = message.getMysint64(); + if (parseInt(f, 10) !== 0) { + writer.writeSint64String( + 4, + f + ); + } + f = message.getMysfixed64(); + if (parseInt(f, 10) !== 0) { + writer.writeSfixed64String( + 5, + f + ); + } +}; + + +/** + * optional uint64 myUnit64 = 1; + * @return {string} + */ +proto.examplecom.AnnotatedMessage.prototype.getMyunit64 = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "0")); +}; + + +/** @param {string} value */ +proto.examplecom.AnnotatedMessage.prototype.setMyunit64 = function(value) { + jspb.Message.setProto3StringIntField(this, 1, value); +}; + + +/** + * optional int64 myInt64 = 2; + * @return {string} + */ +proto.examplecom.AnnotatedMessage.prototype.getMyint64 = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); +}; + + +/** @param {string} value */ +proto.examplecom.AnnotatedMessage.prototype.setMyint64 = function(value) { + jspb.Message.setProto3StringIntField(this, 2, value); +}; + + +/** + * optional fixed64 myFixed64 = 3; + * @return {string} + */ +proto.examplecom.AnnotatedMessage.prototype.getMyfixed64 = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "0")); +}; + + +/** @param {string} value */ +proto.examplecom.AnnotatedMessage.prototype.setMyfixed64 = function(value) { + jspb.Message.setProto3StringIntField(this, 3, value); +}; + + +/** + * optional sint64 mySint64 = 4; + * @return {string} + */ +proto.examplecom.AnnotatedMessage.prototype.getMysint64 = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "0")); +}; + + +/** @param {string} value */ +proto.examplecom.AnnotatedMessage.prototype.setMysint64 = function(value) { + jspb.Message.setProto3StringIntField(this, 4, value); +}; + + +/** + * optional sfixed64 mySfixed64 = 5; + * @return {string} + */ +proto.examplecom.AnnotatedMessage.prototype.getMysfixed64 = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, "0")); +}; + + +/** @param {string} value */ +proto.examplecom.AnnotatedMessage.prototype.setMysfixed64 = function(value) { + jspb.Message.setProto3StringIntField(this, 5, value); +}; + + +goog.object.extend(exports, proto.examplecom); diff --git a/examples/generated/proto/examplecom/annotations_pb_service.d.ts b/examples/generated/proto/examplecom/annotations_pb_service.d.ts new file mode 100644 index 00000000..414651f0 --- /dev/null +++ b/examples/generated/proto/examplecom/annotations_pb_service.d.ts @@ -0,0 +1,3 @@ +// package: examplecom +// file: proto/examplecom/annotations.proto + diff --git a/examples/generated/proto/examplecom/annotations_pb_service.js b/examples/generated/proto/examplecom/annotations_pb_service.js new file mode 100644 index 00000000..414651f0 --- /dev/null +++ b/examples/generated/proto/examplecom/annotations_pb_service.js @@ -0,0 +1,3 @@ +// package: examplecom +// file: proto/examplecom/annotations.proto + diff --git a/package-lock.json b/package-lock.json index 0a646d85..3ab159c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -291,9 +291,9 @@ } }, "google-protobuf": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.5.0.tgz", - "integrity": "sha1-uMxjx02DRXvYqakEUDyO+ya8ozk=" + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.6.1.tgz", + "integrity": "sha512-SJYemeX5GjDLPnadcmCNQePQHCS4Hl5fOcI/JawqDIYFhCmrtYAjcx/oTQx/Wi8UuCuZQhfvftbmPePPAYHFtA==" }, "growl": { "version": "1.10.5", diff --git a/package.json b/package.json index cf0b2ac6..15dc1ed1 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "author": "Improbable", "license": "Apache-2.0", "dependencies": { - "google-protobuf": "^3.5.0" + "google-protobuf": "^3.6.1" }, "devDependencies": { "@types/chai": "^3.5.2", diff --git a/proto/examplecom/annotations.proto b/proto/examplecom/annotations.proto new file mode 100644 index 00000000..5cd6fbc7 --- /dev/null +++ b/proto/examplecom/annotations.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package examplecom; + +message AnnotatedMessage { + uint64 myUnit64 = 1 [jstype = JS_STRING]; + int64 myInt64 = 2 [jstype = JS_STRING]; + fixed64 myFixed64 = 3 [jstype = JS_STRING]; + sint64 mySint64 = 4 [jstype = JS_STRING]; + sfixed64 mySfixed64 = 5 [jstype = JS_STRING]; +} diff --git a/src/ts/message.ts b/src/ts/message.ts index a1485901..58639b1c 100644 --- a/src/ts/message.ts +++ b/src/ts/message.ts @@ -3,12 +3,16 @@ import { withinNamespaceFromExportEntry, normaliseFieldObjectName } from "../util"; import {ExportMap} from "../ExportMap"; -import {FieldDescriptorProto, FileDescriptorProto, DescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import { + FieldDescriptorProto, FileDescriptorProto, DescriptorProto, + FieldOptions +} from "google-protobuf/google/protobuf/descriptor_pb"; import {MESSAGE_TYPE, BYTES_TYPE, ENUM_TYPE, getFieldType, getTypeName} from "./FieldTypes"; import {Printer} from "../Printer"; import {printEnum} from "./enum"; import {printOneOfDecl} from "./oneof"; import {printExtension} from "./extensions"; +import JSType = FieldOptions.JSType; function hasFieldPresence(field: FieldDescriptorProto, fileDescriptor: FileDescriptorProto): boolean { if (field.getLabel() === FieldDescriptorProto.Label.LABEL_REPEATED) { @@ -104,7 +108,20 @@ export function printMessage(fileName: string, exportMap: ExportMap, messageDesc exportType = filePathToPseudoNamespace(fieldEnumType.fileName) + "." + withinNamespace; } } else { - exportType = getTypeName(type); + if (field.getOptions() && field.getOptions().hasJstype()) { + switch (field.getOptions().getJstype()) { + case JSType.JS_NUMBER: + exportType = "number"; + break; + case JSType.JS_STRING: + exportType = "string"; + break; + default: + exportType = getTypeName(type); + } + } else { + exportType = getTypeName(type); + } } let hasClearMethod = false; diff --git a/test/integration/annotations.ts b/test/integration/annotations.ts new file mode 100644 index 00000000..1061efac --- /dev/null +++ b/test/integration/annotations.ts @@ -0,0 +1,24 @@ +import { assert } from "chai"; +import { AnnotatedMessage } from "../../examples/generated/proto/examplecom/annotations_pb"; + +describe("annotations", () => { + it("should detect and honor the `jstype` annotation", () => { + const unsafeInteger = Number.MAX_VALUE.toString() + "9999"; + const msg = new AnnotatedMessage(); + + msg.setMyfixed64(unsafeInteger); + assert.strictEqual(msg.getMyfixed64(), unsafeInteger); + + msg.setMyunit64(unsafeInteger); + assert.strictEqual(msg.getMyunit64(), unsafeInteger); + + msg.setMyint64(unsafeInteger); + assert.strictEqual(msg.getMyint64(), unsafeInteger); + + msg.setMysfixed64(unsafeInteger); + assert.strictEqual(msg.getMysfixed64(), unsafeInteger); + + msg.setMysint64(unsafeInteger); + assert.strictEqual(msg.getMysint64(), unsafeInteger); + }); +});