Skip to content

Instantly share code, notes, and snippets.

@jhartman86
Created October 27, 2015 16:42
Show Gist options
  • Select an option

  • Save jhartman86/9fffb561a640f533b6d4 to your computer and use it in GitHub Desktop.

Select an option

Save jhartman86/9fffb561a640f533b6d4 to your computer and use it in GitHub Desktop.

Revisions

  1. jhartman86 created this gist Oct 27, 2015.
    190 changes: 190 additions & 0 deletions node-injector.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,190 @@
    /**
    * 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();