diff --git a/src/common.js b/src/common.js index 65760abd3..4bef2ef10 100644 --- a/src/common.js +++ b/src/common.js @@ -1,6 +1,8 @@ "use strict"; module.exports = common; +var Type = require("./type"); + /** * Provides common type definitions. * Can also be used to provide additional google types or your own custom types. @@ -51,15 +53,7 @@ common("any", { type: "bytes", id: 2 } - }/*, - options: Object.create({ - __fromObject: function(object) { - return this.fromObject(object); - }, - __toObject: function(options) { - return this.toObject(options); - } - })*/ + } } }); diff --git a/src/index-minimal.js b/src/index-minimal.js index ff2d15c23..fb5908847 100644 --- a/src/index-minimal.js +++ b/src/index-minimal.js @@ -19,6 +19,7 @@ protobuf.BufferReader = require("./reader_buffer"); protobuf.util = require("./util/minimal"); protobuf.rpc = require("./rpc"); protobuf.roots = require("./roots"); +protobuf.wrappers = require("./wrappers"); protobuf.configure = configure; /* istanbul ignore next */ diff --git a/src/type.js b/src/type.js index 215609083..78370cd8f 100644 --- a/src/type.js +++ b/src/type.js @@ -17,7 +17,8 @@ var Enum = require("./enum"), encoder = require("./encoder"), decoder = require("./decoder"), verifier = require("./verifier"), - converter = require("./converter"); + converter = require("./converter"), + wrappers = require("./wrappers"); /** * Constructs a new reflected message type instance. @@ -428,10 +429,13 @@ Type.prototype.create = function create(properties) { Type.prototype.setup = function setup() { // Sets up everything at once so that the prototype chain does not have to be re-evaluated // multiple times (V8, soft-deopt prototype-check). + var fullName = this.fullName, types = []; for (var i = 0; i < /* initializes */ this.fieldsArray.length; ++i) types.push(this._fieldsArray[i].resolve().resolvedType); + + // Replace setup methods with type-specific generated functions this.encode = encoder(this).eof(fullName + "$encode", { Writer : Writer, types : types, @@ -450,14 +454,25 @@ Type.prototype.setup = function setup() { types : types, util : util }); - if (this.options && this.options.__formObject) - this.fromObject = this.options.__formObject.bind({ fromObject: this.fromObject }); this.toObject = converter.toObject(this).eof(fullName + "$toObject", { types : types, util : util }); - if (this.options && this.options.__toObject) - this.toObject = this.options.__toObject.bind({ toObject: this.toObject }); + + // Inject custom wrappers for common types + var wrapper = wrappers[fullName]; + if (wrapper) { + var originalThis = Object.create(this); + // if (wrapper.fromObject) { + originalThis.fromObject = this.fromObject; + this.fromObject = wrapper.fromObject.bind(originalThis); + // } + // if (wrapper.toObject) { + originalThis.toObject = this.toObject; + this.toObject = wrapper.toObject.bind(originalThis); + // } + } + return this; }; diff --git a/src/util.js b/src/util.js index 82722cb19..33ad4176e 100644 --- a/src/util.js +++ b/src/util.js @@ -85,7 +85,7 @@ util.decorateType = function decorateType(ctor, typeName) { return ctor.$type; } - /* istanbul ignore if */ + /* istanbul ignore next */ if (!Type) Type = require("./type"); @@ -109,7 +109,7 @@ util.decorateEnum = function decorateEnum(object) { if (object.$type) return object.$type; - /* istanbul ignore if */ + /* istanbul ignore next */ if (!Enum) Enum = require("./enum"); diff --git a/src/util/minimal.js b/src/util/minimal.js index 43999f95a..4992dbba6 100644 --- a/src/util/minimal.js +++ b/src/util/minimal.js @@ -376,9 +376,9 @@ util.oneOfSetter = function setOneOf(fieldNames) { /** * Default conversion options used for {@link Message#toJSON} implementations. - * + * * These options are close to proto3's JSON mapping with the exception that internal types like Any are handled just like messages. More precisely: - * + * * - Longs become strings * - Enums become string keys * - Bytes become base64 encoded strings @@ -386,7 +386,7 @@ util.oneOfSetter = function setOneOf(fieldNames) { * - Maps become plain objects with all string keys * - Repeated fields become arrays * - NaN and Infinity for float and double fields become strings - * + * * @type {ConversionOptions} * @see https://developers.google.com/protocol-buffers/docs/proto3?hl=en#json */ diff --git a/src/wrappers.js b/src/wrappers.js new file mode 100644 index 000000000..46d75b940 --- /dev/null +++ b/src/wrappers.js @@ -0,0 +1,76 @@ +"use strict"; + +/** + * Wrappers for common types. + * @namespace + */ +var wrappers = exports; + +var util = require("./util/minimal"); + +/** + * From object converter part of a {@link Wrapper}. + * @typedef WrapperFromObjectConverter + * @type {function} + * @param {Object.} object Plain object + * @returns {Message<{}>} + * @this Type + */ + +/** + * To object converter part of a {@link Wrapper}. + * @typedef WrapperToObjectConverter + * @type {function} + * @param {Message<{}>} message Message instance + * @param {ConversionOptions=} options Conversion options + * @returns {Object.} + * @this Type + */ + +/** + * Common type wrapper part of {@link wrappers}. + * @typedef Wrapper + * @type {Object} + * @property {WrapperFromObjectConverter} [fromObject] From object converter + * @property {WrapperToObjectConverter} [toObject] To object converter + */ + +/** + * Custom wrapper for Any. + * @type {Wrapper} + */ +wrappers[".google.protobuf.Any"] = { + + fromObject: function(object) { + + // unwrap value type if mapped + if (object && object["@type"]) { + var type = this.lookup(object["@type"]); + /* istanbul ignore else */ + if (type) + return type.fromObject(object); + } + + return this.fromObject(object); + }, + + toObject: function(message, options) { + + // decode value if requested and unmapped + if (options && options.json && message.type_url && message.value) { + var type = this.lookup(message.type_url); + /* istanbul ignore else */ + if (type) + message = type.decode(message.value); + } + + // wrap value if unmapped + if (!(message instanceof this.ctor)) { + var object = message.toObject(options); + object["@type"] = message.$type.fullName; + return object; + } + + return this.toObject(message, options); + } +}; diff --git a/tests/comp_google_protobuf_any.js b/tests/comp_google_protobuf_any.js new file mode 100644 index 000000000..12b86ec89 --- /dev/null +++ b/tests/comp_google_protobuf_any.js @@ -0,0 +1,60 @@ +var tape = require("tape"); + +var protobuf = require(".."); + +var root = protobuf.Root.fromJSON({ + nested: { + Foo: { + fields: { + foo: { + id: 1, + type: "google.protobuf.Any" + } + } + }, + Bar: { + fields: { + bar: { + id: 1, + type: "string" + } + } + } + } +}).addJSON(protobuf.common["google/protobuf/any.proto"].nested).resolveAll(); + +var Any = root.lookupType(".google.protobuf.Any"), + Foo = root.lookupType(".Foo"), + Bar = root.lookupType(".Bar"); + +tape.test("google.protobuf.Any", function(test) { + + var foo = Foo.fromObject({ + foo: { + type_url: ".Bar", + value: [1 << 3 | 2, 1, 97] // value = "a" + } + }); + test.ok(foo.foo instanceof Any.ctor, "should keep explicit Any in fromObject"); + test.same(foo.foo, { type_url: ".Bar", value: [10, 1, 97] }, "should keep explicit Any in fromObject properly"); + + var obj = Foo.toObject(foo); + test.same(obj.foo, { type_url: ".Bar", value: [10, 1, 97] }, "should keep explicit Any in toObject properly"); + + obj = Foo.toObject(foo, { json: true }); + test.same(obj.foo, { "@type": ".Bar", bar: "a" }, "should decode explicitly Any in toObject if requested"); + + foo = Foo.fromObject({ + foo: { + "@type": ".Bar", + bar: "a" + } + }); + test.ok(foo.foo instanceof Bar.ctor, "should unwrap wrapped Bar in fromObject"); + test.same(foo.foo, { bar: "a" }, "should unwrap wrapper Bar in fromObject properly"); + + obj = Foo.toObject(foo); + test.same(obj.foo, { "@type": ".Bar", bar: "a" }, "should wrap Bar in toObject properly"); + + test.end(); +});