principal
Version:
Async-friendly subclassing and option management
160 lines (158 loc) • 5.54 kB
JavaScript
var principal = module.exports = {
add: function(callerOptions, myOptions) {
if (!callerOptions) {
callerOptions = {};
}
// TODO Array.isArray is not portable to all browsers
if (Array.isArray(callerOptions)) {
// "Why not just push onto callerOptions?" We don't want to modify it
// from the POV of unrelated code.
var options = callerOptions.slice();
// We want our subclasses to win any conflicts, not us
options.unshift(myOptions);
return options;
} else {
// We want our subclasses to win any conflicts, not us
return [ myOptions, callerOptions ];
}
},
resolveOptions: function(self, optionsChain) {
self.options = {};
var merging;
var name;
var value;
var subName;
var subValue;
var i;
for (i in optionsChain) {
merging = optionsChain[i];
for (name in merging) {
value = merging[name];
var merged = false;
if (typeof(value) === 'object') {
for (subName in value) {
subValue = value[subName];
if (subName === '$append') {
if (!Array.isArray(subValue)) {
subValue = [ subValue ];
}
if (self.options[name]) {
self.options[name] = self.options[name].concat(subValue);
} else {
self.options[name] = subValue;
}
merged = true;
break;
} else if (subName === '$prepend') {
if (!Array.isArray(subValue)) {
subValue = [ subValue ];
}
if (self.options[name]) {
self.options[name] = subValue.concat(self.options[name]);
} else {
self.options[name] = subValue;
}
merged = true;
break;
} else if (subName === '$alter') {
// Expects a custom function that modifies the options.
// This function must return a value which becomes the new
// value of the option. However the function also receives the
// entire options object and the name of the field for more
// advanced manipulations that touch multiple options.
self.options[name] = subValue(self.options[name], self.options, name);
merged = true;
break;
}
}
}
if (merged) {
continue;
}
self.options[name] = value;
}
}
},
resolveMethods: function(self, methodsChain) {
var i;
var merging;
var name;
var value;
var subName;
var subValue;
var _super;
for (i = 0; (i < methodsChain.length); i++) {
merging = methodsChain[i];
for (name in merging) {
value = merging[name];
var merged = false;
if (typeof(value) === 'object') {
for (subName in value) {
subValue = value[subName];
if (subName === '$before') {
_super = self[name];
if (_super) {
wrapBefore(_super, name, subValue);
merged = true;
}
} else if (subName === '$after') {
_super = self[name];
if (_super) {
wrapAfter(_super, name, subValue);
merged = true;
}
}
}
}
if (merged) {
continue;
}
self[name] = value;
}
}
function wrapBefore(_super, name, before) {
self[name] = function() {
var args = Array.prototype.slice.call(arguments, 0);
var beforeArgs = args.slice(0, args.length - 1);
var callback = args[args.length - 1];
// One of the perks of a `before` method is having a chance
// to change the arguments to _super, so we leave it up to
// the `before` method to invoke _super with the same or
// different arguments
beforeArgs.push(_super);
beforeArgs.push(callback);
return before.apply(self, beforeArgs);
};
}
function wrapAfter(_super, name, after) {
self[name] = function() {
var args = Array.prototype.slice.call(arguments, 0);
var superArgs = args.slice(0, args.length - 1);
var callback = args[args.length - 1];
superArgs.push(function(err) {
// after function receives original parameters,
// then all arguments passed to callback of _super,
// then the final callback
var afterArgs = args.slice(0, args.length - 1);
afterArgs = afterArgs.concat(Array.prototype.slice.call(arguments, 0));
afterArgs.push(callback);
return after.apply(self, afterArgs);
});
return _super.apply(self, superArgs);
};
}
},
subclass: function(self, base, callerOptions, callerMethods, myOptions, myMethods, callback) {
var optionsChain = principal.add(callerOptions, myOptions);
var methodsChain = principal.add(callerMethods, myMethods);
if (base === Object) {
principal.resolveOptions(self, optionsChain);
principal.resolveMethods(self, methodsChain);
// Using setImmediate here guarantees we can assign the newly
// constructed object to a variable before the callback is invoked
setImmediate(function() { return callback(null); });
return self;
}
return (base).call(self, optionsChain, methodsChain, callback);
}
};