lib/map.js

'use strict';

const {Buffer} = require('buffer');
const encoder = require('./encoder');
const decoder = require('./decoder');
const {MT} = require('./constants');

/**
 * Wrapper around a JavaScript Map object that allows the keys to be
 * any complex type.  The base Map object allows this, but will only
 * compare the keys by identity, not by value.  CborMap translates keys
 * to CBOR first (and base64's them to ensure by-value comparison).
 *
 * This is not a subclass of Object, because it would be tough to get
 * the semantics to be an exact match.
 *
 * @extends Map
 */
class CborMap extends Map {
  /**
   * Creates an instance of CborMap.
   *
   * @param {Iterable<any>} [iterable] An Array or other iterable
   *   object whose elements are key-value pairs (arrays with two elements, e.g.
   *   <code>[[ 1, 'one' ],[ 2, 'two' ]]</code>). Each key-value pair is added
   *   to the new CborMap; null values are treated as undefined.
   */
  constructor(iterable) {
    super(iterable);
  }

  /**
   * @ignore
   * @param {unknown} key
   * @returns {string}
   */
  static _encode(key) {
    return encoder.encodeCanonical(key).toString('base64');
  }

  /**
   * @ignore
   * @param {string} key
   * @returns {unknown}
   */
  static _decode(key) {
    return decoder.decodeFirstSync(key, 'base64');
  }

  /**
   * Retrieve a specified element.
   *
   * @param {any} key The key identifying the element to retrieve.
   *   Can be any type, which will be serialized into CBOR and compared by
   *   value.
   * @returns {any} The element if it exists, or <code>undefined</code>.
   */
  get(key) {
    return super.get(CborMap._encode(key));
  }

  /**
   * Adds or updates an element with a specified key and value.
   *
   * @param {any} key The key identifying the element to store.
   *   Can be any type, which will be serialized into CBOR and compared by
   *   value.
   * @param {any} val The element to store.
   * @returns {this} This object.
   */
  set(key, val) {
    return super.set(CborMap._encode(key), val);
  }

  /**
   * Removes the specified element.
   *
   * @param {any} key The key identifying the element to delete. Can be any
   *   type, which will be serialized into CBOR and compared by value.
   * @returns {boolean} True if an element in the Map object existed and has
   *   been removed, or false if the element does not exist.
   */
  delete(key) {
    return super.delete(CborMap._encode(key));
  }

  /**
   * Does an element with the specified key exist?
   *
   * @param {any} key The key identifying the element to check.
   *   Can be any type, which will be serialized into CBOR and compared by
   *   value.
   * @returns {boolean} True if an element with the specified key exists in
   *   the Map object; otherwise false.
   */
  has(key) {
    return super.has(CborMap._encode(key));
  }

  /**
   * Returns a new Iterator object that contains the keys for each element
   * in the Map object in insertion order.  The keys are decoded into their
   * original format.
   *
   * @returns {MapIterator<any>}
   */
  *keys() {
    for (const k of super.keys()) {
      yield CborMap._decode(k);
    }
  }

  /**
   * Returns a new Iterator object that contains the [key, value] pairs for
   * each element in the Map object in insertion order.
   *
   * @returns {IterableIterator<any>} Key value pairs.
   * @yields {any[]} Key value pairs.
   */
  *entries() {
    for (const kv of super.entries()) {
      yield [CborMap._decode(kv[0]), kv[1]];
    }
  }

  /**
   * Returns a new Iterator object that contains the [key, value] pairs for
   * each element in the Map object in insertion order.
   *
   * @returns {IterableIterator<any>} Key value pairs.
   */
  [Symbol.iterator]() {
    return this.entries();
  }

  /**
   * Executes a provided function once per each key/value pair in the Map
   * object, in insertion order.
   *
   * @param {function(any, any, Map<any,any>): undefined} fun Function to
   *   execute for each element, which takes a value, a key, and the Map
   *   being traversed.
   * @param {any} thisArg Value to use as this when executing callback.
   * @throws {TypeError} Invalid function.
   */
  forEach(fun, thisArg = this) {
    if (typeof fun !== 'function') {
      throw new TypeError('Must be function');
    }
    for (const kv of super.entries()) {
      fun.call(thisArg, kv[1], CborMap._decode(kv[0]), this);
    }
  }

  /**
   * Push the simple value onto the CBOR stream.
   *
   * @param {import('./encoder.js')} gen The generator to push onto.
   * @returns {boolean} True on success.
   */
  encodeCBOR(gen) {
    if (!gen._pushInt(this.size, MT.MAP)) {
      return false;
    }
    if (gen.canonical) {
      const entries = Array.from(super.entries())
        .map(kv => [Buffer.from(kv[0], 'base64'), kv[1]]);
      entries.sort((a, b) => a[0].compare(b[0]));
      for (const kv of entries) {
        if (!(gen.push(kv[0]) && gen.pushAny(kv[1]))) {
          return false;
        }
      }
    } else {
      for (const kv of super.entries()) {
        if (!(gen.push(Buffer.from(kv[0], 'base64')) && gen.pushAny(kv[1]))) {
          return false;
        }
      }
    }
    return true;
  }
}

module.exports = CborMap;