|
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 // Design inspired by: http://www.traitsjs.org/ |
|
12 |
|
13 // shortcuts |
|
14 const getOwnPropertyNames = Object.getOwnPropertyNames, |
|
15 getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor, |
|
16 hasOwn = Object.prototype.hasOwnProperty, |
|
17 _create = Object.create; |
|
18 |
|
19 function doPropertiesMatch(object1, object2, name) { |
|
20 // If `object1` has property with the given `name` |
|
21 return name in object1 ? |
|
22 // then `object2` should have it with the same value. |
|
23 name in object2 && object1[name] === object2[name] : |
|
24 // otherwise `object2` should not have property with the given `name`. |
|
25 !(name in object2); |
|
26 } |
|
27 |
|
28 /** |
|
29 * Compares two trait custom property descriptors if they are the same. If |
|
30 * both are `conflict` or all the properties of descriptor are equal returned |
|
31 * value will be `true`, otherwise it will be `false`. |
|
32 * @param {Object} desc1 |
|
33 * @param {Object} desc2 |
|
34 */ |
|
35 function areSame(desc1, desc2) { |
|
36 return ('conflict' in desc1 && desc1.conflict && |
|
37 'conflict' in desc2 && desc2.conflict) || |
|
38 (doPropertiesMatch(desc1, desc2, 'get') && |
|
39 doPropertiesMatch(desc1, desc2, 'set') && |
|
40 doPropertiesMatch(desc1, desc2, 'value') && |
|
41 doPropertiesMatch(desc1, desc2, 'enumerable') && |
|
42 doPropertiesMatch(desc1, desc2, 'required') && |
|
43 doPropertiesMatch(desc1, desc2, 'conflict')); |
|
44 } |
|
45 |
|
46 /** |
|
47 * Converts array to an object whose own property names represent |
|
48 * values of array. |
|
49 * @param {String[]} names |
|
50 * @returns {Object} |
|
51 * @example |
|
52 * Map(['foo', ...]) => { foo: true, ...} |
|
53 */ |
|
54 function Map(names) { |
|
55 let map = {}; |
|
56 for each (let name in names) |
|
57 map[name] = true; |
|
58 return map; |
|
59 } |
|
60 |
|
61 |
|
62 const ERR_CONFLICT = 'Remaining conflicting property: ', |
|
63 ERR_REQUIRED = 'Missing required property: '; |
|
64 /** |
|
65 * Constant singleton, representing placeholder for required properties. |
|
66 * @type {Object} |
|
67 */ |
|
68 const required = { toString: function()'<Trait.required>' }; |
|
69 exports.required = required; |
|
70 |
|
71 /** |
|
72 * Generates custom **required** property descriptor. Descriptor contains |
|
73 * non-standard property `required` that is equal to `true`. |
|
74 * @param {String} name |
|
75 * property name to generate descriptor for. |
|
76 * @returns {Object} |
|
77 * custom property descriptor |
|
78 */ |
|
79 function Required(name) { |
|
80 function required() { throw new Error(ERR_REQUIRED + name) } |
|
81 return { |
|
82 get: required, |
|
83 set: required, |
|
84 required: true |
|
85 }; |
|
86 } |
|
87 |
|
88 /** |
|
89 * Generates custom **conflicting** property descriptor. Descriptor contains |
|
90 * non-standard property `conflict` that is equal to `true`. |
|
91 * @param {String} name |
|
92 * property name to generate descriptor for. |
|
93 * @returns {Object} |
|
94 * custom property descriptor |
|
95 */ |
|
96 function Conflict(name) { |
|
97 function conflict() { throw new Error(ERR_CONFLICT + name) } |
|
98 return { |
|
99 get: conflict, |
|
100 set: conflict, |
|
101 conflict: true |
|
102 }; |
|
103 } |
|
104 |
|
105 /** |
|
106 * Function generates custom properties descriptor of the `object`s own |
|
107 * properties. All the inherited properties are going to be ignored. |
|
108 * Properties with values matching `required` singleton will be marked as |
|
109 * 'required' properties. |
|
110 * @param {Object} object |
|
111 * Set of properties to generate trait from. |
|
112 * @returns {Object} |
|
113 * Properties descriptor of all of the `object`'s own properties. |
|
114 */ |
|
115 function trait(properties) { |
|
116 let result = {}, |
|
117 keys = getOwnPropertyNames(properties); |
|
118 for each (let key in keys) { |
|
119 let descriptor = getOwnPropertyDescriptor(properties, key); |
|
120 result[key] = (required === descriptor.value) ? Required(key) : descriptor; |
|
121 } |
|
122 return result; |
|
123 } |
|
124 exports.Trait = exports.trait = trait; |
|
125 |
|
126 /** |
|
127 * Composes new trait. If two or more traits have own properties with the |
|
128 * same name, the new trait will contain a 'conflict' property for that name. |
|
129 * 'compose' is a commutative and associative operation, and the order of its |
|
130 * arguments is not significant. |
|
131 * |
|
132 * @params {Object} trait |
|
133 * Takes traits as an arguments |
|
134 * @returns {Object} |
|
135 * New trait containing the combined own properties of all the traits. |
|
136 * @example |
|
137 * var newTrait = compose(trait_1, trait_2, ..., trait_N); |
|
138 */ |
|
139 function compose(trait1, trait2) { |
|
140 let traits = Array.slice(arguments, 0), |
|
141 result = {}; |
|
142 for each (let trait in traits) { |
|
143 let keys = getOwnPropertyNames(trait); |
|
144 for each (let key in keys) { |
|
145 let descriptor = trait[key]; |
|
146 // if property already exists and it's not a requirement |
|
147 if (hasOwn.call(result, key) && !result[key].required) { |
|
148 if (descriptor.required) |
|
149 continue; |
|
150 if (!areSame(descriptor, result[key])) |
|
151 result[key] = Conflict(key); |
|
152 } else { |
|
153 result[key] = descriptor; |
|
154 } |
|
155 } |
|
156 } |
|
157 return result; |
|
158 } |
|
159 exports.compose = compose; |
|
160 |
|
161 /** |
|
162 * Composes new trait with the same own properties as the original trait, |
|
163 * except that all property names appearing in the first argument are replaced |
|
164 * by 'required' property descriptors. |
|
165 * @param {String[]} keys |
|
166 * Array of strings property names. |
|
167 * @param {Object} trait |
|
168 * A trait some properties of which should be excluded. |
|
169 * @returns {Object} |
|
170 * @example |
|
171 * var newTrait = exclude(['name', ...], trait) |
|
172 */ |
|
173 function exclude(keys, trait) { |
|
174 let exclusions = Map(keys), |
|
175 result = {}; |
|
176 |
|
177 keys = getOwnPropertyNames(trait); |
|
178 |
|
179 for each (let key in keys) { |
|
180 if (!hasOwn.call(exclusions, key) || trait[key].required) |
|
181 result[key] = trait[key]; |
|
182 else |
|
183 result[key] = Required(key); |
|
184 } |
|
185 return result; |
|
186 } |
|
187 |
|
188 /** |
|
189 * Composes a new trait with all of the combined properties of the argument |
|
190 * traits. In contrast to `compose`, `override` immediately resolves all |
|
191 * conflicts resulting from this composition by overriding the properties of |
|
192 * later traits. Trait priority is from left to right. I.e. the properties of |
|
193 * the leftmost trait are never overridden. |
|
194 * @params {Object} trait |
|
195 * @returns {Object} |
|
196 * @examples |
|
197 * // override is associative: |
|
198 * override(t1,t2,t3) |
|
199 * // is equivalent to |
|
200 * override(t1, override(t2, t3)) |
|
201 * // or |
|
202 * to override(override(t1, t2), t3) |
|
203 * |
|
204 * // override is not commutative: |
|
205 * override(t1,t2) |
|
206 * // is not equivalent to |
|
207 * override(t2,t1) |
|
208 */ |
|
209 function override() { |
|
210 let traits = Array.slice(arguments, 0), |
|
211 result = {}; |
|
212 for each (let trait in traits) { |
|
213 let keys = getOwnPropertyNames(trait); |
|
214 for each(let key in keys) { |
|
215 let descriptor = trait[key]; |
|
216 if (!hasOwn.call(result, key) || result[key].required) |
|
217 result[key] = descriptor; |
|
218 } |
|
219 } |
|
220 return result; |
|
221 } |
|
222 exports.override = override; |
|
223 |
|
224 /** |
|
225 * Composes a new trait with the same properties as the original trait, except |
|
226 * that all properties whose name is an own property of map will be renamed to |
|
227 * map[name], and a 'required' property for name will be added instead. |
|
228 * @param {Object} map |
|
229 * An object whose own properties serve as a mapping from old names to new |
|
230 * names. |
|
231 * @param {Object} trait |
|
232 * A trait object |
|
233 * @returns {Object} |
|
234 * @example |
|
235 * var newTrait = rename(map, trait); |
|
236 */ |
|
237 function rename(map, trait) { |
|
238 let result = {}, |
|
239 keys = getOwnPropertyNames(trait); |
|
240 for each(let key in keys) { |
|
241 // must be renamed & it's not requirement |
|
242 if (hasOwn.call(map, key) && !trait[key].required) { |
|
243 let alias = map[key]; |
|
244 if (hasOwn.call(result, alias) && !result[alias].required) |
|
245 result[alias] = Conflict(alias); |
|
246 else |
|
247 result[alias] = trait[key]; |
|
248 if (!hasOwn.call(result, key)) |
|
249 result[key] = Required(key); |
|
250 } else { // must not be renamed or its a requirement |
|
251 // property is not in result trait yet |
|
252 if (!hasOwn.call(result, key)) |
|
253 result[key] = trait[key]; |
|
254 // property is already in resulted trait & it's not requirement |
|
255 else if (!trait[key].required) |
|
256 result[key] = Conflict(key); |
|
257 } |
|
258 } |
|
259 return result; |
|
260 } |
|
261 |
|
262 /** |
|
263 * Composes new resolved trait, with all the same properties as the original |
|
264 * trait, except that all properties whose name is an own property of |
|
265 * resolutions will be renamed to `resolutions[name]`. If it is |
|
266 * `resolutions[name]` is `null` value is changed into a required property |
|
267 * descriptor. |
|
268 * function can be implemented as `rename(map,exclude(exclusions, trait))` |
|
269 * where map is the subset of mappings from oldName to newName and exclusions |
|
270 * is an array of all the keys that map to `null`. |
|
271 * Note: it's important to **first** `exclude`, **then** `rename`, since |
|
272 * `exclude` and rename are not associative. |
|
273 * @param {Object} resolutions |
|
274 * An object whose own properties serve as a mapping from old names to new |
|
275 * names, or to `null` if the property should be excluded. |
|
276 * @param {Object} trait |
|
277 * A trait object |
|
278 * @returns {Object} |
|
279 * Resolved trait with the same own properties as the original trait. |
|
280 */ |
|
281 function resolve(resolutions, trait) { |
|
282 let renames = {}, |
|
283 exclusions = [], |
|
284 keys = getOwnPropertyNames(resolutions); |
|
285 for each (let key in keys) { // pre-process renamed and excluded properties |
|
286 if (resolutions[key]) // old name -> new name |
|
287 renames[key] = resolutions[key]; |
|
288 else // name -> undefined |
|
289 exclusions.push(key); |
|
290 } |
|
291 return rename(renames, exclude(exclusions, trait)); |
|
292 } |
|
293 exports.resolve = resolve; |
|
294 |
|
295 /** |
|
296 * `create` is like `Object.create`, except that it ensures that: |
|
297 * - an exception is thrown if 'trait' still contains required properties |
|
298 * - an exception is thrown if 'trait' still contains conflicting |
|
299 * properties |
|
300 * @param {Object} |
|
301 * prototype of the completed object |
|
302 * @param {Object} trait |
|
303 * trait object to be turned into a complete object |
|
304 * @returns {Object} |
|
305 * An object with all of the properties described by the trait. |
|
306 */ |
|
307 function create(proto, trait) { |
|
308 let properties = {}, |
|
309 keys = getOwnPropertyNames(trait); |
|
310 for each(let key in keys) { |
|
311 let descriptor = trait[key]; |
|
312 if (descriptor.required && !hasOwn.call(proto, key)) |
|
313 throw new Error(ERR_REQUIRED + key); |
|
314 else if (descriptor.conflict) |
|
315 throw new Error(ERR_CONFLICT + key); |
|
316 else |
|
317 properties[key] = descriptor; |
|
318 } |
|
319 return _create(proto, properties); |
|
320 } |
|
321 exports.create = create; |
|
322 |