-
Notifications
You must be signed in to change notification settings - Fork 103
/
stampit.js
369 lines (325 loc) · 12.4 KB
/
stampit.js
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
/**
* Stampit
**
* Create objects from reusable, composable behaviors.
**
* Copyright (c) 2013 Eric Elliott
* http://opensource.org/licenses/MIT
**/
import forEach from 'lodash/collection/forEach';
import isFunction from 'lodash/lang/isFunction';
import isObject from 'lodash/lang/isObject';
import {
merge,
mergeChainNonFunctions,
mergeUnique,
mixin,
mixinChainFunctions,
mixinFunctions
} from 'supermixer';
const create = Object.create;
function isThenable(value) {
return value && isFunction(value.then);
}
function extractFunctions(...args) {
const result = [];
if (isFunction(args[0])) {
forEach(args, fn => { // assuming all the arguments are functions
if (isFunction(fn)) {
result.push(fn);
}
});
} else if (isObject(args[0])) {
forEach(args, obj => {
forEach(obj, fn => {
if (isFunction(fn)) {
result.push(fn);
}
});
});
}
return result;
}
function addMethods(fixed, ...methods) {
return mixinFunctions(fixed.methods, ...methods);
}
function addRefs(fixed, ...refs) {
fixed.refs = fixed.state = mixin(fixed.refs, ...refs);
return fixed.refs;
}
function addInit(fixed, ...inits) {
const extractedInits = extractFunctions(...inits);
fixed.init = fixed.enclose = fixed.init.concat(extractedInits);
return fixed.init;
}
function addProps(fixed, ...propses) {
return merge(fixed.props, ...propses);
}
function addStatic(fixed, ...statics) {
return mixin(fixed.static, ...statics);
}
function cloneAndExtend(fixed, extensionFunction, ...args) {
const stamp = stampit(fixed);
extensionFunction(stamp.fixed, ...args);
return stamp;
}
function compose(...factories) {
const result = stampit();
forEach(factories, source => {
if (source && source.fixed) {
addMethods(result.fixed, source.fixed.methods);
// We might end up having two different stampit modules loaded and used in conjunction.
// These || operators ensure that old stamps could be combined with the current version stamps.
// 'state' is the old name for 'refs'
addRefs(result.fixed, source.fixed.refs || source.fixed.state);
// 'enclose' is the old name for 'init'
addInit(result.fixed, source.fixed.init || source.fixed.enclose);
addProps(result.fixed, source.fixed.props);
addStatic(result.fixed, source.fixed.static);
}
});
return mixin(result, result.fixed.static);
}
/**
* Return a factory function that will produce new objects using the
* components that are passed in or composed.
*
* @param {Object} [options] Options to build stamp from: `{ methods, refs, init, props }`
* @param {Object} [options.methods] A map of method names and bodies for delegation.
* @param {Object} [options.refs] A map of property names and values to be mixed into each new object.
* @param {Object} [options.init] A closure (function) used to create private data and privileged methods.
* @param {Object} [options.props] An object to be deeply cloned into each newly stamped object.
* @param {Object} [options.static] An object to be mixed into each `this` and derived stamps (not objects!).
* @return {Function(refs)} factory A factory to produce objects.
* @return {Function(refs)} factory.create Just like calling the factory function.
* @return {Object} factory.fixed An object map containing the stamp components.
* @return {Function(methods)} factory.methods Add methods to the stamp. Chainable.
* @return {Function(refs)} factory.refs Add references to the stamp. Chainable.
* @return {Function(Function(context))} factory.init Add a closure which called on object instantiation. Chainable.
* @return {Function(props)} factory.props Add deeply cloned properties to the produced objects. Chainable.
* @return {Function(stamps)} factory.compose Combine several stamps into single. Chainable.
* @return {Function(statics)} factory.static Add properties to the stamp (not objects!). Chainable.
*/
const stampit = function stampit(options) {
const fixed = {methods: {}, refs: {}, init: [], props: {}, static: {}};
fixed.state = fixed.refs; // Backward compatibility. 'state' is the old name for 'refs'.
fixed.enclose = fixed.init; // Backward compatibility. 'enclose' is the old name for 'init'.
if (options) {
addMethods(fixed, options.methods);
addRefs(fixed, options.refs);
addInit(fixed, options.init);
addProps(fixed, options.props);
addStatic(fixed, options.static);
}
const factory = function Factory(refs, ...args) {
let instance = mixin(create(fixed.methods), fixed.refs, refs);
mergeUnique(instance, fixed.props); // props are safely merged into refs
let nextPromise = null;
if (fixed.init.length > 0) {
forEach(fixed.init, fn => {
if (!isFunction(fn)) {
return; // not a function, do nothing.
}
// Check if we are in the async mode.
if (!nextPromise) {
// Call the init().
const callResult = fn.call(instance, {args, instance, stamp: factory});
if (!callResult) {
return; // The init() returned nothing. Proceed to the next init().
}
// Returned value is meaningful.
// It will replace the stampit-created object.
if (!isThenable(callResult)) {
instance = callResult; // stamp is synchronous so far.
return;
}
// This is the sync->async conversion point.
// Since now our factory will return a promise, not an object.
nextPromise = callResult;
} else {
// As long as one of the init() functions returned a promise,
// now our factory will 100% return promise too.
// Linking the init() functions into the promise chain.
nextPromise = nextPromise.then(newInstance => {
// The previous promise might want to return a value,
// which we should take as a new object instance.
instance = newInstance || instance;
// Calling the following init().
// NOTE, than `fn` is wrapped to a closure within the forEach loop.
const callResult = fn.call(instance, {args, instance, stamp: factory});
// Check if call result is truthy.
if (!callResult) {
// The init() returned nothing. Thus using the previous object instance.
return instance;
}
if (!isThenable(callResult)) {
// This init() was synchronous and returned a meaningful value.
instance = callResult;
// Resolve the instance for the next `then()`.
return instance;
}
// The init() returned another promise. It is becoming our nextPromise.
return callResult;
});
}
});
}
// At the end we should resolve the last promise and
// return the resolved value (as a promise too).
return nextPromise ? nextPromise.then(newInstance => newInstance || instance) : instance;
};
const refsMethod = cloneAndExtend.bind(null, fixed, addRefs);
const initMethod = cloneAndExtend.bind(null, fixed, addInit);
return mixin(factory, {
/**
* Creates a new object instance from the stamp.
*/
create: factory,
/**
* The stamp components.
*/
fixed,
/**
* Take n objects and add them to the methods list of a new stamp. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
methods: cloneAndExtend.bind(null, fixed, addMethods),
/**
* Take n objects and add them to the references list of a new stamp. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
refs: refsMethod,
/**
* @deprecated since v2.0. Use refs() instead.
* Alias to refs().
* @return {Function} A new stamp (factory object).
*/
state: refsMethod,
/**
* Take n functions, an array of functions, or n objects and add
* the functions to the initializers list of a new stamp. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
init: initMethod,
/**
* @deprecated since v2.0. User init() instead.
* Alias to init().
* @return {Function} A new stamp (factory object).
*/
enclose: initMethod,
/**
* Take n objects and deep merge them to the properties. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
props: cloneAndExtend.bind(null, fixed, addProps),
/**
* Take n objects and add all props to the factory object. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
static(...statics) {
const newStamp = cloneAndExtend(factory.fixed, addStatic, ...statics);
return mixin(newStamp, newStamp.fixed.static);
},
/**
* Take one or more factories produced from stampit() and
* combine them with `this` to produce and return a new factory.
* Combining overrides properties with last-in priority.
* @param {[Function]|...Function} factories Stampit factories.
* @return {Function} A new stampit factory composed from arguments.
*/
compose: (...factories) => compose(factory, ...factories)
}, fixed.static);
};
// Static methods
function isStamp(obj) {
return (
isFunction(obj) &&
isFunction(obj.methods) &&
// isStamp can be called for old stampit factory object.
// We should check old names (state and enclose) too.
(isFunction(obj.refs) || isFunction(obj.state)) &&
(isFunction(obj.init) || isFunction(obj.enclose)) &&
isFunction(obj.props) &&
isFunction(obj.static) &&
isObject(obj.fixed)
);
}
function convertConstructor(Constructor) {
const stamp = stampit();
stamp.fixed.refs = stamp.fixed.state = mergeChainNonFunctions(stamp.fixed.refs, Constructor.prototype);
mixin(stamp, mixin(stamp.fixed.static, Constructor));
mixinChainFunctions(stamp.fixed.methods, Constructor.prototype);
addInit(stamp.fixed, ({ instance, args }) => Constructor.apply(instance, args));
return stamp;
}
function shortcutMethod(extensionFunction, ...args) {
const stamp = stampit();
extensionFunction(stamp.fixed, ...args);
return stamp;
}
function mixinWithConsoleWarning() {
console.log(
'stampit.mixin(), .mixIn(), .extend(), and .assign() are deprecated.',
'Use Object.assign or _.assign instead');
return mixin.apply(this, arguments);
}
export default mixin(stampit, {
/**
* Take n objects and add them to the methods list of a new stamp. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
methods: shortcutMethod.bind(null, addMethods),
/**
* Take n objects and add them to the references list of a new stamp. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
refs: shortcutMethod.bind(null, addRefs),
/**
* Take n functions, an array of functions, or n objects and add
* the functions to the initializers list of a new stamp. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
init: shortcutMethod.bind(null, addInit),
/**
* Take n objects and deep merge them to the properties. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
props: shortcutMethod.bind(null, addProps),
/**
* Take n objects and add all props to the factory object. Creates new stamp.
* @return {Function} A new stamp (factory object).
*/
static(...statics) {
const newStamp = shortcutMethod(addStatic, ...statics);
return mixin(newStamp, newStamp.fixed.static);
},
/**
* Take two or more factories produced from stampit() and
* combine them to produce a new factory.
* Combining overrides properties with last-in priority.
* @param {[Function]|...Function} factories Stamps produced by stampit().
* @return {Function} A new stampit factory composed from arguments.
*/
compose: compose,
/**
* @deprecated Since v2.2. Use Object.assign or _.assign instead.
* Alias to Object.assign.
*/
mixin: mixinWithConsoleWarning,
extend: mixinWithConsoleWarning,
mixIn: mixinWithConsoleWarning,
assign: mixinWithConsoleWarning,
/**
* Check if an object is a stamp.
* @param {Object} obj An object to check.
* @returns {Boolean}
*/
isStamp,
/**
* Take an old-fashioned JS constructor and return a stampit stamp
* that you can freely compose with other stamps.
* @param {Function} Constructor
* @return {Function} A composable stampit factory (aka stamp).
*/
convertConstructor
});