@clevyr/functional-di

    0.3.3 • Public • Published

    Functional Dependency Injector

    A function based dependency injector for JavaScript and NodeJS.

    • Auto injects dependencies into resources.
    • Follows the AngularJS DI model with some additions.
    • Supports singleton/caching on definition and on resolve.
    • Supports non-dependency injected arguments (optional or required) in functions.
    • Supports optionally injected arguments in functions.

    Uses the AngularJS notation of $ before a variable indicating a public app variable. While $$ before a variable indicates a private app variable.

    Public app variables

    • $inject - Defines the array of dependencies to inject as arguments. Supports '?' for optional and '()' for resolveArgs.
    • $resolveArgs - Defines the array of dependencies that will be resolved as arguments, not dependency injected. Supports '?' for optional.
    • $singleton - Defines that the result of this factory should be cached.
    • $filename - Defines the path to the source file of the injected object. Used for building an injection history for debugging.

    Private app variables

    • $$registerSourceFile - Defines the path to the file where this factory was registered with the DI.
    • $$curried - Defines whether or not this factory has been curried.
    • $$resolveArgs - Defines the array of arguments that need to be provided when an item is resolved.

    Installation

    npm install
    

    Usage

    First require 'functional-di'. This will return a diContainer factory. Each time you call this, you can generate a new dependency injection container.

    const diContainer = require('functional-di')();

    In order to use the dependency injector, each dependency must be defined before it can be injected. It can be defined directly (if not a function) or through the use of a factory function using the register() method. Since 'functional-di' returns a factory, make sure to export your container instance so it can be referenced elsewhere in the code.

    // File di.js
    const diContainer = require('functional-di')();
     
    const someDependencyFactory = require('./someDependency.js');
     
    const staticValue = 'someStaticValueOrObject';
     
    diContainer.register('someDependency', someDependencyFactory);
    diContainer.register('staticValue', staticValue, null, {filename: __filename});
     
    module.exports = diContainer;

    Each factory can depend upon other dependencies that can be auto-injected by the diContainer using the $inject array. Let's create a factory with a dependency:

    // File someDependency.js
    module.exports = function someDependencyFactory(staticValue) {
      console.log(staticValue === 'someStaticValueOrObject'); // Returns true.
      return {
        doSomething: true
      };
    };
     
    module.exports.$inject = ['staticValue'];
    module.exports.$filename = __filename;

    Now in order to actually get the value of our di factory, use the resolve() method.

    // (di.js) is from the above example.
    const diContainer = require('./di.js');
     
    const someDependency = diContainer.resolve('someDependency');

    You can pass extra parameters on factory functions down through the dependency injection when resolve() is called. These parameters must be defined in the $inject array, but they are surrounded by parentheses. These extra parameters can be optional (see the 'Optional Arguments' section below).

    Let's build a new dependency that takes in an extra argument.

    // File otherDependency.js
    module.exports = function otherDependencyFactory(staticValue, isBlue) {
      console.log('isBlue', isBlue);
      return {
        doSomethingElse: true
      };
    };
     
    module.exports.$inject = ['staticValue', '(isBlue)'];
    module.exports.$filename = __filename;

    Now let's register it with the diContainer and resolve it.

    // File di.js
    const diContainer = require('functional-di')();
     
    diContainer.register('otherDependency', otherDependencyFactory);
     
    diContainer.resolve('otherDependency', {isBlue: true});
    // We will see 'isBlue true' in the console log.
     
    diContainer.resolve('otherDependency', {isBlue: false});
    // We will see 'isBlue false' in the console log.
     
    // This format is used to send arguments to a deeper dependency.
    diContainer.resolve('otherDependency', {
      otherDependency: {isBlue: true}
    });
    // We will see 'isBlue true' in the console log.
    // If you are using complex dependency injection with multiple layers, it is recommended to always use this style.

    Now let's send an extra argument down two levels of dependencies. First we will define a third dependency that depends on the otherDependency.

    // File thirdDependency.js
    module.exports = function thirdDependencyFactory(otherDependency, isGreen) {
      console.log('isGreen', isGreen);
      return {
        doAnything: true
      };
    };
     
    module.exports.$inject = ['otherDependency', '(isGreen)'];
    module.exports.$filename = __filename;

    Now let's register it with the diContainer and resolve it.

    // File di.js
    const diContainer = require('functional-di')();
     
    diContainer.register('otherDependency', otherDependencyFactory);
    diContainer.register('thirdDependency', thirdDependencyFactory);
     
    diContainer.resolve('otherDependency', {
      otherDependency: {isBlue: true},
      thirdDependency: {isGreen: false}
    });
    // We will see 'isBlue true' and 'isGreen false' in the console log.

    Shortcut for $inject

    To make things easier, you can simply send true to the $inject parameter and the arguments will be automatically detected.

    module.exports = function shortcutFactory(dependency1, dependency2) {
      return {
        doSomething: dependency1.doSomething,
        doSomethingElse: dependency2.doSomething
      };
    };
     
    module.exports.$inject = true;
    module.exports.$filename = __filename;

    If any of the arguments are to be injected during the resolve process, you must define $resolveArgs on the factory with the name of the resolve arguments. Otherwise, the argument will attempt to be injected. Notice how you do not need the parentheses inside the $resolveArgs array.

    module.exports = function shortcutFactory(dependency1, dependency2, isGreen) {
      console.log('isGreen', isGreen);
      return {
        doSomething: dependency1.doSomething,
        doSomethingElse: dependency2.doSomething
      };
    };
     
    module.exports.$inject = true;
    module.exports.$resolveArgs = ['isGreen'];
    module.exports.$filename = __filename;

    IMPORTANT: If any of the arguments for the function are optional, you must given them a default value or an error will be thrown when they fail to be injected.

    module.exports = function shortcutFactory(dependency1, dependency2, optionalDependency = null) {
      if (optionalDependency) {
        console.log('Dependency was found.');
      }
      return {
        doSomething: dependency1.doSomething,
        doSomethingElse: dependency2.doSomething
      };
    };
     
    module.exports.$inject = true;
    module.exports.$filename = __filename;

    Resolve arg that are optional will be defined in the $resolveArgs array using '?' (see the 'Optional Arguments' section below).

    module.exports = function shortcutFactory(dependency1, dependency2, isGreen) {
      console.log('isGreen', isGreen);
      return {
        doSomething: dependency1.doSomething,
        doSomethingElse: dependency2.doSomething
      };
    };
     
    module.exports.$inject = true;
    module.exports.$resolveArgs = ['isGreen?'];
    module.exports.$filename = __filename;

    Optional Arguments

    It is possible to define that injected arguments and/or resolve arguments are optional. If these items are not found, then undefined will be provided as their value. (Since undefined is used, any optional values defined in the function params will still be populated).

    Let's create a factory with an optional injected dependency.

    module.exports = function exampleFactory(dependency1, dependency2 = null) {
      return {
        doSomething: dependency1.doSomething,
        doSomethingElse: (dependency2) ? dependency2.doSomething : null
      };
    };
     
    module.exports.$filename = __filename;
     
    // The `= null` in the argument definition is required to make dependency2 optional.
    module.exports.$inject = true;
    // --or--
    // Use ? after the dependency name to indicate it is optional (The `= null` will not do this by itself!).
    module.exports.$inject = ['dependency1', 'dependency2?'];

    Now let's add an optional resolve argument. (Side Note: Notice now the optional injected parameters don't have to be after the required ones).

    module.exports = function exampleFactory(dependency1 = null, dependency2, optionalResolved) {
      console.log('optional resolved', optionalResolved);
      return {
        doSomething: (dependency1) ? dependency1.doSomething : null,
        doSomethingElse: dependency2.doSomething(optionalResolved || 'default')
      };
    };
     
    module.exports.$filename = __filename;
     
    // The ? in the $resolveArgs definition is required to make the resolved argument optional.
    module.exports.$inject = true;
    module.exports.$resolveArgs = ['optionalResolved?'];
    // --or--
    // Use `(name)?` when defining the resolved argument name to make it optional.
    module.exports.$inject = ['dependency1?', 'dependency2', '(optionalResolved)?'];

    File Tracking

    In order to improve debugging and error checking flows, the diContainer can track where the injected files come from as well as where they were injected into the system. By default, the system will throw an error or warning if this information is missing.

    function testFactory() {
      return {
        some: 'value'
      };
    }
     
    // Tells the DiContainer the file where the testFactory exists.
    testFactory.$filename = __filename;
     
    // Tells the DiContainer the file where testFactory was registered.
    const previousSource = diContainer.setRegisterSource(__filename);
    diContainer.register('test', testFactory);

    Turning Off Source Checks

    Use the skipTraceErrors diContainer option to turn off the error when no register source is defined and the console log warning when no $filename is defined.

    const diContainerFactory = require('@clevyr/functional-di');
     
    const diContainer = diContainerFactory(null, null, {skipTraceErrors: true});
     
    diContainer.register('something', true); // Will not throw errors or warnings.

    Register Source

    When you define the registerSource (diContainer.setRegisterSource();) the system will remember this source and apply it to all following registrations. This value is not carried over if the diContainer is cloned.

    The setRegisterSource function returns the previous source, which allows you to temporarily change the source and then change it back.

    const previousSource = diContainer.setRegisterSource(__filename);
    diContainer.register('test', testFactory);
    diContainer.setRegisterSource(previousSource);

    Alternatively, each diContainer.register() call can be given the registerSourceFile option to specify this value for just the current injected factory.

    diContainer.register('test', testFactory, false, {
      registerSourceFile: __filename
    });

    If no register source is defined, the diContainer will throw an error. This can be bypassed using the skipTraceErrors diContainer option.

    Register File Path

    The path to the source file of a registered factory can be defined using the $filename property. In NodeJs, you can use the __filename variable to easily set this value. If this value is not set, a warning will be console logged unless the skipTraceErrors diContainer option is truthy.

    Alternatively, each diContainer.register() call can be given the $filename option to override/specify this value for just the current injected factory.

    diContainer.register('test', testFactory, false, {
      filename: '/path/to/testFactory/file'
    });

    When defining a static value instead of a factory, you can use the override option to prevent the error and have proper error tracking.

    diContainer.register('test', 'staticValue', true, {
      filename: '/path/to/where/static/value/is/defined'
    });

    Stack Trace

    When an anticipated diContainer error occurs, the stack trace will be overridden with the injection/resolution path instead of the code path. In most cases, the code path was not helpful (just a lodash loop).

    An example of the stack trace (with notes after the //s).

    DiContainer: The item asdf has not been defined in the DI Container.
        at asdf (?) // The item that caused the error.
            registered in [?]
        at errorHelper (/code/src/utils/errorHelper.js) // The item that requested the item.
            registered in [/code/src/index.js] // Where this item was registered (diContainer.register()).
        at validateTokenService (/code/src/modules/csrfToken/services/validateTokenService.js)
            registered in [/code/src/modules/csrfToken/index.js]
        at csrfTokenGetRoute (/code/src/modules/csrfToken/routes/csrfTokenGetRoute.js)
            registered in [/code/src/modules/csrfToken/index.js]
        at moduleRoutes (/code/src/modules/csrfToken/routes.js)
            registered in [/code/src/modules/csrfToken/index.js]
        at moduleAsPlugin (/code/src/modules/csrfToken/plugin.js) // The item that was first resolved (diContainer.resolve()).
            registered in [/code/src/modules/csrfToken/index.js]
    

    Singletons/Caching

    The results of factories can be cached/singleton in two ways.

    1. Set $singleton on the factory.
    function testFactory() {
      return {
        some: 'value'
      };
    }
     
    testFactory.$singleton = true;
    testFactory.$filename = __filename;
     
    diContainer.register('test', testFactory);
     
    console.log(diContainer.resolve('test') === diContainer.resolve('test')); // Logs true.
    1. Send true as the third argument when registering the factory. This will override anything set using $singleton.
    function testFactory() {
      return {
        some: 'value'
      };
    };
     
    testFactory.$singleton = false;
    testFactory.$filename = __filename;
     
    // Here we override the $singleton property by sending true for the 3rd arg.
    diContainer.register('test', testFactory, true);
     
    console.log(diContainer.resolve('test') === diContainer.resolve('test')); // Logs true.

    Clearing Singletons

    Singletons/cache can be cleared (useful for testing) using the clearSingletons() method.

    const diContainer = require('functional-di')();
     
    diContainer.register('someDependency', someDependencyFactory, true);
    const first = diContainer.resolve('someDependency');
     
    diContainer.clearSingletons();
    const second = diContainer.resolve('someDependency');
     
    console.log(first === second); // Logs false.

    Aliases

    The di container allows aliases to map a name to another registered dependency. This alias will take precedence over an item registered to the name. Aliases will be carried over with diContainer.clone() unless the skipAliases argument is true.

    const diContainer = require('functional-di')();
     
    diContainer.register('someDependency', someDependencyFactory);
    diContainer.alias('newDependency', 'someDependency');
    const newDependency = diContainer.resolve('newDependency');
     
    // Aliases can be unset by setting them to false (or falsey).
    diContainer.alias('newDependency', false);
    diContainer.resolve('newDependency'); // Throws undefined error.

    Cloning the Container

    The di container can be shallow cloned. The new container will still contain references to everything registered with the original container, but all singletons will be cleared (by default). The options provided to the original di container will also carry through to the clone.

    const diContainer = require('functional-di')(null, null, {skipTraceErrors: true});
     
    diContainer.register('someDependency', someDependencyFactory);
     
    const newDiContainer = diContainer.clone();
     
    newDiContainer.resolve('someDependency'); // This works and returns the item.
     
    diContainer.register('otherDependency', otherDependencyFactory);
     
    newDiContainer.resolve('otherDependency'); // Throws an error.

    The singletons can be kept by sending true for the keepSingletons argument.

    const diContainer = require('functional-di')();
     
    diContainer.register('someSingleton', someDependencyFactory);
    diContainer.resolve('someSingleton'); // The singleton is defined on first resolve.
     
    const newDiContainer = diContainer.clone(true);
     
    newDiContainer.resolve('someSingleton'); // This works and returns the singleton item.

    The aliases can be skipped by sending true for the skipAliases argument.

    const diContainer = require('functional-di')();
     
    diContainer.register('someSingleton', someDependencyFactory);
    diContainer.alias('newSingleton', 'someSingleton');
     
    const newDiContainer = diContainer.clone(false, true);
     
    newDiContainer.resolve('newSingleton'); // This throws a not found error.

    Self-Injecting

    Although being an anti-pattern, the DI container can self-inject itself as a dependency if required. To do so, just inject 'diContainer'. This instance of diContainer is always a singleton (cached).

    module.exports = function selfInjectExample(diContainer) {
      const otherDependency = diContainer.resolve('otherDependency');
    };
     
    module.exports.$inject = ['diContainer'];
    module.exports.$filename = __filename;

    Curried Instance

    You can request a curried instance of an injected object using diContainer.resolveCurry(). This function will return the object instance and accepts the object of resolve arguments to send to the function and its sub dependencies.

    function exampleFactory(optionalArg) {
      return optionalArg || false;
    }
    exampleFactory.$inject = ['(optionalArg)?'];
    exampleFactory.$filename = __filename;
     
    const diContainer = require('functional-di')();
     
    diContainer.register('example', exampleFactory);
    const curriedExample = diContainer.resolveCurry('example');
     
    console.log(curriedExample()); // Logs `false`.
    console.log(curriedExample({optionalArg: 'argValue'})); // Logs 'argValue'.

    Resolve As Factory

    This is similar to a curried instance, except that you will receive the injected factory back that must take in the resolve arguments directly.

    function exampleFactory(dependency1, requiredArg, optionalArg) {
      return {
        required: requiredArg,
        optional: optionalArg
      };
    }
    exampleFactory.$inject = ['dependency1', '(requiredArg)', '(optionalArg)?'];
    exampleFactory.$filename = __filename;
     
    const diContainer = require('functional-di')();
     
    diContainer.register('dependency1', {});
    diContainer.register('example', exampleFactory);
    const factory = diContainer.resolveFactory('example');
     
    console.log(factory()); // Logs `{required: undefined, optional: undefined}`.
    console.log(factory('argValue', 'otherValue')); // Logs `{required: 'argValue', optional: otherValue}`.

    Tests

    The tests use the ava framework.

    npm test
    

    Linters

    Lint config files included at root of project:

    • .eslintrc, .eslintignore
    • .jscsrc
    // ESLint
    npm install -g eslint
    
    eslint src
     - or -
    npm run lint
    
    // JSCS
    npm install -g jscs
    jscs src
    

    Keywords

    Install

    npm i @clevyr/functional-di

    DownloadsWeekly Downloads

    48

    Version

    0.3.3

    License

    ISC

    Unpacked Size

    195 kB

    Total Files

    14

    Last publish

    Collaborators

    • clevyr