light-traits.js 22 KB

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