summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/Dict.jsm
blob: 2d276113adfa9167b52f6a2d9f02916ed167b2cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

this.EXPORTED_SYMBOLS = ["Dict"];

/**
 * Transforms a given key into a property name guaranteed not to collide with
 * any built-ins.
 */
function convert(aKey) {
  return ":" + aKey;
}

/**
 * Transforms a property into a key suitable for providing to the outside world.
 */
function unconvert(aProp) {
  return aProp.substr(1);
}

/**
 * A dictionary of strings to arbitrary JS objects. This should be used whenever
 * the keys are potentially arbitrary, to avoid collisions with built-in
 * properties.
 *
 * @param aInitial An object containing the initial keys and values of this
 *                 dictionary. Only the "own" enumerable properties of the
 *                 object are considered.
 *                 If |aInitial| is a string, it is assumed to be JSON and parsed into an object.
 */
this.Dict = function Dict(aInitial) {
  if (aInitial === undefined)
    aInitial = {};
  if (typeof aInitial == "string")
    aInitial = JSON.parse(aInitial);
  var items = {}, count = 0;
  // That we don't look up the prototype chain is guaranteed by Iterator.
  for (var [key, val] in Iterator(aInitial)) {
    items[convert(key)] = val;
    count++;
  }
  this._state = {count: count, items: items};
  return Object.freeze(this);
}

Dict.prototype = Object.freeze({
  /**
   * The number of items in the dictionary.
   */
  get count() {
    return this._state.count;
  },

  /**
   * Gets the value for a key from the dictionary. If the key is not a string,
   * it will be converted to a string before the lookup happens.
   *
   * @param aKey The key to look up
   * @param [aDefault] An optional default value to return if the key is not
   *                   present. Defaults to |undefined|.
   * @returns The item, or aDefault if it isn't found.
   */
  get: function Dict_get(aKey, aDefault) {
    var prop = convert(aKey);
    var items = this._state.items;
    return items.hasOwnProperty(prop) ? items[prop] : aDefault;
  },

  /**
   * Sets the value for a key in the dictionary. If the key is a not a string,
   * it will be converted to a string before the set happens.
   */
  set: function Dict_set(aKey, aValue) {
    var prop = convert(aKey);
    var items = this._state.items;
    if (!items.hasOwnProperty(prop))
      this._state.count++;
    items[prop] = aValue;
  },

  /**
   * Sets a lazy getter function for a key's value. If the key is a not a string,
   * it will be converted to a string before the set happens.
   * @param aKey
   *        The key to set
   * @param aThunk
   *        A getter function to be called the first time the value for aKey is
   *        retrieved. It is guaranteed that aThunk wouldn't be called more
   *        than once.  Note that the key value may be retrieved either
   *        directly, by |get|, or indirectly, by |listvalues| or by iterating
   *        |values|.  For the later, the value is only retrieved if and when
   *        the iterator gets to the value in question.  Also note that calling
   *        |has| for a lazy-key does not invoke aThunk.
   *
   * @note No context is provided for aThunk when it's invoked.
   *       Use Function.bind if you wish to run it in a certain context.
   */
  setAsLazyGetter: function Dict_setAsLazyGetter(aKey, aThunk) {
    let prop = convert(aKey);
    let items = this._state.items;
    if (!items.hasOwnProperty(prop))
      this._state.count++;

    Object.defineProperty(items, prop, {
      get: function() {
        delete items[prop];
        return items[prop] = aThunk();
      },
      configurable: true,
      enumerable: true     
    });
  },

  /**
   * Returns whether a key is set as a lazy getter.  This returns
   * true only if the getter function was not called already.
   * @param aKey
   *        The key to look up.
   * @returns whether aKey is set as a lazy getter.
   */
  isLazyGetter: function Dict_isLazyGetter(aKey) {
    let descriptor = Object.getOwnPropertyDescriptor(this._state.items,
                                                     convert(aKey));
    return (descriptor && descriptor.get != null);
  },

  /**
   * Returns whether a key is in the dictionary. If the key is a not a string,
   * it will be converted to a string before the lookup happens.
   */
  has: function Dict_has(aKey) {
    return (this._state.items.hasOwnProperty(convert(aKey)));
  },

  /**
   * Deletes a key from the dictionary. If the key is a not a string, it will be
   * converted to a string before the delete happens.
   *
   * @returns true if the key was found, false if it wasn't.
   */
  del: function Dict_del(aKey) {
    var prop = convert(aKey);
    if (this._state.items.hasOwnProperty(prop)) {
      delete this._state.items[prop];
      this._state.count--;
      return true;
    }
    return false;
  },

  /**
   * Returns a shallow copy of this dictionary.
   */
  copy: function Dict_copy() {
    var newItems = {};
    for (var [key, val] in this.items)
      newItems[key] = val;
    return new Dict(newItems);
  },

  /*
   * List and iterator functions
   *
   * No guarantees whatsoever are made about the order of elements.
   */

  /**
   * Returns a list of all the keys in the dictionary in an arbitrary order.
   */
  listkeys: function Dict_listkeys() {
    return [unconvert(k) for (k in this._state.items)];
  },

  /**
   * Returns a list of all the values in the dictionary in an arbitrary order.
   */
  listvalues: function Dict_listvalues() {
    var items = this._state.items;
    return [items[k] for (k in items)];
  },

  /**
   * Returns a list of all the items in the dictionary as key-value pairs
   * in an arbitrary order.
   */
  listitems: function Dict_listitems() {
    var items = this._state.items;
    return [[unconvert(k), items[k]] for (k in items)];
  },

  /**
   * Returns an iterator over all the keys in the dictionary in an arbitrary
   * order. No guarantees are made about what happens if the dictionary is
   * mutated during iteration.
   */
  get keys() {
    // If we don't capture this._state.items here then the this-binding will be
    // incorrect when the generator is executed
    var items = this._state.items;
    return (unconvert(k) for (k in items));
  },

  /**
   * Returns an iterator over all the values in the dictionary in an arbitrary
   * order. No guarantees are made about what happens if the dictionary is
   * mutated during iteration.
   */
  get values() {
    // If we don't capture this._state.items here then the this-binding will be
    // incorrect when the generator is executed
    var items = this._state.items;
    return (items[k] for (k in items));
  },

  /**
   * Returns an iterator over all the items in the dictionary as key-value pairs
   * in an arbitrary order. No guarantees are made about what happens if the
   * dictionary is mutated during iteration.
   */
  get items() {
    // If we don't capture this._state.items here then the this-binding will be
    // incorrect when the generator is executed
    var items = this._state.items;
    return ([unconvert(k), items[k]] for (k in items));
  },

  /**
   * Returns a String representation of this dictionary.
   */
  toString: function Dict_toString() {
    return "{" +
      [(key + ": " + val) for ([key, val] in this.items)].join(", ") +
      "}";
  },

  /**
   * Returns a JSON representation of this dictionary.
   */
  toJSON: function Dict_toJSON() {
    let obj = {};
    for (let [key, item] of Iterator(this._state.items)) {
      obj[unconvert(key)] = item;
    }
    return JSON.stringify(obj);
  },
});