michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Dict"]; michael@0: michael@0: /** michael@0: * Transforms a given key into a property name guaranteed not to collide with michael@0: * any built-ins. michael@0: */ michael@0: function convert(aKey) { michael@0: return ":" + aKey; michael@0: } michael@0: michael@0: /** michael@0: * Transforms a property into a key suitable for providing to the outside world. michael@0: */ michael@0: function unconvert(aProp) { michael@0: return aProp.substr(1); michael@0: } michael@0: michael@0: /** michael@0: * A dictionary of strings to arbitrary JS objects. This should be used whenever michael@0: * the keys are potentially arbitrary, to avoid collisions with built-in michael@0: * properties. michael@0: * michael@0: * @param aInitial An object containing the initial keys and values of this michael@0: * dictionary. Only the "own" enumerable properties of the michael@0: * object are considered. michael@0: * If |aInitial| is a string, it is assumed to be JSON and parsed into an object. michael@0: */ michael@0: this.Dict = function Dict(aInitial) { michael@0: if (aInitial === undefined) michael@0: aInitial = {}; michael@0: if (typeof aInitial == "string") michael@0: aInitial = JSON.parse(aInitial); michael@0: var items = {}, count = 0; michael@0: // That we don't look up the prototype chain is guaranteed by Iterator. michael@0: for (var [key, val] in Iterator(aInitial)) { michael@0: items[convert(key)] = val; michael@0: count++; michael@0: } michael@0: this._state = {count: count, items: items}; michael@0: return Object.freeze(this); michael@0: } michael@0: michael@0: Dict.prototype = Object.freeze({ michael@0: /** michael@0: * The number of items in the dictionary. michael@0: */ michael@0: get count() { michael@0: return this._state.count; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the value for a key from the dictionary. If the key is not a string, michael@0: * it will be converted to a string before the lookup happens. michael@0: * michael@0: * @param aKey The key to look up michael@0: * @param [aDefault] An optional default value to return if the key is not michael@0: * present. Defaults to |undefined|. michael@0: * @returns The item, or aDefault if it isn't found. michael@0: */ michael@0: get: function Dict_get(aKey, aDefault) { michael@0: var prop = convert(aKey); michael@0: var items = this._state.items; michael@0: return items.hasOwnProperty(prop) ? items[prop] : aDefault; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the value for a key in the dictionary. If the key is a not a string, michael@0: * it will be converted to a string before the set happens. michael@0: */ michael@0: set: function Dict_set(aKey, aValue) { michael@0: var prop = convert(aKey); michael@0: var items = this._state.items; michael@0: if (!items.hasOwnProperty(prop)) michael@0: this._state.count++; michael@0: items[prop] = aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Sets a lazy getter function for a key's value. If the key is a not a string, michael@0: * it will be converted to a string before the set happens. michael@0: * @param aKey michael@0: * The key to set michael@0: * @param aThunk michael@0: * A getter function to be called the first time the value for aKey is michael@0: * retrieved. It is guaranteed that aThunk wouldn't be called more michael@0: * than once. Note that the key value may be retrieved either michael@0: * directly, by |get|, or indirectly, by |listvalues| or by iterating michael@0: * |values|. For the later, the value is only retrieved if and when michael@0: * the iterator gets to the value in question. Also note that calling michael@0: * |has| for a lazy-key does not invoke aThunk. michael@0: * michael@0: * @note No context is provided for aThunk when it's invoked. michael@0: * Use Function.bind if you wish to run it in a certain context. michael@0: */ michael@0: setAsLazyGetter: function Dict_setAsLazyGetter(aKey, aThunk) { michael@0: let prop = convert(aKey); michael@0: let items = this._state.items; michael@0: if (!items.hasOwnProperty(prop)) michael@0: this._state.count++; michael@0: michael@0: Object.defineProperty(items, prop, { michael@0: get: function() { michael@0: delete items[prop]; michael@0: return items[prop] = aThunk(); michael@0: }, michael@0: configurable: true, michael@0: enumerable: true michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Returns whether a key is set as a lazy getter. This returns michael@0: * true only if the getter function was not called already. michael@0: * @param aKey michael@0: * The key to look up. michael@0: * @returns whether aKey is set as a lazy getter. michael@0: */ michael@0: isLazyGetter: function Dict_isLazyGetter(aKey) { michael@0: let descriptor = Object.getOwnPropertyDescriptor(this._state.items, michael@0: convert(aKey)); michael@0: return (descriptor && descriptor.get != null); michael@0: }, michael@0: michael@0: /** michael@0: * Returns whether a key is in the dictionary. If the key is a not a string, michael@0: * it will be converted to a string before the lookup happens. michael@0: */ michael@0: has: function Dict_has(aKey) { michael@0: return (this._state.items.hasOwnProperty(convert(aKey))); michael@0: }, michael@0: michael@0: /** michael@0: * Deletes a key from the dictionary. If the key is a not a string, it will be michael@0: * converted to a string before the delete happens. michael@0: * michael@0: * @returns true if the key was found, false if it wasn't. michael@0: */ michael@0: del: function Dict_del(aKey) { michael@0: var prop = convert(aKey); michael@0: if (this._state.items.hasOwnProperty(prop)) { michael@0: delete this._state.items[prop]; michael@0: this._state.count--; michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a shallow copy of this dictionary. michael@0: */ michael@0: copy: function Dict_copy() { michael@0: var newItems = {}; michael@0: for (var [key, val] in this.items) michael@0: newItems[key] = val; michael@0: return new Dict(newItems); michael@0: }, michael@0: michael@0: /* michael@0: * List and iterator functions michael@0: * michael@0: * No guarantees whatsoever are made about the order of elements. michael@0: */ michael@0: michael@0: /** michael@0: * Returns a list of all the keys in the dictionary in an arbitrary order. michael@0: */ michael@0: listkeys: function Dict_listkeys() { michael@0: return [unconvert(k) for (k in this._state.items)]; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a list of all the values in the dictionary in an arbitrary order. michael@0: */ michael@0: listvalues: function Dict_listvalues() { michael@0: var items = this._state.items; michael@0: return [items[k] for (k in items)]; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a list of all the items in the dictionary as key-value pairs michael@0: * in an arbitrary order. michael@0: */ michael@0: listitems: function Dict_listitems() { michael@0: var items = this._state.items; michael@0: return [[unconvert(k), items[k]] for (k in items)]; michael@0: }, michael@0: michael@0: /** michael@0: * Returns an iterator over all the keys in the dictionary in an arbitrary michael@0: * order. No guarantees are made about what happens if the dictionary is michael@0: * mutated during iteration. michael@0: */ michael@0: get keys() { michael@0: // If we don't capture this._state.items here then the this-binding will be michael@0: // incorrect when the generator is executed michael@0: var items = this._state.items; michael@0: return (unconvert(k) for (k in items)); michael@0: }, michael@0: michael@0: /** michael@0: * Returns an iterator over all the values in the dictionary in an arbitrary michael@0: * order. No guarantees are made about what happens if the dictionary is michael@0: * mutated during iteration. michael@0: */ michael@0: get values() { michael@0: // If we don't capture this._state.items here then the this-binding will be michael@0: // incorrect when the generator is executed michael@0: var items = this._state.items; michael@0: return (items[k] for (k in items)); michael@0: }, michael@0: michael@0: /** michael@0: * Returns an iterator over all the items in the dictionary as key-value pairs michael@0: * in an arbitrary order. No guarantees are made about what happens if the michael@0: * dictionary is mutated during iteration. michael@0: */ michael@0: get items() { michael@0: // If we don't capture this._state.items here then the this-binding will be michael@0: // incorrect when the generator is executed michael@0: var items = this._state.items; michael@0: return ([unconvert(k), items[k]] for (k in items)); michael@0: }, michael@0: michael@0: /** michael@0: * Returns a String representation of this dictionary. michael@0: */ michael@0: toString: function Dict_toString() { michael@0: return "{" + michael@0: [(key + ": " + val) for ([key, val] in this.items)].join(", ") + michael@0: "}"; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a JSON representation of this dictionary. michael@0: */ michael@0: toJSON: function Dict_toJSON() { michael@0: let obj = {}; michael@0: for (let [key, item] of Iterator(this._state.items)) { michael@0: obj[unconvert(key)] = item; michael@0: } michael@0: return JSON.stringify(obj); michael@0: }, michael@0: });