0c86c89151f6a269438e56a2b7f4a8c36bc790f5
[pwl6.git] / src / yuu / yT.js
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
5 */
6
7 (function (module) {
8 "use strict";
9
10 /** yT - yuu type creation
11
12 yT is a function like `Object.create`, but with support for
13 more powerful property descriptors (referred to as _extended
14 property descriptors (XPDs)_). Most standard JavaScript
15 property descriptors are valid XPDs, but XPDs allow shortcuts
16 to specify common descriptor patterns.
17
18 Equivalents for `Object.defineProperties` and
19 `Object.defineProperty` are also provided.
20
21 ## Extended Property Descriptors
22
23 Any standard descriptor that has a `get` function (called an
24 'accessor descriptor') or a `value` property (called a 'data
25 descriptor') is also valid XPD.
26
27 An extended descriptor that does not have either of these and
28 does not meet any of the other conditions below is equivalent
29 to a data descriptor with a `value` of itself, referred to as
30 a 'bare value descriptor'. For example, the following two XPDs
31 are equivalent:
32
33 { x: 1 } { x: { value: 1 } }
34
35 (This means a useless descriptor like `{}` is interpreted as a
36 data descriptor with value `undefined` by `Object.create` but
37 a bare value descriptor with value `{}` by `yT`.)
38
39 In addition, extended descriptors have several other formats
40 which can be used to generate different kinds of idomatic
41 accessors:
42
43 * `alias` - This property is a synonym for a different property.
44 Reads and writes to it will be mapped to reads and writes
45 to the aliased property.
46 { firstChild: { alias: "children[0]" } }
47 is equivalent to
48 { firstChild: {
49 get: function () { return this.children[0]; },
50 set: function (v) { this.children[0] = v; }
51 } }
52
53 * `proxy` - This property is a synonym for a method call
54 on a different object.
55 { start: { proxy: "engine.start" } }
56 is equivalent to
57 { start: { value: function () {
58 return this.engine.start.apply(this.engine, arguments);
59 } } }
60
61 Using `alias` rather than `proxy` would result in the
62 wrong (non-engine) `this` argument being passed to the
63 `start` method.
64
65 * `aliasSynthetic` - Aliases don't work if one of the
66 properties in the lookup chain is a temporary variable.
67 For example, aliasing `x` to `position[0]` is no good if
68 `position` itself has a getter like `transform.slice(12)`
69 because the assignment to the returned value will have
70 no effect.
71
72 `aliasSynthetic` can be used to capture the temporary,
73 assign to it, and then assign the whole temporary back.
74 { x: { aliasSynthetic: "position[0]" } }
75 Generates the same `get` as `alias`, but `set` is
76 function (v) {
77 var t = this.position;
78 t[0] = v;
79 this.position = t;
80 }
81
82 `aliasSynthetic` assumes the next-to-last value is the
83 temporary, e.g. in `a.b.c.d`, `a.b.c` is the temporary.
84 If rather e.g. `a.b` is the temporary, you can separate
85 the `alias` and `synthetic` parts:
86 { x: { alias: "a.b.c.d", synthetic: "a.b" } }
87
88 * `swizzle` - Swizzling lets you treat separate properties
89 as one array property. For example if you have a color
90 class with individual r, g, and b properties,
91 { rgb: { swizzle: ["r", "g", "b"] } }
92 is equivalent to
93 { rgb: {
94 get: function () { return [this.r, this.g, this.b] },
95 set: function (v) {
96 this.r = v[0];
97 this.g = v[1];
98 this.b = v[2];
99 }
100 } }
101
102 Any descriptor may also have the `chainable` property set,
103 which generates a chainable setter function. Chainable data
104 descriptors are writable by default.
105 { x: { value: 0, chainable: true } }
106 is equivalent to
107 { x: { value: 0, writable: true },
108 setX: { value: function (v) { this.x = v; return this; } }
109 }
110
111 ## Example
112
113 An example of a simple 2D Point class using XPDs:
114
115 var Point = yT(Object, {
116 constructor: function (x, y) {
117 this.x = x || 0;
118 this.y = y || 0;
119 },
120
121 0: { alias: "x" },
122 1: { alias: "y" },
123 xy: { swizzle: "xy" },
124 yx: { swizzle: "yx" },
125
126 angle: {
127 chainable: true,
128 get: function () {
129 return Math.atan2(this.y, this.x);
130 },
131 set: function (angle) {
132 var magnitude = this.magnitude;
133 this.x = Math.cos(angle) * magnitude;
134 this.y = Math.sin(angle) * magnitude;
135 }
136 },
137
138 magnitude: {
139 chainable: true,
140 get: function () {
141 return Math.sqrt(this.x * this.x + this.y * this.y);
142 },
143 set: function (magnitude) {
144 var angle = this.angle;
145 this.x = Math.cos(angle) * magnitude;
146 this.y = Math.sin(angle) * magnitude;
147 }
148 },
149
150 length: 2
151 });
152
153 var p = new Point(3, 0);
154 p[0] === 3; // true
155 p.y = 4; p[1] == 4; // true
156 p.magnitude = 1; // normalize
157 p.xy = p.yx; // transpose
158
159 new Point().setMagnitude(m).setAngle(a);
160 // construct a point from an angle and magnitude in a
161 // single step.
162 */
163
164 /* jshint -W054 */ // Function constructors are the whole point here.
165
166 function isFunction (o) {
167 /** Check if a value is a function */
168 return Object.prototype.toString.call(o) === '[object Function]';
169 }
170
171 function update (dst, src) {
172 /** Copy every enumerable key and its value from src to dst */
173 for (var k in src)
174 dst[k] = src[k];
175 return dst;
176 }
177
178 function rooted (path) {
179 if (typeof path === "number")
180 return "[" + path + "]";
181 else if (path[0] !== "." && path[0] !== "[")
182 return "." + path;
183 else
184 return path;
185 }
186
187 function chainableName (name) {
188 return "set" + name[0].toUpperCase() + name.substring(1);
189 }
190
191 function chainableSetter (name) {
192 return new Function(
193 "value",
194 "this" + rooted(name) + " = value; " + "return this;");
195 }
196
197 function alias (path, readonly) {
198 path = rooted(path);
199 return readonly
200 ? { get: new Function("return this" + path + ";") }
201 : { get: new Function("return this" + path + ";"),
202 set: new Function("v", "return this" + path + " = v;") };
203 }
204
205 function proxy (path) {
206 path = rooted(path);
207 var prop = path.substr(0, path.lastIndexOf("."));
208 return {
209 value: new Function(
210 "return this" + path + ".apply(this" + prop + ", arguments);")
211 };
212 }
213
214 function swizzle (props) {
215 props = Array.prototype.map.call(props, function (p) {
216 return "this" + rooted(p);
217 });
218 var assignments = props.map(function (p, i) {
219 return p + " = x[" + i + "];";
220 });
221 return {
222 get: new Function("return [" + props.join(", ") + "];"),
223 set: new Function("x", assignments.join("\n"))
224 };
225 }
226
227 function aliasSynthetic (path) {
228 var idx = Math.max(path.lastIndexOf("."), path.lastIndexOf("["));
229 return synthetic(path.substring(0, idx), path);
230 }
231
232 function synthetic (synthPath, path) {
233 synthPath = rooted(synthPath);
234 path = rooted(path).substring(synthPath.length);
235 return {
236 get: new Function("return this" + synthPath + path + ";"),
237 set: new Function("v",
238 ["var t = this" + synthPath + ";",
239 "t" + path + " = v; ",
240 "this" + synthPath + " = t;",
241 ].join("\n"))
242 };
243 }
244
245 function isAccessorDescriptor (pd) {
246 /** Check if `pd` is a descriptor describing an accessor */
247 return pd && typeof pd === "object" && isFunction(pd.get);
248 }
249
250 function isDataDescriptor (pd) {
251 /** Check if `pd` is a descriptor describing data */
252 return pd && typeof pd === "object" && ("value" in pd);
253 }
254
255 function isDescriptor (pd) {
256 /** Check if `pd` is any kind of descriptor */
257 return isDataDescriptor(pd) || isAccessorDescriptor(pd);
258 }
259
260 function createDescriptors (name, xpd, pds) {
261 if (xpd.alias !== undefined) {
262 if (xpd.synthetic !== undefined)
263 pds[name] = update(xpd, synthetic(xpd.alias, xpd.synthetic));
264 else
265 pds[name] = update(xpd, alias(xpd.alias, xpd.readonly));
266 } else if (xpd.proxy !== undefined) {
267 pds[name] = update(xpd, proxy(xpd.proxy));
268 } else if (xpd.swizzle !== undefined) {
269 pds[name] = update(xpd, swizzle(xpd.swizzle));
270 } else if (xpd.aliasSynthetic !== undefined) {
271 pds[name] = update(xpd, aliasSynthetic(xpd.aliasSynthetic));
272 }
273
274 pds[name] = isDescriptor(xpd) ? xpd : { value: xpd };
275 if (xpd.chainable) {
276 pds[chainableName(name)] = { value: chainableSetter(name) };
277 if (isDataDescriptor(pds[name]) && !("writable" in pds[name]))
278 pds[name].writable = true;
279 }
280 }
281
282 function xpdsToPds (xpds) {
283 var pds = {};
284 Object.keys(xpds).forEach(function (k) {
285 createDescriptors(k, xpds[k], pds);
286 });
287 return pds;
288 }
289
290 function T (parent, xpds) {
291 /** Create a new class described by XPDs
292
293 This function is similar to `Object.create` but supports
294 extended property descriptors as described above.
295
296 yT(parent, map of extended descriptors)
297 yT(map of extended descriptors)
298 // The parent is assumed to be Object
299
300 This returns a _function_ which acts as a constructor for
301 the provided type. If there is a property descriptor named
302 `constructor` it is returned; otherwise a new function
303 that calls the parent's.
304
305 `parent` may be either the parent constructor or a
306 prototype.
307 */
308 if (!xpds) {
309 xpds = parent;
310 parent = Object;
311 }
312 parent = isFunction(parent) && parent.prototype || parent;
313 var pds = xpdsToPds(xpds);
314 var ctor = pds.constructor && pds.constructor.value;
315 if (!ctor || ctor === {}.constructor) {
316 ctor = parent
317 ? function () { parent.constructor.apply(this, arguments); }
318 : function () { };
319 pds.constructor = { value: ctor };
320 }
321 ctor.prototype = Object.create(parent, pds);
322 return ctor;
323 }
324
325 T.defineProperties = function (o, xpds) {
326 /** Add properties described by XPDs to an object
327
328 This function is similar to`Object.defineProperties`,
329 but supports the same features as `yT`.
330 */
331 return Object.defineProperties(o, xpdsToPds(xpds));
332 };
333
334 T.defineProperty = function (o, name, xpd) {
335 /** Add a property described by an XPD to an object
336
337 This function is similar to`Object.defineProperty`,
338 but supports the same features as `yT`.
339 */
340 var xpds = {};
341 xpds[name] = xpd;
342 return T.defineProperties(o, xpds);
343 };
344
345 T.getPropertyDescriptor = function (o, name) {
346 /** Look up a descriptor from `o`'s prototype chain */
347 var v = null;
348 while (!v && o) {
349 v = Object.getOwnPropertyDescriptor(o, name);
350 o = Object.getPrototypeOf(o);
351 }
352 return v;
353 };
354
355 T.isAccessorDescriptor = isAccessorDescriptor;
356 T.isDataDescriptor = isDataDescriptor;
357 T.isDescriptor = isDescriptor;
358
359 if (module)
360 module.exports = T;
361 else
362 this.yT = T;
363 }).call(typeof exports === "undefined" ? this : exports,
364 typeof exports === "undefined" ? null : module);