UNPKG

extensible

Version:

Create highly extensible software components.

314 lines (262 loc) 9.29 kB
/* jshint eqnull:true, evil: true */ var xtend = require('xtend'); var create = require('object-create'); var jsonify = require('jsonify'); var has = require('has'); var slice = Array.prototype.slice; var reservedNames = { $use: true, $defineMethod: true, $getMethodDescriptor: true, $eachMethodDescriptor: true, $eachLayer: true, $fork: true, $instance: true, $instanceOf: true, $layerClass: true, $layers: true, $descriptors: true, $parent: true }; function findAvailable(args, name) { for (var i = 0, l = args.length; i < l; i++) if (args[i] === name) return findAvailable(args, name + '_'); return name; } function installLayerClass(target, sup) { var layerClass = function Layer(impl, next) { this.impl = impl; this.next = next; layerClass.hasInstances = true; }; if (sup) { layerClass.prototype = create(sup.$layerClass.prototype, { constructor: { value: layerClass, enumerable: false, writable: true, configurable: true } }); } target.$layerClass = layerClass; } // Base object only containing the basic infrastructure for adding // methods and layers function Extensible() { // methods installed into this object this.$descriptors = {}; this.$layers = {top: null}; installLayerClass(this); } // Adds or upgrade an extensible method to the object. For minimal overhead // and better vm optimization, the wrapper functions will be generated. Extensible.prototype.$defineMethod = function(name, args, descriptor) { if (has(reservedNames, name)) throw new Error("Name '" + name + "' is reserved, use another"); // any name is accepted, let json handle escaping var str = jsonify.stringify(name); // Get an array of the method parameters var argsArray = args && args.split(/\s*,\s*/) || []; // Layer argument. 'findAvailable' will ensure we dont clash names var layer = findAvailable(argsArray, 'layer'); // Extra argument that layers can use to send data across non-adjacent // layers in the chain. Each layer has the opportunity of modifying the // state(stateNew) but if a falsy value is passed, the original state // persists(stateOrig) var stateOrig = findAvailable(argsArray, 'stateOrig'); var stateNew = findAvailable(argsArray, 'stateNew'); // Helpers on the generated method var ctx = findAvailable(argsArray, 'ctx'); var cargs = args ? ',' + args : ''; // override for context passed to middlewares. For now only used for // the $call method on functions var self = findAvailable(argsArray, 'self'); var next = findAvailable(argsArray, 'next'); // Actual function arguments. It contains an extra self that can be used // to override the context for callable objects var fargs; if (name === '$call') { fargs = argsArray.slice(); fargs.push(self); } // largs is the parameter array for the layer version of the method. It // contains three extra parameters: 'layer', 'stateOrig' and 'self' var largs = argsArray.slice(); largs.push(layer); largs.push(stateOrig); largs.push(self); // nargs is the parameter array for the 'next' function passed to the layer // implementation. It has the same signature as the method, but with an // trailing 'stateNew' parameter that can be used to modify state for // the next layer var nargs = argsArray.slice(); nargs.push(stateNew); if (this.$layerClass.hasInstances) // If any layers were added for the current layer class, create a new one // inheriting from it. This ensures we can safely override methods without // affecting previous layers installLayerClass(this, this); var oargs, oldDescriptor; if (oldDescriptor = this.$getMethodDescriptor(name)) { // oargs is how we call the next layer. for that we use the old descriptor // arguments oargs = oldDescriptor.args.slice(); // nargs also must be updated to receive arguments compatible with the // next layer nargs = oargs.slice(); nargs.push(stateNew); oargs = oargs.join(', '); } else { oargs = args; } var coargs = oargs ? ',' + oargs : ''; // Generate the wrapper on the object itself. It just delegates work to // the top layer this[name] = new Function(name === '$call' ? fargs.join(', ') : args, ( this.DEBUG ? '\n if (!this.$layerClass.hasInstances)' + '\n throw new Error("Layer class implementation missing");' : '' ) + '\n return this.$layers.top['+str+'].call(this'+cargs+','+ ' this.$layers.top, null, '+ (name === '$call' ? self + ' ||' : '') + 'this);\n'); // Generate the implementation wrapper on the layer class itself. // This function's job is to check if an the implementation for the method // is defined on the current layer, and if so, call the implementation with // the received args and a 'next' function that user code can use to call // the next layer. // If no implementation is provided, the next layer will be called directly. // This is what allows the user to only 'wrap' certain methods while leaving // other unnafected this.$layerClass.prototype[name] = new Function(largs.join(', '), '\n var '+ctx+' = this;' + '\n var '+next+' = '+layer+'.next;' + '\n if ('+layer+'.impl['+str+'])' + '\n return '+layer+'.impl['+str+'].call('+self + cargs+', ' + '\n function('+nargs.join(', ')+') {' + ( this.DEBUG ? '\n if (!'+next+' || typeof '+next+'['+str+'] !== "function")' + '\n throw new Error(' + ' "Method \'"+ '+str+' +"\' has no more layers");\n' : '' ) + '\n return '+next+'['+str+'].call('+ctx + coargs+', ' + next+', '+stateNew+' || '+stateOrig+', '+self+');' + '\n }, '+layer+', '+stateOrig+', '+ctx+');' + ( oldDescriptor && this.DEBUG ? '\n throw new Error(' + '"Must provide an implementation for the upgraded method");\n' : '' ) + ( this.DEBUG ? '\n if (!'+next+' || typeof '+next+'['+str+'] !== "function")' + '\n throw new Error(' + ' "Method \'"+ '+str+' +"\' has no more layers");\n' : '' ) + '\n return '+next+'['+str+'].call('+ctx + cargs+', ' + next+', '+stateOrig+', '+self+');\n'); descriptor = xtend({ name: name, args: argsArray, objectMethod: this[name], layerMethod: this.$layerClass.prototype[name] }, descriptor); if (oldDescriptor) { // keep a link to the old descriptor descriptor.old = oldDescriptor; } this.$descriptors[name] = descriptor; }; // Wraps the object into a new layer Extensible.prototype.$use = function(middleware, opts) { if (typeof middleware === 'function') // call factory function middleware = middleware.call(this, opts); this.$layers.top = new this.$layerClass(middleware, this.$layers.top); }; // Returns metadata associated with the method `name`. Extensible.prototype.$getMethodDescriptor = function(name) { return this.$descriptors[name]; }; // Iterates through each installed method Extensible.prototype.$eachMethodDescriptor = function(cb) { for (var k in this.$descriptors) { if (!has(this.$descriptors, k)) continue; cb(this.$descriptors[k]); } }; // Iterates through each layer of this object Extensible.prototype.$eachLayer = function(cb) { function next(layer) { if (!layer) return; if (layer.next) next(layer.next); cb(layer); } next(this.$layers.top); }; // Creates a new object whose prototype is set to the current object. Extensible.prototype.$instance = function() { var rv; if (typeof this === 'function') rv = this.$fork(true, true); else rv = this.$fork(false, true); if (rv.$constructor) { var args = slice.call(arguments); args.push(rv.$layers.top); rv.$constructor.apply(rv, args); } return rv; }; // Forks by creating a new object with all methods and layers from the current // object. Extensible.prototype.$fork = function(asCallable, inheritProperties) { var rv; if (asCallable || typeof this === 'function') { rv = function Callable() { var args = slice.call(arguments); args.push(this); return rv.$call.apply(rv, args); }; var k; // cant inherit from functions in a portable way, so simply copy // the relevant properties for (k in reservedNames) { if (!has(reservedNames, k)) continue; rv[k] = this[k]; } // and also methods defined with '$defineMethod' for (k in this.$descriptors) { if (!has(this.$descriptors, k)) continue; rv[k] = this[k]; } } else { rv = create(this); } if (!inheritProperties) { rv.$layers = {top: null}; rv.$descriptors = xtend({}, this.$descriptors); rv.$layerClass = this.$layerClass; this.$eachLayer(function(layer) { rv.$use(layer.impl); }); } rv.$parent = this; return rv; }; Extensible.prototype.$instanceOf = function(other) { var current = this; while (current) { if (current.$parent === other) return true; current = current.$parent; } return false; }; module.exports = function extensible() { return new Extensible(); };