addon-sdk/source/lib/sdk/deprecated/light-traits.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:65d99115002f
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 "use strict";
6
7 module.metadata = {
8 "stability": "deprecated"
9 };
10
11 // `var` is being used in the module in order to make it reusable in
12 // environments in which `let` is not yet supported.
13
14 // Shortcut to `Object.prototype.hasOwnProperty.call`.
15 // owns(object, name) would be the same as
16 // Object.prototype.hasOwnProperty.call(object, name);
17 var owns = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
18
19 /**
20 * Whether or not given property descriptors are equivalent. They are
21 * equivalent either if both are marked as 'conflict' or 'required' property
22 * or if all the properties of descriptors are equal.
23 * @param {Object} actual
24 * @param {Object} expected
25 */
26 function equivalentDescriptors(actual, expected) {
27 return (actual.conflict && expected.conflict) ||
28 (actual.required && expected.required) ||
29 equalDescriptors(actual, expected);
30 }
31 /**
32 * Whether or not given property descriptors define equal properties.
33 */
34 function equalDescriptors(actual, expected) {
35 return actual.get === expected.get &&
36 actual.set === expected.set &&
37 actual.value === expected.value &&
38 !!actual.enumerable === !!expected.enumerable &&
39 !!actual.configurable === !!expected.configurable &&
40 !!actual.writable === !!expected.writable;
41 }
42
43 // Utilities that throwing exceptions for a properties that are marked
44 // as "required" or "conflict" properties.
45 function throwConflictPropertyError(name) {
46 throw new Error("Remaining conflicting property: `" + name + "`");
47 }
48 function throwRequiredPropertyError(name) {
49 throw new Error("Missing required property: `" + name + "`");
50 }
51
52 /**
53 * Generates custom **required** property descriptor. Descriptor contains
54 * non-standard property `required` that is equal to `true`.
55 * @param {String} name
56 * property name to generate descriptor for.
57 * @returns {Object}
58 * custom property descriptor
59 */
60 function RequiredPropertyDescriptor(name) {
61 // Creating function by binding first argument to a property `name` on the
62 // `throwConflictPropertyError` function. Created function is used as a
63 // getter & setter of the created property descriptor. This way we ensure
64 // that we throw exception late (on property access) if object with
65 // `required` property was instantiated using built-in `Object.create`.
66 var accessor = throwRequiredPropertyError.bind(null, name);
67 return { get: accessor, set: accessor, required: true };
68 }
69
70 /**
71 * Generates custom **conflicting** property descriptor. Descriptor contains
72 * non-standard property `conflict` that is equal to `true`.
73 * @param {String} name
74 * property name to generate descriptor for.
75 * @returns {Object}
76 * custom property descriptor
77 */
78 function ConflictPropertyDescriptor(name) {
79 // For details see `RequiredPropertyDescriptor` since idea is same.
80 var accessor = throwConflictPropertyError.bind(null, name);
81 return { get: accessor, set: accessor, conflict: true };
82 }
83
84 /**
85 * Tests if property is marked as `required` property.
86 */
87 function isRequiredProperty(object, name) {
88 return !!object[name].required;
89 }
90
91 /**
92 * Tests if property is marked as `conflict` property.
93 */
94 function isConflictProperty(object, name) {
95 return !!object[name].conflict;
96 }
97
98 /**
99 * Function tests whether or not method of the `source` object with a given
100 * `name` is inherited from `Object.prototype`.
101 */
102 function isBuiltInMethod(name, source) {
103 var target = Object.prototype[name];
104
105 // If methods are equal then we know it's `true`.
106 return target == source ||
107 // If `source` object comes form a different sandbox `==` will evaluate
108 // to `false`, in that case we check if functions names and sources match.
109 (String(target) === String(source) && target.name === source.name);
110 }
111
112 /**
113 * Function overrides `toString` and `constructor` methods of a given `target`
114 * object with a same-named methods of a given `source` if methods of `target`
115 * object are inherited / copied from `Object.prototype`.
116 * @see create
117 */
118 function overrideBuiltInMethods(target, source) {
119 if (isBuiltInMethod("toString", target.toString)) {
120 Object.defineProperty(target, "toString", {
121 value: source.toString,
122 configurable: true,
123 enumerable: false
124 });
125 }
126
127 if (isBuiltInMethod("constructor", target.constructor)) {
128 Object.defineProperty(target, "constructor", {
129 value: source.constructor,
130 configurable: true,
131 enumerable: false
132 });
133 }
134 }
135
136 /**
137 * Composes new trait with the same own properties as the original trait,
138 * except that all property names appearing in the first argument are replaced
139 * by "required" property descriptors.
140 * @param {String[]} keys
141 * Array of strings property names.
142 * @param {Object} trait
143 * A trait some properties of which should be excluded.
144 * @returns {Object}
145 * @example
146 * var newTrait = exclude(["name", ...], trait)
147 */
148 function exclude(names, trait) {
149 var map = {};
150
151 Object.keys(trait).forEach(function(name) {
152
153 // If property is not excluded (the array of names does not contain it),
154 // or it is a "required" property, copy it to the property descriptor `map`
155 // that will be used for creation of resulting trait.
156 if (!~names.indexOf(name) || isRequiredProperty(trait, name))
157 map[name] = { value: trait[name], enumerable: true };
158
159 // For all the `names` in the exclude name array we create required
160 // property descriptors and copy them to the `map`.
161 else
162 map[name] = { value: RequiredPropertyDescriptor(name), enumerable: true };
163 });
164
165 return Object.create(Trait.prototype, map);
166 }
167
168 /**
169 * Composes new instance of `Trait` with a properties of a given `trait`,
170 * except that all properties whose name is an own property of `renames` will
171 * be renamed to `renames[name]` and a `"required"` property for name will be
172 * added instead.
173 *
174 * For each renamed property, a required property is generated. If
175 * the `renames` map two properties to the same name, a conflict is generated.
176 * If the `renames` map a property to an existing unrenamed property, a
177 * conflict is generated.
178 *
179 * @param {Object} renames
180 * An object whose own properties serve as a mapping from old names to new
181 * names.
182 * @param {Object} trait
183 * A new trait with renamed properties.
184 * @returns {Object}
185 * @example
186 *
187 * // Return trait with `bar` property equal to `trait.foo` and with
188 * // `foo` and `baz` "required" properties.
189 * var renamedTrait = rename({ foo: "bar", baz: null }), trait);
190 *
191 * // t1 and t2 are equivalent traits
192 * var t1 = rename({a: "b"}, t);
193 * var t2 = compose(exclude(["a"], t), { a: { required: true }, b: t[a] });
194 */
195 function rename(renames, trait) {
196 var map = {};
197
198 // Loop over all the properties of the given `trait` and copy them to a
199 // property descriptor `map` that will be used for the creation of the
200 // resulting trait. Also, rename properties in the `map` as specified by
201 // `renames`.
202 Object.keys(trait).forEach(function(name) {
203 var alias;
204
205 // If the property is in the `renames` map, and it isn't a "required"
206 // property (which should never need to be aliased because "required"
207 // properties never conflict), then we must try to rename it.
208 if (owns(renames, name) && !isRequiredProperty(trait, name)) {
209 alias = renames[name];
210
211 // If the `map` already has the `alias`, and it isn't a "required"
212 // property, that means the `alias` conflicts with an existing name for a
213 // provided trait (that can happen if >=2 properties are aliased to the
214 // same name). In this case we mark it as a conflicting property.
215 // Otherwise, everything is fine, and we copy property with an `alias`
216 // name.
217 if (owns(map, alias) && !map[alias].value.required) {
218 map[alias] = {
219 value: ConflictPropertyDescriptor(alias),
220 enumerable: true
221 };
222 }
223 else {
224 map[alias] = {
225 value: trait[name],
226 enumerable: true
227 };
228 }
229
230 // Regardless of whether or not the rename was successful, we check to
231 // see if the original `name` exists in the map (such a property
232 // could exist if previous another property was aliased to this `name`).
233 // If it isn't, we mark it as "required", to make sure the caller
234 // provides another value for the old name, which methods of the trait
235 // might continue to reference.
236 if (!owns(map, name)) {
237 map[name] = {
238 value: RequiredPropertyDescriptor(name),
239 enumerable: true
240 };
241 }
242 }
243
244 // Otherwise, either the property isn't in the `renames` map (thus the
245 // caller is not trying to rename it) or it is a "required" property.
246 // Either way, we don't have to alias the property, we just have to copy it
247 // to the map.
248 else {
249 // The property isn't in the map yet, so we copy it over.
250 if (!owns(map, name)) {
251 map[name] = { value: trait[name], enumerable: true };
252 }
253
254 // The property is already in the map (that means another property was
255 // aliased with this `name`, which creates a conflict if the property is
256 // not marked as "required"), so we have to mark it as a "conflict"
257 // property.
258 else if (!isRequiredProperty(trait, name)) {
259 map[name] = {
260 value: ConflictPropertyDescriptor(name),
261 enumerable: true
262 };
263 }
264 }
265 });
266 return Object.create(Trait.prototype, map);
267 }
268
269 /**
270 * Composes new resolved trait, with all the same properties as the original
271 * `trait`, except that all properties whose name is an own property of
272 * `resolutions` will be renamed to `resolutions[name]`.
273 *
274 * If `resolutions[name]` is `null`, the value is mapped to a property
275 * descriptor that is marked as a "required" property.
276 */
277 function resolve(resolutions, trait) {
278 var renames = {};
279 var exclusions = [];
280
281 // Go through each mapping in `resolutions` object and distribute it either
282 // to `renames` or `exclusions`.
283 Object.keys(resolutions).forEach(function(name) {
284
285 // If `resolutions[name]` is a truthy value then it's a mapping old -> new
286 // so we copy it to `renames` map.
287 if (resolutions[name])
288 renames[name] = resolutions[name];
289
290 // Otherwise it's not a mapping but an exclusion instead in which case we
291 // add it to the `exclusions` array.
292 else
293 exclusions.push(name);
294 });
295
296 // First `exclude` **then** `rename` and order is important since
297 // `exclude` and `rename` are not associative.
298 return rename(renames, exclude(exclusions, trait));
299 }
300
301 /**
302 * Create a Trait (a custom property descriptor map) that represents the given
303 * `object`'s own properties. Property descriptor map is a "custom", because it
304 * inherits from `Trait.prototype` and it's property descriptors may contain
305 * two attributes that is not part of the ES5 specification:
306 *
307 * - "required" (this property must be provided by another trait
308 * before an instance of this trait can be created)
309 * - "conflict" (when the trait is composed with another trait,
310 * a unique value for this property is provided by two or more traits)
311 *
312 * Data properties bound to the `Trait.required` singleton exported by
313 * this module will be marked as "required" properties.
314 *
315 * @param {Object} object
316 * Map of properties to compose trait from.
317 * @returns {Trait}
318 * Trait / Property descriptor map containing all the own properties of the
319 * given argument.
320 */
321 function trait(object) {
322 var map;
323 var trait = object;
324
325 if (!(object instanceof Trait)) {
326 // If the passed `object` is not already an instance of `Trait`, we create
327 // a property descriptor `map` containing descriptors for the own properties
328 // of the given `object`. `map` is then used to create a `Trait` instance
329 // after all properties are mapped. Note that we can't create a trait and
330 // then just copy properties into it since that will fail for inherited
331 // read-only properties.
332 map = {};
333
334 // Each own property of the given `object` is mapped to a data property
335 // whose value is a property descriptor.
336 Object.keys(object).forEach(function (name) {
337
338 // If property of an `object` is equal to a `Trait.required`, it means
339 // that it was marked as "required" property, in which case we map it
340 // to "required" property.
341 if (Trait.required ==
342 Object.getOwnPropertyDescriptor(object, name).value) {
343 map[name] = {
344 value: RequiredPropertyDescriptor(name),
345 enumerable: true
346 };
347 }
348 // Otherwise property is mapped to it's property descriptor.
349 else {
350 map[name] = {
351 value: Object.getOwnPropertyDescriptor(object, name),
352 enumerable: true
353 };
354 }
355 });
356
357 trait = Object.create(Trait.prototype, map);
358 }
359 return trait;
360 }
361
362 /**
363 * Compose a property descriptor map that inherits from `Trait.prototype` and
364 * contains property descriptors for all the own properties of the passed
365 * traits.
366 *
367 * If two or more traits have own properties with the same name, the returned
368 * trait will contain a "conflict" property for that name. Composition is a
369 * commutative and associative operation, and the order of its arguments is
370 * irrelevant.
371 */
372 function compose(trait1, trait2/*, ...*/) {
373 // Create a new property descriptor `map` to which all the own properties
374 // of the passed traits are copied. This map will be used to create a `Trait`
375 // instance that will be the result of this composition.
376 var map = {};
377
378 // Properties of each passed trait are copied to the composition.
379 Array.prototype.forEach.call(arguments, function(trait) {
380 // Copying each property of the given trait.
381 Object.keys(trait).forEach(function(name) {
382
383 // If `map` already owns a property with the `name` and it is not
384 // marked "required".
385 if (owns(map, name) && !map[name].value.required) {
386
387 // If the source trait's property with the `name` is marked as
388 // "required", we do nothing, as the requirement was already resolved
389 // by a property in the `map` (because it already contains a
390 // non-required property with that `name`). But if properties are just
391 // different, we have a name clash and we substitute it with a property
392 // that is marked "conflict".
393 if (!isRequiredProperty(trait, name) &&
394 !equivalentDescriptors(map[name].value, trait[name])
395 ) {
396 map[name] = {
397 value: ConflictPropertyDescriptor(name),
398 enumerable: true
399 };
400 }
401 }
402
403 // Otherwise, the `map` does not have an own property with the `name`, or
404 // it is marked "required". Either way, the trait's property is copied to
405 // the map (if the property of the `map` is marked "required", it is going
406 // to be resolved by the property that is being copied).
407 else {
408 map[name] = { value: trait[name], enumerable: true };
409 }
410 });
411 });
412
413 return Object.create(Trait.prototype, map);
414 }
415
416 /**
417 * `defineProperties` is like `Object.defineProperties`, except that it
418 * ensures that:
419 * - An exception is thrown if any property in a given `properties` map
420 * is marked as "required" property and same named property is not
421 * found in a given `prototype`.
422 * - An exception is thrown if any property in a given `properties` map
423 * is marked as "conflict" property.
424 * @param {Object} object
425 * Object to define properties on.
426 * @param {Object} properties
427 * Properties descriptor map.
428 * @returns {Object}
429 * `object` that was passed as a first argument.
430 */
431 function defineProperties(object, properties) {
432
433 // Create a map into which we will copy each verified property from the given
434 // `properties` description map. We use it to verify that none of the
435 // provided properties is marked as a "conflict" property and that all
436 // "required" properties are resolved by a property of an `object`, so we
437 // can throw an exception before mutating object if that isn't the case.
438 var verifiedProperties = {};
439
440 // Coping each property from a given `properties` descriptor map to a
441 // verified map of property descriptors.
442 Object.keys(properties).forEach(function(name) {
443
444 // If property is marked as "required" property and we don't have a same
445 // named property in a given `object` we throw an exception. If `object`
446 // has same named property just skip this property since required property
447 // is was inherited and there for requirement was satisfied.
448 if (isRequiredProperty(properties, name)) {
449 if (!(name in object))
450 throwRequiredPropertyError(name);
451 }
452
453 // If property is marked as "conflict" property we throw an exception.
454 else if (isConflictProperty(properties, name)) {
455 throwConflictPropertyError(name);
456 }
457
458 // If property is not marked neither as "required" nor "conflict" property
459 // we copy it to verified properties map.
460 else {
461 verifiedProperties[name] = properties[name];
462 }
463 });
464
465 // If no exceptions were thrown yet, we know that our verified property
466 // descriptor map has no properties marked as "conflict" or "required",
467 // so we just delegate to the built-in `Object.defineProperties`.
468 return Object.defineProperties(object, verifiedProperties);
469 }
470
471 /**
472 * `create` is like `Object.create`, except that it ensures that:
473 * - An exception is thrown if any property in a given `properties` map
474 * is marked as "required" property and same named property is not
475 * found in a given `prototype`.
476 * - An exception is thrown if any property in a given `properties` map
477 * is marked as "conflict" property.
478 * @param {Object} prototype
479 * prototype of the composed object
480 * @param {Object} properties
481 * Properties descriptor map.
482 * @returns {Object}
483 * An object that inherits form a given `prototype` and implements all the
484 * properties defined by a given `properties` descriptor map.
485 */
486 function create(prototype, properties) {
487
488 // Creating an instance of the given `prototype`.
489 var object = Object.create(prototype);
490
491 // Overriding `toString`, `constructor` methods if they are just inherited
492 // from `Object.prototype` with a same named methods of the `Trait.prototype`
493 // that will have more relevant behavior.
494 overrideBuiltInMethods(object, Trait.prototype);
495
496 // Trying to define given `properties` on the `object`. We use our custom
497 // `defineProperties` function instead of build-in `Object.defineProperties`
498 // that behaves exactly the same, except that it will throw if any
499 // property in the given `properties` descriptor is marked as "required" or
500 // "conflict" property.
501 return defineProperties(object, properties);
502 }
503
504 /**
505 * Composes new trait. If two or more traits have own properties with the
506 * same name, the new trait will contain a "conflict" property for that name.
507 * "compose" is a commutative and associative operation, and the order of its
508 * arguments is not significant.
509 *
510 * **Note:** Use `Trait.compose` instead of calling this function with more
511 * than one argument. The multiple-argument functionality is strictly for
512 * backward compatibility.
513 *
514 * @params {Object} trait
515 * Takes traits as an arguments
516 * @returns {Object}
517 * New trait containing the combined own properties of all the traits.
518 * @example
519 * var newTrait = compose(trait_1, trait_2, ..., trait_N)
520 */
521 function Trait(trait1, trait2) {
522
523 // If the function was called with one argument, the argument should be
524 // an object whose properties are mapped to property descriptors on a new
525 // instance of Trait, so we delegate to the trait function.
526 // If the function was called with more than one argument, those arguments
527 // should be instances of Trait or plain property descriptor maps
528 // whose properties should be mixed into a new instance of Trait,
529 // so we delegate to the compose function.
530
531 return trait2 === undefined ? trait(trait1) : compose.apply(null, arguments);
532 }
533
534 Object.freeze(Object.defineProperties(Trait.prototype, {
535 toString: {
536 value: function toString() {
537 return "[object " + this.constructor.name + "]";
538 }
539 },
540
541 /**
542 * `create` is like `Object.create`, except that it ensures that:
543 * - An exception is thrown if this trait defines a property that is
544 * marked as required property and same named property is not
545 * found in a given `prototype`.
546 * - An exception is thrown if this trait contains property that is
547 * marked as "conflict" property.
548 * @param {Object}
549 * prototype of the compared object
550 * @returns {Object}
551 * An object with all of the properties described by the trait.
552 */
553 create: {
554 value: function createTrait(prototype) {
555 return create(undefined === prototype ? Object.prototype : prototype,
556 this);
557 },
558 enumerable: true
559 },
560
561 /**
562 * Composes a new resolved trait, with all the same properties as the original
563 * trait, except that all properties whose name is an own property of
564 * `resolutions` will be renamed to the value of `resolutions[name]`. If
565 * `resolutions[name]` is `null`, the property is marked as "required".
566 * @param {Object} resolutions
567 * An object whose own properties serve as a mapping from old names to new
568 * names, or to `null` if the property should be excluded.
569 * @returns {Object}
570 * New trait with the same own properties as the original trait but renamed.
571 */
572 resolve: {
573 value: function resolveTrait(resolutions) {
574 return resolve(resolutions, this);
575 },
576 enumerable: true
577 }
578 }));
579
580 /**
581 * @see compose
582 */
583 Trait.compose = Object.freeze(compose);
584 Object.freeze(compose.prototype);
585
586 /**
587 * Constant singleton, representing placeholder for required properties.
588 * @type {Object}
589 */
590 Trait.required = Object.freeze(Object.create(Object.prototype, {
591 toString: {
592 value: Object.freeze(function toString() {
593 return "<Trait.required>";
594 })
595 }
596 }));
597 Object.freeze(Trait.required.toString.prototype);
598
599 exports.Trait = Object.freeze(Trait);

mercurial