diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/binary/constants.js | 4 | ||||
-rw-r--r-- | js/binary/decoder_test.js | 3 | ||||
-rw-r--r-- | js/binary/reader_test.js | 25 | ||||
-rw-r--r-- | js/binary/writer.js | 36 | ||||
-rw-r--r-- | js/map.js | 482 | ||||
-rw-r--r-- | js/message.js | 97 | ||||
-rw-r--r-- | js/message_test.js | 33 | ||||
-rw-r--r-- | js/package.json | 16 | ||||
-rw-r--r-- | js/test.proto | 5 | ||||
-rw-r--r-- | js/testbinary.proto | 27 |
10 files changed, 689 insertions, 39 deletions
diff --git a/js/binary/constants.js b/js/binary/constants.js index 836216bf..ef5fecdd 100644 --- a/js/binary/constants.js +++ b/js/binary/constants.js @@ -141,8 +141,8 @@ jspb.ReaderFunction; /** * A writer function serializes a message to a BinaryWriter. - * @typedef {!function(!jspb.Message, !jspb.BinaryWriter):void | - * !function(!jspb.ConstBinaryMessage, !jspb.BinaryWriter):void} + * @typedef {function((!jspb.Message|!jspb.ConstBinaryMessage), + * !jspb.BinaryWriter):void} */ jspb.WriterFunction; diff --git a/js/binary/decoder_test.js b/js/binary/decoder_test.js index d045e912..ac312648 100644 --- a/js/binary/decoder_test.js +++ b/js/binary/decoder_test.js @@ -147,9 +147,8 @@ function doTestSignedValue(readValue, describe('binaryDecoderTest', function() { /** * Tests the decoder instance cache. - * @suppress {visibility} */ - it('testInstanceCache', function() { + it('testInstanceCache', /** @suppress {visibility} */ function() { // Empty the instance caches. jspb.BinaryDecoder.instanceCache_ = []; diff --git a/js/binary/reader_test.js b/js/binary/reader_test.js index db674cf8..95711385 100644 --- a/js/binary/reader_test.js +++ b/js/binary/reader_test.js @@ -52,9 +52,8 @@ goog.require('jspb.BinaryWriter'); describe('binaryReaderTest', function() { /** * Tests the reader instance cache. - * @suppress {visibility} */ - it('testInstanceCaches', function() { + it('testInstanceCaches', /** @suppress {visibility} */ function() { var writer = new jspb.BinaryWriter(); var dummyMessage = /** @type {!jspb.BinaryMessage} */({}); writer.writeMessage(1, dummyMessage, goog.nullFunction); @@ -131,9 +130,8 @@ describe('binaryReaderTest', function() { /** * Verifies that misuse of the reader class triggers assertions. - * @suppress {checkTypes|visibility} */ - it('testReadErrors', function() { + it('testReadErrors', /** @suppress {checkTypes|visibility} */ function() { // Calling readMessage on a non-delimited field should trigger an // assertion. var reader = jspb.BinaryReader.alloc([8, 1]); @@ -200,7 +198,7 @@ describe('binaryReaderTest', function() { * @private * @suppress {missingProperties} */ - function doTestUnsignedField_(readField, + var doTestUnsignedField_ = function(readField, writeField, epsilon, upperLimit, filter) { assertNotNull(readField); assertNotNull(writeField); @@ -252,7 +250,7 @@ describe('binaryReaderTest', function() { * @private * @suppress {missingProperties} */ - function doTestSignedField_(readField, + var doTestSignedField_ = function(readField, writeField, epsilon, lowerLimit, upperLimit, filter) { var writer = new jspb.BinaryWriter(); @@ -321,12 +319,12 @@ describe('binaryReaderTest', function() { * Tests fields that use varint encoding. */ it('testVarintFields', function() { - assertNotNull(jspb.BinaryReader.prototype.readUint32); - assertNotNull(jspb.BinaryReader.prototype.writeUint32); - assertNotNull(jspb.BinaryReader.prototype.readUint64); - assertNotNull(jspb.BinaryReader.prototype.writeUint64); - assertNotNull(jspb.BinaryReader.prototype.readBool); - assertNotNull(jspb.BinaryReader.prototype.writeBool); + assertNotUndefined(jspb.BinaryReader.prototype.readUint32); + assertNotUndefined(jspb.BinaryWriter.prototype.writeUint32); + assertNotUndefined(jspb.BinaryReader.prototype.readUint64); + assertNotUndefined(jspb.BinaryWriter.prototype.writeUint64); + assertNotUndefined(jspb.BinaryReader.prototype.readBool); + assertNotUndefined(jspb.BinaryWriter.prototype.writeBool); doTestUnsignedField_( jspb.BinaryReader.prototype.readUint32, jspb.BinaryWriter.prototype.writeUint32, @@ -369,8 +367,7 @@ describe('binaryReaderTest', function() { var bytesCount = (hexString.length + 1) / 3; var bytes = new Uint8Array(bytesCount); for (var i = 0; i < bytesCount; i++) { - byte = parseInt(hexString.substring(i * 3, i * 3 + 2), 16); - bytes[i] = byte; + bytes[i] = parseInt(hexString.substring(i * 3, i * 3 + 2), 16); } var reader = jspb.BinaryReader.alloc(bytes); reader.nextField(); diff --git a/js/binary/writer.js b/js/binary/writer.js index be4478ee..3eb2f1bd 100644 --- a/js/binary/writer.js +++ b/js/binary/writer.js @@ -717,11 +717,19 @@ jspb.BinaryWriter.prototype.writeBytes = function(field, value) { /** * Writes a message to the buffer. - * @template MessageType * @param {number} field The field number. * @param {?MessageType} value The message to write. - * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value - * to write and the writer to write it with. + * @param {function(MessageTypeNonNull, !jspb.BinaryWriter)} writerCallback + * Will be invoked with the value to write and the writer to write it with. + * @template MessageType + * Use go/closure-ttl to declare a non-nullable version of MessageType. Replace + * the null in blah|null with none. This is necessary because the compiler will + * infer MessageType to be nullable if the value parameter is nullable. + * @template MessageTypeNonNull := + * cond(isUnknown(MessageType), unknown(), + * mapunion(MessageType, (X) => + * cond(eq(X, 'null'), none(), X))) + * =: */ jspb.BinaryWriter.prototype.writeMessage = function( field, value, writerCallback) { @@ -735,12 +743,20 @@ jspb.BinaryWriter.prototype.writeMessage = function( /** * Writes a group message to the buffer. * - * @template MessageType * @param {number} field The field number. * @param {?MessageType} value The message to write, wrapped with START_GROUP / * END_GROUP tags. Will be a no-op if 'value' is null. - * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value - * to write and the writer to write it with. + * @param {function(MessageTypeNonNull, !jspb.BinaryWriter)} writerCallback + * Will be invoked with the value to write and the writer to write it with. + * @template MessageType + * Use go/closure-ttl to declare a non-nullable version of MessageType. Replace + * the null in blah|null with none. This is necessary because the compiler will + * infer MessageType to be nullable if the value parameter is nullable. + * @template MessageTypeNonNull := + * cond(isUnknown(MessageType), unknown(), + * mapunion(MessageType, (X) => + * cond(eq(X, 'null'), none(), X))) + * =: */ jspb.BinaryWriter.prototype.writeGroup = function( field, value, writerCallback) { @@ -1122,8 +1138,8 @@ jspb.BinaryWriter.prototype.writeRepeatedBytes = function(field, value) { * @param {number} field The field number. * @param {?Array.<MessageType>} value The array of messages to * write. - * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value - * to write and the writer to write it with. + * @param {function(MessageType, !jspb.BinaryWriter)} writerCallback + * Will be invoked with the value to write and the writer to write it with. */ jspb.BinaryWriter.prototype.writeRepeatedMessage = function( field, value, writerCallback) { @@ -1142,8 +1158,8 @@ jspb.BinaryWriter.prototype.writeRepeatedMessage = function( * @param {number} field The field number. * @param {?Array.<MessageType>} value The array of messages to * write. - * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value - * to write and the writer to write it with. + * @param {function(MessageType, !jspb.BinaryWriter)} writerCallback + * Will be invoked with the value to write and the writer to write it with. */ jspb.BinaryWriter.prototype.writeRepeatedGroup = function( field, value, writerCallback) { diff --git a/js/map.js b/js/map.js new file mode 100644 index 00000000..821765ec --- /dev/null +++ b/js/map.js @@ -0,0 +1,482 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +goog.provide('jspb.Map'); + +goog.require('goog.asserts'); + +goog.forwardDeclare('jspb.BinaryReader'); +goog.forwardDeclare('jspb.BinaryWriter'); + + + +/** + * Constructs a new Map. A Map is a container that is used to implement map + * fields on message objects. It closely follows the ES6 Map API; however, + * it is distinct because we do not want to depend on external polyfills or + * on ES6 itself. + * + * This constructor should only be called from generated message code. It is not + * intended for general use by library consumers. The callback function + * arguments are references to methods in `BinaryReader` and `BinaryWriter`, as + * well as constructors and reader/writer methods in submessage types if + * appropriate, that are used for binary serialization and parsing. + * + * @template K, V + * + * @param {!Array<!Array<!Object>>} arr + * + * @param {function(this:jspb.BinaryWriter,number,K)=} opt_keyWriterFn + * The method on BinaryWriter that writes type K to the stream. + * + * @param {function(this:jspb.BinaryReader):K=} opt_keyReaderFn + * The method on BinaryReader that reads type K from the stream. + * + * @param {function(this:jspb.BinaryWriter,number,V)| + * function(this:jspb.BinaryReader,V,?)=} opt_valueWriterFn + * The method on BinaryWriter that writes type V to the stream. May be + * writeMessage, in which case the second callback arg form is used. + * + * @param {function(this:jspb.BinaryReader):V| + * function(this:jspb.BinaryReader,V, + * function(V,!jspb.BinaryReader))=} opt_valueReaderFn + * The method on BinaryReader that reads type V from the stream. May be + * readMessage, in which case the second callback arg form is used. + * + * @param {?function(new:V)|function(new:V,?)=} opt_valueCtor + * The constructor for type V, if type V is a message type. + * + * @param {?function(V,!jspb.BinaryWriter)=} opt_valueWriterCallback + * The BinaryWriter serialization callback for type V, if V is a message + * type. + * + * @param {?function(V,!jspb.BinaryReader)=} opt_valueReaderCallback + * The BinaryReader parsing callback for type V, if V is a message type. + * + * @constructor + * @struct + */ +jspb.Map = function( + arr, opt_keyWriterFn, opt_keyReaderFn, opt_valueWriterFn, opt_valueReaderFn, + opt_valueCtor, opt_valueWriterCallback, opt_valueReaderCallback) { + + /** @const @private */ + this.arr_ = arr; + /** @const @private */ + this.keyWriterFn_ = opt_keyWriterFn; + /** @const @private */ + this.keyReaderFn_ = opt_keyReaderFn; + /** @const @private */ + this.valueWriterFn_ = opt_valueWriterFn; + /** @const @private */ + this.valueReaderFn_ = opt_valueReaderFn; + /** @const @private */ + this.valueCtor_ = opt_valueCtor; + /** @const @private */ + this.valueWriterCallback_ = opt_valueWriterCallback; + /** @const @private */ + this.valueReaderCallback_ = opt_valueReaderCallback; + + /** @type {!Object<string, !jspb.Map.Entry_<K,V>>} @private */ + this.map_ = {}; + + /** + * Is `this.arr_ updated with respect to `this.map_`? + * @type {boolean} + */ + this.arrClean = true; + + if (this.arr_.length > 0) { + this.loadFromArray_(); + } +}; + + +/** + * Load initial content from underlying array. + * @private + */ +jspb.Map.prototype.loadFromArray_ = function() { + for (var i = 0; i < this.arr_.length; i++) { + var record = this.arr_[i]; + var key = record[0]; + var value = record[1]; + this.map_[key.toString()] = new jspb.Map.Entry_(key, value); + } + this.arrClean = true; +}; + + +/** + * Synchronize content to underlying array, if needed, and return it. + * @return {!Array<!Array<!Object>>} + */ +jspb.Map.prototype.toArray = function() { + if (this.arrClean) { + if (this.valueCtor_) { + // We need to recursively sync maps in submessages to their arrays. + var m = this.map_; + for (var p in m) { + if (Object.prototype.hasOwnProperty.call(m, p)) { + m[p].valueWrapper.toArray(); + } + } + } + } else { + // Delete all elements. + this.arr_.length = 0; + var strKeys = this.stringKeys_(); + // Output keys in deterministic (sorted) order. + strKeys.sort(); + for (var i = 0; i < strKeys.length; i++) { + var entry = this.map_[strKeys[i]]; + var valueWrapper = /** @type {!Object} */ (entry.valueWrapper); + if (valueWrapper) { + valueWrapper.toArray(); + } + this.arr_.push([entry.key, entry.value]); + } + this.arrClean = true; + } + return this.arr_; +}; + + +/** + * Helper: return an iterator over an array. + * @template T + * @param {!Array<T>} arr the array + * @return {!Iterator<T>} an iterator + * @private + */ +jspb.Map.arrayIterator_ = function(arr) { + var idx = 0; + return /** @type {!Iterator} */ ({ + next: function() { + if (idx < arr.length) { + return { done: false, value: arr[idx++] }; + } else { + return { done: true }; + } + } + }); +}; + + +/** + * Returns the map's length (number of key/value pairs). + * @return {number} + */ +jspb.Map.prototype.getLength = function() { + return this.stringKeys_().length; +}; + + +/** + * Clears the map. + */ +jspb.Map.prototype.clear = function() { + this.map_ = {}; + this.arrClean = false; +}; + + +/** + * Deletes a particular key from the map. + * N.B.: differs in name from ES6 Map's `delete` because IE8 does not support + * reserved words as property names. + * @this {jspb.Map} + * @param {K} key + * @return {boolean} Whether any entry with this key was deleted. + */ +jspb.Map.prototype.del = function(key) { + var keyValue = key.toString(); + var hadKey = this.map_.hasOwnProperty(keyValue); + delete this.map_[keyValue]; + this.arrClean = false; + return hadKey; +}; + + +/** + * Returns an array of [key, value] pairs in the map. + * + * This is redundant compared to the plain entries() method, but we provide this + * to help out Angular 1.x users. Still evaluating whether this is the best + * option. + * + * @return {!Array<K|V>} + */ +jspb.Map.prototype.getEntryList = function() { + var entries = []; + var strKeys = this.stringKeys_(); + strKeys.sort(); + for (var i = 0; i < strKeys.length; i++) { + var entry = this.map_[strKeys[i]]; + entries.push([entry.key, entry.value]); + } + return entries; +}; + + +/** + * Returns an iterator over [key, value] pairs in the map. + * Closure compiler sadly doesn't support tuples, ie. Iterator<[K,V]>. + * @return {!Iterator<!Array<K|V>>} + * The iterator + */ +jspb.Map.prototype.entries = function() { + var entries = []; + var strKeys = this.stringKeys_(); + strKeys.sort(); + for (var i = 0; i < strKeys.length; i++) { + var entry = this.map_[strKeys[i]]; + entries.push([entry.key, this.wrapEntry_(entry)]); + } + return jspb.Map.arrayIterator_(entries); +}; + + +/** + * Returns an iterator over keys in the map. + * @return {!Iterator<K>} The iterator + */ +jspb.Map.prototype.keys = function() { + var keys = []; + var strKeys = this.stringKeys_(); + strKeys.sort(); + for (var i = 0; i < strKeys.length; i++) { + var entry = this.map_[strKeys[i]]; + keys.push(entry.key); + } + return jspb.Map.arrayIterator_(keys); +}; + + +/** + * Returns an iterator over values in the map. + * @return {!Iterator<V>} The iterator + */ +jspb.Map.prototype.values = function() { + var values = []; + var strKeys = this.stringKeys_(); + strKeys.sort(); + for (var i = 0; i < strKeys.length; i++) { + var entry = this.map_[strKeys[i]]; + values.push(this.wrapEntry_(entry)); + } + return jspb.Map.arrayIterator_(values); +}; + + +/** + * Iterates over entries in the map, calling a function on each. + * @template T + * @param {function(this:T, V, K, ?jspb.Map<K, V>)} cb + * @param {T=} opt_thisArg + */ +jspb.Map.prototype.forEach = function(cb, opt_thisArg) { + var strKeys = this.stringKeys_(); + strKeys.sort(); + for (var i = 0; i < strKeys.length; i++) { + var entry = this.map_[strKeys[i]]; + cb.call(opt_thisArg, this.wrapEntry_(entry), entry.key, this); + } +}; + + +/** + * Sets a key in the map to the given value. + * @param {K} key The key + * @param {V} value The value + * @return {!jspb.Map<K,V>} + */ +jspb.Map.prototype.set = function(key, value) { + var entry = new jspb.Map.Entry_(key); + if (this.valueCtor_) { + entry.valueWrapper = value; + // .toArray() on a message returns a reference to the underlying array + // rather than a copy. + entry.value = value.toArray(); + } else { + entry.value = value; + } + this.map_[key.toString()] = entry; + this.arrClean = false; + return this; +}; + + +/** + * Helper: lazily construct a wrapper around an entry, if needed, and return the + * user-visible type. + * @param {!jspb.Map.Entry_<K,V>} entry + * @return {V} + * @private + */ +jspb.Map.prototype.wrapEntry_ = function(entry) { + if (this.valueCtor_) { + if (!entry.valueWrapper) { + entry.valueWrapper = new this.valueCtor_(entry.value); + } + return /** @type {V} */ (entry.valueWrapper); + } else { + return entry.value; + } +}; + + +/** + * Gets the value corresponding to a key in the map. + * @param {K} key + * @return {V|undefined} The value, or `undefined` if key not present + */ +jspb.Map.prototype.get = function(key) { + var keyValue = key.toString(); + var entry = this.map_[keyValue]; + if (entry) { + return this.wrapEntry_(entry); + } else { + return undefined; + } +}; + + +/** + * Determines whether the given key is present in the map. + * @param {K} key + * @return {boolean} `true` if the key is present + */ +jspb.Map.prototype.has = function(key) { + var keyValue = key.toString(); + return (keyValue in this.map_); +}; + + +/** + * Write this Map field in wire format to a BinaryWriter, using the given field + * number. + * @param {number} fieldNumber + * @param {!jspb.BinaryWriter} writer + */ +jspb.Map.prototype.serializeBinary = function(fieldNumber, writer) { + var strKeys = this.stringKeys_(); + strKeys.sort(); + for (var i = 0; i < strKeys.length; i++) { + var entry = this.map_[strKeys[i]]; + writer.beginSubMessage(fieldNumber); + this.keyWriterFn_.call(writer, 1, entry.key); + if (this.valueCtor_) { + this.valueWriterFn_.call(writer, 2, this.wrapEntry_(entry), + this.valueWriterCallback_); + } else { + this.valueWriterFn_.call(writer, 2, entry.value); + } + writer.endSubMessage(); + } +}; + + +/** + * Read one key/value message from the given BinaryReader. Compatible as the + * `reader` callback parameter to jspb.BinaryReader.readMessage, to be called + * when a key/value pair submessage is encountered. + * @param {!jspb.Map} map + * @param {!jspb.BinaryReader} reader + */ +jspb.Map.deserializeBinary = function(map, reader) { + var key = undefined; + var value = undefined; + + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + if (field == 1) { + // Key. + key = map.keyReaderFn_.call(reader); + } else if (field == 2) { + // Value. + if (map.valueCtor_) { + value = new map.valueCtor_(); + map.valueReaderFn_.call(reader, value, map.valueReaderCallback_); + } else { + value = map.valueReaderFn_.call(reader); + } + } + } + + goog.asserts.assert(key != undefined); + goog.asserts.assert(value != undefined); + map.set(key, value); +}; + + +/** + * Helper: compute the list of all stringified keys in the underlying Object + * map. + * @return {!Array<string>} + * @private + */ +jspb.Map.prototype.stringKeys_ = function() { + var m = this.map_; + var ret = []; + for (var p in m) { + if (Object.prototype.hasOwnProperty.call(m, p)) { + ret.push(p); + } + } + return ret; +}; + + + +/** + * @param {!K} key The entry's key. + * @param {V=} opt_value The entry's value wrapper. + * @constructor + * @struct + * @template K, V + * @private + */ +jspb.Map.Entry_ = function(key, opt_value) { + /** @const {K} */ + this.key = key; + + // The JSPB-serializable value. For primitive types this will be of type V. + // For message types it will be an array. + /** @type {V} */ + this.value = opt_value; + + // Only used for submessage values. + /** @type {V} */ + this.valueWrapper = undefined; +}; diff --git a/js/message.js b/js/message.js index 813e6b8c..3863bac0 100644 --- a/js/message.js +++ b/js/message.js @@ -41,6 +41,7 @@ goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.crypt.base64'); goog.require('goog.json'); +goog.require('jspb.Map'); // Not needed in compilation units that have no protos with xids. goog.forwardDeclare('xid.String'); @@ -371,7 +372,8 @@ jspb.Message.materializeExtensionObject_ = function(msg, suggestedPivot) { // the object is not an array, since arrays are valid field values. // NOTE(lukestebbing): We avoid looking at .length to avoid a JIT bug // in Safari on iOS 8. See the description of CL/86511464 for details. - if (obj && typeof obj == 'object' && !goog.isArray(obj)) { + if (obj && typeof obj == 'object' && !goog.isArray(obj) && + !(jspb.Message.SUPPORTS_UINT8ARRAY_ && obj instanceof Uint8Array)) { msg.pivot_ = foundIndex - msg.arrayIndexOffset_; msg.extensionObject_ = obj; return; @@ -738,6 +740,62 @@ jspb.Message.getFieldProto3 = function(msg, fieldNumber, defaultValue) { /** + * Gets the value of a map field, lazily creating the map container if + * necessary. + * + * This should only be called from generated code, because it requires knowledge + * of serialization/parsing callbacks (which are required by the map at + * construction time, and the map may be constructed here). + * + * The below callbacks are used to allow the map to serialize and parse its + * binary wire format data. Their purposes are described in more detail in + * `jspb.Map`'s constructor documentation. + * + * @template K, V + * @param {!jspb.Message} msg + * @param {number} fieldNumber + * @param {boolean|undefined} noLazyCreate + * @param {?=} opt_valueCtor + * @param {function(number,K)=} opt_keyWriterFn + * @param {function():K=} opt_keyReaderFn + * @param {function(number,V)|function(number,V,?)| + * function(number,V,?,?,?,?)=} opt_valueWriterFn + * @param {function():V| + * function(V,function(?,?))=} opt_valueReaderFn + * @param {function(?,?)|function(?,?,?,?,?)=} opt_valueWriterCallback + * @param {function(?,?)=} opt_valueReaderCallback + * @return {!jspb.Map<K, V>|undefined} + * @protected + */ +jspb.Message.getMapField = function(msg, fieldNumber, noLazyCreate, + opt_valueCtor, opt_keyWriterFn, opt_keyReaderFn, opt_valueWriterFn, + opt_valueReaderFn, opt_valueWriterCallback, opt_valueReaderCallback) { + if (!msg.wrappers_) { + msg.wrappers_ = {}; + } + // If we already have a map in the map wrappers, return that. + if (fieldNumber in msg.wrappers_) { + return msg.wrappers_[fieldNumber]; + } else if (noLazyCreate) { + return undefined; + } else { + // Wrap the underlying elements array with a Map. + var arr = jspb.Message.getField(msg, fieldNumber); + if (!arr) { + arr = []; + jspb.Message.setField(msg, fieldNumber, arr); + } + return msg.wrappers_[fieldNumber] = + new jspb.Map( + /** @type {!Array<!Array<!Object>>} */ (arr), + opt_keyWriterFn, opt_keyReaderFn, opt_valueWriterFn, + opt_valueReaderFn, opt_valueCtor, opt_valueWriterCallback, + opt_valueReaderCallback); + } +}; + + +/** * Sets the value of a non-extension field. * @param {!jspb.Message} msg A jspb proto. * @param {number} fieldNumber The field number. @@ -953,12 +1011,45 @@ jspb.Message.toMap = function( /** + * Syncs all map fields' contents back to their underlying arrays. + * @private + */ +jspb.Message.prototype.syncMapFields_ = function() { + // This iterates over submessage, map, and repeated fields, which is intended. + // Submessages can contain maps which also need to be synced. + // + // There is a lot of opportunity for optimization here. For example we could + // statically determine that some messages have no submessages with maps and + // optimize this method away for those just by generating one extra static + // boolean per message type. + if (this.wrappers_) { + for (var fieldNumber in this.wrappers_) { + var val = this.wrappers_[fieldNumber]; + if (goog.isArray(val)) { + for (var i = 0; i < val.length; i++) { + if (val[i]) { + val[i].toArray(); + } + } + } else { + // Works for submessages and maps. + if (val) { + val.toArray(); + } + } + } + } +}; + + +/** * Returns the internal array of this proto. * <p>Note: If you use this array to construct a second proto, the content * would then be partially shared between the two protos. * @return {!Array} The proto represented as an array. */ jspb.Message.prototype.toArray = function() { + this.syncMapFields_(); return this.array; }; @@ -972,6 +1063,7 @@ jspb.Message.prototype.toArray = function() { * @override */ jspb.Message.prototype.toString = function() { + this.syncMapFields_(); return this.array.toString(); }; @@ -1293,6 +1385,9 @@ jspb.Message.clone_ = function(obj) { } return clonedArray; } + if (jspb.Message.SUPPORTS_UINT8ARRAY_ && obj instanceof Uint8Array) { + return new Uint8Array(obj); + } var clone = {}; for (var key in obj) { if ((o = obj[key]) != null) { diff --git a/js/message_test.js b/js/message_test.js index 01add5f1..0b0c0172 100644 --- a/js/message_test.js +++ b/js/message_test.js @@ -34,6 +34,7 @@ goog.setTestOnly(); goog.require('goog.json'); goog.require('goog.testing.asserts'); +goog.require('goog.userAgent'); // CommonJS-LoadFromFile: google-protobuf jspb goog.require('jspb.Message'); @@ -66,6 +67,7 @@ goog.require('proto.jspb.test.Simple1'); goog.require('proto.jspb.test.Simple2'); goog.require('proto.jspb.test.SpecialCases'); goog.require('proto.jspb.test.TestClone'); +goog.require('proto.jspb.test.TestEndsWithBytes'); goog.require('proto.jspb.test.TestGroup'); goog.require('proto.jspb.test.TestGroup1'); goog.require('proto.jspb.test.TestMessageWithOneof'); @@ -438,6 +440,8 @@ describe('Message test suite', function() { }); it('testClone', function() { + var supportsUint8Array = + !goog.userAgent.IE || goog.userAgent.isVersionOrHigher('10'); var original = new proto.jspb.test.TestClone(); original.setStr('v1'); var simple1 = new proto.jspb.test.Simple1(['x1', ['y1', 'z1']]); @@ -445,12 +449,14 @@ describe('Message test suite', function() { var simple3 = new proto.jspb.test.Simple1(['x3', ['y3', 'z3']]); original.setSimple1(simple1); original.setSimple2List([simple2, simple3]); + var bytes1 = supportsUint8Array ? new Uint8Array([1, 2, 3]) : '123'; + original.setBytesField(bytes1); var extension = new proto.jspb.test.CloneExtension(); extension.setExt('e1'); original.setExtension(proto.jspb.test.IsExtension.extField, extension); var clone = original.cloneMessage(); assertArrayEquals(['v1',, ['x1', ['y1', 'z1']],, - [['x2', ['y2', 'z2']], ['x3', ['y3', 'z3']]],,, { 100: [, 'e1'] }], + [['x2', ['y2', 'z2']], ['x3', ['y3', 'z3']]], bytes1,, { 100: [, 'e1'] }], clone.toArray()); clone.setStr('v2'); var simple4 = new proto.jspb.test.Simple1(['a1', ['b1', 'c1']]); @@ -458,18 +464,26 @@ describe('Message test suite', function() { var simple6 = new proto.jspb.test.Simple1(['a3', ['b3', 'c3']]); clone.setSimple1(simple4); clone.setSimple2List([simple5, simple6]); + if (supportsUint8Array) { + clone.getBytesField()[0] = 4; + assertObjectEquals(bytes1, original.getBytesField()); + } + var bytes2 = supportsUint8Array ? new Uint8Array([4, 5, 6]) : '456'; + clone.setBytesField(bytes2); var newExtension = new proto.jspb.test.CloneExtension(); newExtension.setExt('e2'); clone.setExtension(proto.jspb.test.CloneExtension.extField, newExtension); assertArrayEquals(['v2',, ['a1', ['b1', 'c1']],, - [['a2', ['b2', 'c2']], ['a3', ['b3', 'c3']]],,, { 100: [, 'e2'] }], + [['a2', ['b2', 'c2']], ['a3', ['b3', 'c3']]], bytes2,, { 100: [, 'e2'] }], clone.toArray()); assertArrayEquals(['v1',, ['x1', ['y1', 'z1']],, - [['x2', ['y2', 'z2']], ['x3', ['y3', 'z3']]],,, { 100: [, 'e1'] }], + [['x2', ['y2', 'z2']], ['x3', ['y3', 'z3']]], bytes1,, { 100: [, 'e1'] }], original.toArray()); }); it('testCopyInto', function() { + var supportsUint8Array = + !goog.userAgent.IE || goog.userAgent.isVersionOrHigher('10'); var original = new proto.jspb.test.TestClone(); original.setStr('v1'); var dest = new proto.jspb.test.TestClone(); @@ -484,6 +498,10 @@ describe('Message test suite', function() { original.setSimple2List([simple2, simple3]); dest.setSimple1(destSimple1); dest.setSimple2List([destSimple2, destSimple3]); + var bytes1 = supportsUint8Array ? new Uint8Array([1, 2, 3]) : '123'; + var bytes2 = supportsUint8Array ? new Uint8Array([4, 5, 6]) : '456'; + original.setBytesField(bytes1); + dest.setBytesField(bytes2); var extension = new proto.jspb.test.CloneExtension(); extension.setExt('e1'); original.setExtension(proto.jspb.test.CloneExtension.extField, extension); @@ -496,6 +514,15 @@ describe('Message test suite', function() { dest.getSimple1().setAString('new value'); assertNotEquals(dest.getSimple1().getAString(), original.getSimple1().getAString()); + if (supportsUint8Array) { + dest.getBytesField()[0] = 7; + assertObjectEquals(bytes1, original.getBytesField()); + assertObjectEquals(new Uint8Array([7, 2, 3]), dest.getBytesField()); + } else { + dest.setBytesField('789'); + assertObjectEquals(bytes1, original.getBytesField()); + assertObjectEquals('789', dest.getBytesField()); + } dest.getExtension(proto.jspb.test.CloneExtension.extField). setExt('new value'); assertNotEquals( diff --git a/js/package.json b/js/package.json index ae05722a..657b08bd 100644 --- a/js/package.json +++ b/js/package.json @@ -1,17 +1,19 @@ { "name": "google-protobuf", - "version": "3.0.0-alpha.6", + "version": "3.0.0-alpha.6.2", "description": "Protocol Buffers for JavaScript", "main": "google-protobuf.js", - "dependencies": { + "files": [ + "google" + ], + "dependencies": {}, + "devDependencies": { + "glob": "~6.0.4", + "google-closure-compiler": "~20160619.0.0", "google-closure-library": "~20160125.0.0", "gulp": "~3.9.0", "jasmine": "~2.4.1" }, - "devDependencies": { - "google-closure-compiler": "~20151216.2.0", - "glob": "~6.0.4" - }, "scripts": { "test": "node ./node_modules/gulp/bin/gulp.js test" }, @@ -19,6 +21,6 @@ "type": "git", "url": "https://github.com/google/protobuf/tree/master/js" }, - "author": "", + "author": "Google Protocol Buffers Team", "license": "Apache-2.0" } diff --git a/js/test.proto b/js/test.proto index 6b9dc891..cf2eafef 100644 --- a/js/test.proto +++ b/js/test.proto @@ -160,6 +160,7 @@ message TestClone { optional string str = 1; optional Simple1 simple1 = 3; repeated Simple1 simple2 = 5; + optional bytes bytes_field = 6; optional string unused = 7; extensions 10 to max; } @@ -228,3 +229,7 @@ message TestMessageWithOneof { } } +message TestEndsWithBytes { + optional int32 value = 1; + optional bytes data = 2; +} diff --git a/js/testbinary.proto b/js/testbinary.proto index 60c70190..116f17fb 100644 --- a/js/testbinary.proto +++ b/js/testbinary.proto @@ -183,3 +183,30 @@ extend TestExtendable { [packed=true]; } + +message TestMapFields { + map<string, string> map_string_string = 1; + map<string, int32> map_string_int32 = 2; + map<string, int64> map_string_int64 = 3; + map<string, bool> map_string_bool = 4; + map<string, double> map_string_double = 5; + map<string, MapValueEnum> map_string_enum = 6; + map<string, MapValueMessage> map_string_msg = 7; + + map<int32, string> map_int32_string = 8; + map<int64, string> map_int64_string = 9; + map<bool, string> map_bool_string = 10; + + optional TestMapFields test_map_fields = 11; + map<string, TestMapFields> map_string_testmapfields = 12; +} + +enum MapValueEnum { + MAP_VALUE_FOO = 0; + MAP_VALUE_BAR = 1; + MAP_VALUE_BAZ = 2; +} + +message MapValueMessage { + optional int32 foo = 1; +} |