• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Protocol Buffers - Google's data interchange format
2// Copyright 2008 Google Inc.  All rights reserved.
3// https://developers.google.com/protocol-buffers/
4//
5// Redistribution and use in source and binary forms, with or without
6// modification, are permitted provided that the following conditions are
7// met:
8//
9//     * Redistributions of source code must retain the above copyright
10// notice, this list of conditions and the following disclaimer.
11//     * Redistributions in binary form must reproduce the above
12// copyright notice, this list of conditions and the following disclaimer
13// in the documentation and/or other materials provided with the
14// distribution.
15//     * Neither the name of Google Inc. nor the names of its
16// contributors may be used to endorse or promote products derived from
17// this software without specific prior written permission.
18//
19// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31goog.provide('jspb.Map');
32
33goog.require('goog.asserts');
34
35goog.requireType('jspb.BinaryReader');
36goog.requireType('jspb.BinaryWriter');
37
38
39
40/**
41 * Constructs a new Map. A Map is a container that is used to implement map
42 * fields on message objects. It closely follows the ES6 Map API; however,
43 * it is distinct because we do not want to depend on external polyfills or
44 * on ES6 itself.
45 *
46 * This constructor should only be called from generated message code. It is not
47 * intended for general use by library consumers.
48 *
49 * @template K, V
50 *
51 * @param {!Array<!Array<?>>} arr
52 *
53 * @param {?function(new:V, ?=)=} opt_valueCtor
54 *    The constructor for type V, if type V is a message type.
55 *
56 * @constructor
57 * @struct
58 */
59jspb.Map = function(arr, opt_valueCtor) {
60  /** @const @private */
61  this.arr_ = arr;
62  /** @const @private */
63  this.valueCtor_ = opt_valueCtor;
64
65  /** @type {!Object<string, !jspb.Map.Entry_<K,V>>} @private */
66  this.map_ = {};
67
68  /**
69   * Is `this.arr_ updated with respect to `this.map_`?
70   * @type {boolean}
71   */
72  this.arrClean = true;
73
74  if (this.arr_.length > 0) {
75    this.loadFromArray_();
76  }
77};
78
79
80/**
81 * Load initial content from underlying array.
82 * @private
83 */
84jspb.Map.prototype.loadFromArray_ = function() {
85  for (var i = 0; i < this.arr_.length; i++) {
86    var record = this.arr_[i];
87    var key = record[0];
88    var value = record[1];
89    this.map_[key.toString()] = new jspb.Map.Entry_(key, value);
90  }
91  this.arrClean = true;
92};
93
94
95/**
96 * Synchronize content to underlying array, if needed, and return it.
97 * @return {!Array<!Array<!Object>>}
98 */
99jspb.Map.prototype.toArray = function() {
100  if (this.arrClean) {
101    if (this.valueCtor_) {
102      // We need to recursively sync maps in submessages to their arrays.
103      var m = this.map_;
104      for (var p in m) {
105        if (Object.prototype.hasOwnProperty.call(m, p)) {
106          var valueWrapper = /** @type {?jspb.Message} */ (m[p].valueWrapper);
107          if (valueWrapper) {
108            valueWrapper.toArray();
109          }
110        }
111      }
112    }
113  } else {
114    // Delete all elements.
115    this.arr_.length = 0;
116    var strKeys = this.stringKeys_();
117    // Output keys in deterministic (sorted) order.
118    strKeys.sort();
119    for (var i = 0; i < strKeys.length; i++) {
120      var entry = this.map_[strKeys[i]];
121      var valueWrapper = /** @type {?jspb.Message} */ (entry.valueWrapper);
122      if (valueWrapper) {
123        valueWrapper.toArray();
124      }
125      this.arr_.push([entry.key, entry.value]);
126    }
127    this.arrClean = true;
128  }
129  return this.arr_;
130};
131
132
133/**
134 * Returns the map formatted as an array of key-value pairs, suitable for the
135 * toObject() form of a message.
136 *
137 * @param {boolean=} includeInstance Whether to include the JSPB instance for
138 *    transitional soy proto support: http://goto/soy-param-migration
139 * @param {function((boolean|undefined),V):!Object=} valueToObject
140 *    The static toObject() method, if V is a message type.
141 * @return {!Array<!Array<!Object>>}
142 */
143jspb.Map.prototype.toObject = function(includeInstance, valueToObject) {
144  var rawArray = this.toArray();
145  var entries = [];
146  for (var i = 0; i < rawArray.length; i++) {
147    var entry = this.map_[rawArray[i][0].toString()];
148    this.wrapEntry_(entry);
149    var valueWrapper = /** @type {V|undefined} */ (entry.valueWrapper);
150    if (valueWrapper) {
151      goog.asserts.assert(valueToObject);
152      entries.push([entry.key, valueToObject(includeInstance, valueWrapper)]);
153    } else {
154      entries.push([entry.key, entry.value]);
155    }
156  }
157  return entries;
158};
159
160
161/**
162 * Returns a Map from the given array of key-value pairs when the values are of
163 * message type. The values in the array must match the format returned by their
164 * message type's toObject() method.
165 *
166 * @template K, V
167 * @param {!Array<!Array<!Object>>} entries
168 * @param {function(new:V,?=)} valueCtor
169 *    The constructor for type V.
170 * @param {function(!Object):V} valueFromObject
171 *    The fromObject function for type V.
172 * @return {!jspb.Map<K, V>}
173 */
174jspb.Map.fromObject = function(entries, valueCtor, valueFromObject) {
175  var result = new jspb.Map([], valueCtor);
176  for (var i = 0; i < entries.length; i++) {
177    var key = entries[i][0];
178    var value = valueFromObject(entries[i][1]);
179    result.set(key, value);
180  }
181  return result;
182};
183
184
185/**
186 * Helper: an IteratorIterable over an array.
187 * @template T
188 * @param {!Array<T>} arr the array
189 * @implements {IteratorIterable<T>}
190 * @constructor @struct
191 * @private
192 */
193jspb.Map.ArrayIteratorIterable_ = function(arr) {
194  /** @type {number} @private */
195  this.idx_ = 0;
196
197  /** @const @private */
198  this.arr_ = arr;
199};
200
201
202/** @override @final */
203jspb.Map.ArrayIteratorIterable_.prototype.next = function() {
204  if (this.idx_ < this.arr_.length) {
205    return {done: false, value: this.arr_[this.idx_++]};
206  } else {
207    return {done: true, value: undefined};
208  }
209};
210
211if (typeof(Symbol) != 'undefined') {
212  /** @override */
213  jspb.Map.ArrayIteratorIterable_.prototype[Symbol.iterator] = function() {
214    return this;
215  };
216}
217
218
219/**
220 * Returns the map's length (number of key/value pairs).
221 * @return {number}
222 */
223jspb.Map.prototype.getLength = function() {
224  return this.stringKeys_().length;
225};
226
227
228/**
229 * Clears the map.
230 */
231jspb.Map.prototype.clear = function() {
232  this.map_ = {};
233  this.arrClean = false;
234};
235
236
237/**
238 * Deletes a particular key from the map.
239 * N.B.: differs in name from ES6 Map's `delete` because IE8 does not support
240 * reserved words as property names.
241 * @this {jspb.Map}
242 * @param {K} key
243 * @return {boolean} Whether any entry with this key was deleted.
244 */
245jspb.Map.prototype.del = function(key) {
246  var keyValue = key.toString();
247  var hadKey = this.map_.hasOwnProperty(keyValue);
248  delete this.map_[keyValue];
249  this.arrClean = false;
250  return hadKey;
251};
252
253
254/**
255 * Returns an array of [key, value] pairs in the map.
256 *
257 * This is redundant compared to the plain entries() method, but we provide this
258 * to help out Angular 1.x users.  Still evaluating whether this is the best
259 * option.
260 *
261 * @return {!Array<!Array<K|V>>}
262 */
263jspb.Map.prototype.getEntryList = function() {
264  var entries = [];
265  var strKeys = this.stringKeys_();
266  strKeys.sort();
267  for (var i = 0; i < strKeys.length; i++) {
268    var entry = this.map_[strKeys[i]];
269    entries.push([entry.key, entry.value]);
270  }
271  return entries;
272};
273
274
275/**
276 * Returns an iterator-iterable over [key, value] pairs in the map.
277 * Closure compiler sadly doesn't support tuples, ie. Iterator<[K,V]>.
278 * @return {!IteratorIterable<!Array<K|V>>} The iterator-iterable.
279 */
280jspb.Map.prototype.entries = function() {
281  var entries = [];
282  var strKeys = this.stringKeys_();
283  strKeys.sort();
284  for (var i = 0; i < strKeys.length; i++) {
285    var entry = this.map_[strKeys[i]];
286    entries.push([entry.key, this.wrapEntry_(entry)]);
287  }
288  return new jspb.Map.ArrayIteratorIterable_(entries);
289};
290
291
292/**
293 * Returns an iterator-iterable over keys in the map.
294 * @return {!IteratorIterable<K>} The iterator-iterable.
295 */
296jspb.Map.prototype.keys = function() {
297  var keys = [];
298  var strKeys = this.stringKeys_();
299  strKeys.sort();
300  for (var i = 0; i < strKeys.length; i++) {
301    var entry = this.map_[strKeys[i]];
302    keys.push(entry.key);
303  }
304  return new jspb.Map.ArrayIteratorIterable_(keys);
305};
306
307
308/**
309 * Returns an iterator-iterable over values in the map.
310 * @return {!IteratorIterable<V>} The iterator-iterable.
311 */
312jspb.Map.prototype.values = function() {
313  var values = [];
314  var strKeys = this.stringKeys_();
315  strKeys.sort();
316  for (var i = 0; i < strKeys.length; i++) {
317    var entry = this.map_[strKeys[i]];
318    values.push(this.wrapEntry_(entry));
319  }
320  return new jspb.Map.ArrayIteratorIterable_(values);
321};
322
323
324/**
325 * Iterates over entries in the map, calling a function on each.
326 * @template T
327 * @param {function(this:T, V, K, ?jspb.Map<K, V>)} cb
328 * @param {T=} opt_thisArg
329 */
330jspb.Map.prototype.forEach = function(cb, opt_thisArg) {
331  var strKeys = this.stringKeys_();
332  strKeys.sort();
333  for (var i = 0; i < strKeys.length; i++) {
334    var entry = this.map_[strKeys[i]];
335    cb.call(opt_thisArg, this.wrapEntry_(entry), entry.key, this);
336  }
337};
338
339
340/**
341 * Sets a key in the map to the given value.
342 * @param {K} key The key
343 * @param {V} value The value
344 * @return {!jspb.Map<K,V>}
345 */
346jspb.Map.prototype.set = function(key, value) {
347  var entry = new jspb.Map.Entry_(key);
348  if (this.valueCtor_) {
349    entry.valueWrapper = value;
350    // .toArray() on a message returns a reference to the underlying array
351    // rather than a copy.
352    entry.value = value.toArray();
353  } else {
354    entry.value = value;
355  }
356  this.map_[key.toString()] = entry;
357  this.arrClean = false;
358  return this;
359};
360
361
362/**
363 * Helper: lazily construct a wrapper around an entry, if needed, and return the
364 * user-visible type.
365 * @param {!jspb.Map.Entry_<K,V>} entry
366 * @return {V}
367 * @private
368 */
369jspb.Map.prototype.wrapEntry_ = function(entry) {
370  if (this.valueCtor_) {
371    if (!entry.valueWrapper) {
372      entry.valueWrapper = new this.valueCtor_(entry.value);
373    }
374    return /** @type {V} */ (entry.valueWrapper);
375  } else {
376    return entry.value;
377  }
378};
379
380
381/**
382 * Gets the value corresponding to a key in the map.
383 * @param {K} key
384 * @return {V|undefined} The value, or `undefined` if key not present
385 */
386jspb.Map.prototype.get = function(key) {
387  var keyValue = key.toString();
388  var entry = this.map_[keyValue];
389  if (entry) {
390    return this.wrapEntry_(entry);
391  } else {
392    return undefined;
393  }
394};
395
396
397/**
398 * Determines whether the given key is present in the map.
399 * @param {K} key
400 * @return {boolean} `true` if the key is present
401 */
402jspb.Map.prototype.has = function(key) {
403  var keyValue = key.toString();
404  return (keyValue in this.map_);
405};
406
407
408/**
409 * Write this Map field in wire format to a BinaryWriter, using the given field
410 * number.
411 * @param {number} fieldNumber
412 * @param {!jspb.BinaryWriter} writer
413 * @param {function(this:jspb.BinaryWriter,number,K)} keyWriterFn
414 *     The method on BinaryWriter that writes type K to the stream.
415 * @param {function(this:jspb.BinaryWriter,number,V,?=)|
416 *          function(this:jspb.BinaryWriter,number,V,?)} valueWriterFn
417 *     The method on BinaryWriter that writes type V to the stream.  May be
418 *     writeMessage, in which case the second callback arg form is used.
419 * @param {function(V,!jspb.BinaryWriter)=} opt_valueWriterCallback
420 *    The BinaryWriter serialization callback for type V, if V is a message
421 *    type.
422 */
423jspb.Map.prototype.serializeBinary = function(
424    fieldNumber, writer, keyWriterFn, valueWriterFn, opt_valueWriterCallback) {
425  var strKeys = this.stringKeys_();
426  strKeys.sort();
427  for (var i = 0; i < strKeys.length; i++) {
428    var entry = this.map_[strKeys[i]];
429    writer.beginSubMessage(fieldNumber);
430    keyWriterFn.call(writer, 1, entry.key);
431    if (this.valueCtor_) {
432      valueWriterFn.call(writer, 2, this.wrapEntry_(entry),
433                         opt_valueWriterCallback);
434    } else {
435      /** @type {function(this:jspb.BinaryWriter,number,?)} */ (valueWriterFn)
436          .call(writer, 2, entry.value);
437    }
438    writer.endSubMessage();
439  }
440};
441
442
443/**
444 * Read one key/value message from the given BinaryReader. Compatible as the
445 * `reader` callback parameter to jspb.BinaryReader.readMessage, to be called
446 * when a key/value pair submessage is encountered. If the Key is undefined,
447 * we should default it to 0.
448 * @template K, V
449 * @param {!jspb.Map} map
450 * @param {!jspb.BinaryReader} reader
451 * @param {function(this:jspb.BinaryReader):K} keyReaderFn
452 *     The method on BinaryReader that reads type K from the stream.
453 *
454 * @param {function(this:jspb.BinaryReader):V|
455 *          function(this:jspb.BinaryReader,V,
456 *                  function(V,!jspb.BinaryReader))} valueReaderFn
457 *    The method on BinaryReader that reads type V from the stream. May be
458 *    readMessage, in which case the second callback arg form is used.
459 *
460 * @param {?function(V,!jspb.BinaryReader)=} opt_valueReaderCallback
461 *    The BinaryReader parsing callback for type V, if V is a message type
462 *
463 * @param {K=} opt_defaultKey
464 *    The default value for the type of map keys. Accepting map
465 *    entries with unset keys is required for maps to be backwards compatible
466 *    with the repeated message representation described here: goo.gl/zuoLAC
467 *
468 */
469jspb.Map.deserializeBinary = function(map, reader, keyReaderFn, valueReaderFn,
470                                      opt_valueReaderCallback, opt_defaultKey) {
471  var key = opt_defaultKey;
472  var value = undefined;
473
474  while (reader.nextField()) {
475    if (reader.isEndGroup()) {
476      break;
477    }
478    var field = reader.getFieldNumber();
479
480    if (field == 1) {
481      // Key.
482      key = keyReaderFn.call(reader);
483    } else if (field == 2) {
484      // Value.
485      if (map.valueCtor_) {
486        goog.asserts.assert(opt_valueReaderCallback);
487        value = new map.valueCtor_();
488        valueReaderFn.call(reader, value, opt_valueReaderCallback);
489      } else {
490        value =
491            (/** @type {function(this:jspb.BinaryReader):?} */ (valueReaderFn))
492                .call(reader);
493      }
494    }
495  }
496
497  goog.asserts.assert(key != undefined);
498  goog.asserts.assert(value != undefined);
499  map.set(key, value);
500};
501
502
503/**
504 * Helper: compute the list of all stringified keys in the underlying Object
505 * map.
506 * @return {!Array<string>}
507 * @private
508 */
509jspb.Map.prototype.stringKeys_ = function() {
510  var m = this.map_;
511  var ret = [];
512  for (var p in m) {
513    if (Object.prototype.hasOwnProperty.call(m, p)) {
514      ret.push(p);
515    }
516  }
517  return ret;
518};
519
520
521
522/**
523 * @param {K} key The entry's key.
524 * @param {V=} opt_value The entry's value wrapper.
525 * @constructor
526 * @struct
527 * @template K, V
528 * @private
529 */
530jspb.Map.Entry_ = function(key, opt_value) {
531  /** @const {K} */
532  this.key = key;
533
534  // The JSPB-serializable value.  For primitive types this will be of type V.
535  // For message types it will be an array.
536  /** @type {V} */
537  this.value = opt_value;
538
539  // Only used for submessage values.
540  /** @type {V} */
541  this.valueWrapper = undefined;
542};
543