Skip to content

Instantly share code, notes, and snippets.

@jhartman86
Created October 27, 2015 16:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jhartman86/9fffb561a640f533b6d4 to your computer and use it in GitHub Desktop.
Save jhartman86/9fffb561a640f533b6d4 to your computer and use it in GitHub Desktop.
AngularJS-style dependency injection for Node
/**
* 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