Created
October 27, 2015 16:42
-
-
Save jhartman86/9fffb561a640f533b6d4 to your computer and use it in GitHub Desktop.
AngularJS-style dependency injection for Node
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Angular-ish style dependency injection for node; relies on (but does not | |
* override) node's require() mechanisms; merely compliments the loading process | |
* and allows you to structure your app so that you *know* what you'll get back | |
* from a dependency. Otherwise known as an inversion of control container. | |
* @usage: require('node-injector').using(module).factory('thing', ['dep1', 'dep2', function( dep1, dep2 ){ }]) | |
* @returns {Injector} | |
* @constructor | |
*/ | |
function InversionController(){ | |
// @ref: http://noder.io/guide/quickstart.html | |
// @ref: http://www.royjacobs.org/intravenous/ | |
var self = this | |
,graph = {} | |
,configs = { | |
requireBase : './' | |
,errorOnCyclical : true | |
,errorOnRedeclare : true | |
}; | |
/** | |
* Dependency injection handler; receives arguments | |
* as an array such that dependencies are declared | |
* as strings corresponding to their names registered | |
* via factory/service declarations. The focus is on | |
* letting require() do its thing in as standard a manner | |
* as possible, since that handles caching internally :). | |
* @param {array} args ['dep1', 'dep2', func] | |
* @return {array} Dependency tree (ordered as | |
* resolved functions) | |
*/ | |
function resolver( key, args ){ | |
var injections = [] | |
,error; | |
// Iterate through dependency strings, and load via | |
// require if haven't already been loaded. | |
for(var i = 0, l = args.length; i < l; i++){ | |
// Should always be last argument | |
if( typeof args[i] === 'function' ){ | |
break; | |
} | |
injections.push(require(configs['requireBase'] + args[i])); | |
} | |
/** | |
* Cyclical dependency checker; looks through the | |
* dependency graph and tries to find dependencies that | |
* reference each other, IF they are factories or services | |
* (since run blocks aren't declaring themselves of anything). | |
*/ | |
if( key && !graph[key] ){ | |
graph[key] = args.slice(0, (args.length - 1)); | |
for(var x = 0, y = graph[key].length; x < y; x++){ | |
if( graph[graph[key][x]] && (graph[graph[key][x]].indexOf(key) !== -1) ){ | |
error = new Error("Cyclical dependency detected; dependencies " + graph[key][x] + " & " + key + " both depend on each other."); | |
} | |
} | |
} | |
/** | |
* Throw an error if cyclical dependencies are found. | |
*/ | |
if( error && configs.errorOnCyclical ){ | |
throw error; | |
} | |
return injections; | |
} | |
/** | |
* Checks the dependency graph to make sure a factory or service | |
* isn't being redeclared. | |
* @param {string} key Service descriptor | |
* @throws error | |
*/ | |
function checkIfRedeclaring( key ){ | |
if( configs.errorOnRedeclare && graph[key] ){ | |
throw new Error('Redeclaring factory or service: ' + key); | |
} | |
} | |
/** | |
* Set a config value | |
* @param {string} key Config key | |
* @param {mixed} value Config value | |
* @return {this} This for chaining | |
*/ | |
this.setConfig = function( key, value ){ | |
configs[key] = value; | |
return self; | |
}; | |
/** | |
* Get either a specific config value by passing in | |
* config key, or the whole config object by calling | |
* without an argument | |
* @param {string} key Config key | |
* @return {mixed} Config value | |
*/ | |
this.getConfig = function( key ){ | |
if( key ){ | |
return configs[key]; | |
} | |
return configs; | |
}; | |
/** | |
* Get the current dependency graph (what depends on what) | |
* @return {object} {mod:['dep1','dep2'],mod2:['dep1',...]} | |
*/ | |
this.getDependencyGraph = function(){ | |
return graph; | |
}; | |
/** | |
* Run something and inject any dependencies; this is NOT part of | |
* the Injector class as this can be used to simply inject things into | |
* a one off function, OR, as a loader that just invokes other things that | |
* need to be "executed" (eg. a job queue that only has to be called once to | |
* be doing its... job of waiting for... jobs). | |
* @param {array} args Injection format | |
* @return {mixed|null} Not required to return anything | |
* as this doesn't register itself for availability anywhere | |
* else. | |
*/ | |
this.run = function( args ){ | |
if( typeof(args[args.length - 1]) === 'function' ){ | |
args[args.length - 1].apply(null, resolver(false, args)); | |
return; | |
} | |
resolver(false, args); | |
}; | |
/** | |
* Injector class | |
* @param {object} _module Module instance from the file | |
* we're specifically binding against | |
*/ | |
function Injector( _module ){ | |
if( typeof(_module) !== 'object' || ! _module['exports'] ){ | |
throw('Injector requires the module to be passed in'); | |
} | |
this._module = _module; | |
} | |
/** | |
* Registery a factory (simply calls the func() with its | |
* required dependencies.) | |
* @param {string} key Name to register the dependency by | |
* @param {array} args Injection format | |
* @return {mixed} Whatever the dependency chooses to publish | |
*/ | |
Injector.prototype.factory = function( key, args ){ | |
checkIfRedeclaring(key); | |
this._module.exports = args[args.length-1].apply(null, resolver(key, args)); | |
return this; | |
}; | |
/** | |
* Register a service, which is the same as factory except | |
* that the func argument at the end will get new'd. | |
* @param {string} key Name to register dependency by | |
* @param {array} args Injection format | |
* @return {object} Instance | |
*/ | |
Injector.prototype.service = function( key, args ){ | |
checkIfRedeclaring(key); | |
this._module.exports = new (Function.prototype.bind.apply(args[args.length-1], [null].concat(resolver(key, args))))(); | |
return this; | |
}; | |
/** | |
* This method is really the entry point for using this | |
* entire thing, as its responsible for receiving the relevant | |
* module to bind against, and making it available to the | |
* Injector class. | |
* @param {object} _module module var from file | |
* @return {object} Instance of Injector class with | |
* the relevant module kept as a property. | |
*/ | |
this.using = function( _module ){ | |
return new Injector(_module); | |
}; | |
return self; | |
} | |
module.exports = new InversionController(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment