0.5.1 • Published 8 years ago

bolus v0.5.1

Weekly downloads
39
License
MIT
Repository
github
Last release
8 years ago

view on npm npm module downloads per month Build Status

bolus

Simple dependency injection module for Node.js with inspiration from AngularJS.

Installation

$ npm install bolus

Examples

Examples can be found in a separate GitHub repository: bolus-examples.

Usage

The basic idea behind bolus is to register factory functions with an injector and then resolve them later. Each factory function takes its dependencies as arguments and then returns any value. Here is a basic example:

// index.js
// Import the module.
var Injector = require("bolus");

// Create a new injector.
var injector = new Injector();

// Register a module named 'a' that returns the value 5 when resolved.
injector.register("a", function () {
    return 5;
});

// Now register a module named 'b' that depends on 'a'.
injector.register("b", function (a) {
    return a + 7;
});

// Now resolve 'b'.
var b = injector.resolve("b");
console.log(b); // prints 12

A more typical use case is when the modules 'a' and 'b' are in separate files. Let's move the factory functions into their own files:

// a.js
module.exports = function () {
    return 5;
};
// b.js
module.exports = function (a) {
    return a + 7;
};

Now our main code can be written simply as:

// index.js
// Import the module.
var Injector = require("bolus");

// Create a new injector.
var injector = new Injector();

// Register all JS files as modules.
injector.registerPath("**/*.js");

// Now resolve 'b'.
var b = injector.resolve("b");
console.log(b); // prints 12

By default, the registerPath method uses the basename of the files (in this case 'a' and 'b') as the registered name. (This can be overridden by specifying a nameMakerCallback.) As you can see, bolus tries to be DRY by using the file name as the registered module name and the named factory arguments as the dependency names. If you wish, you can also specify these explicitly:

module.exports = function (foo, bar) {
    return foo + bar;
};

module.exports.$name = "some name";
module.exports.$inject = ["some dependency", "some other dependency"];

External Dependencies

When using bolus, you can (and should) register any external dependencies (including core Node.js modules) with the injector as well. This ensures consistency and eases testing. You do this by passing an object to the registerRequires method.

var Injector = require("bolus");
var injector = new Injector();

injector.registerRequires({
    fs: "fs",
    Sequelize: "sequelize"
});

The keys are names that will be registered with the injector, the values are the names of the modules that will be passed to Node's require. These can then be injected into a module in the usual way.

module.exports = function (a, fs, Sequelize) {
    ...
};

Unit Testing

One of the best features of dependency injection is the simplicity of testing. Rather than load the entire application, you can load just the code of interest in isolation and test it with controlled inputs. Here's how a Jasmine test of the 'a' module could look:

// a.spec.js
var Injector = require("bolus");

describe("a", function () {
    it("should return 5", function () {
        var injector = new Injector();
        injector.registerPath("app/a.js");
        var a = injector.resolve("a");
        expect(a).toBe(5);
    });
});

The idea is that you create a new, clean injector for every test, inject your module of interest and any dependencies, and then run your test.

If you use Jasmine or Mocha, bolus will handle the lifecycle of the injector for you. Simply call the Injector.loadTestGlobals method to place add all of the injector methods on the global scope. Each of these methods apply only to the injector for the current test. So the test for 'a' would become:

// a.spec.js
require("bolus").loadTestGlobals();

describe("a", function () {
    it("should return 5", function () {
        registerPath("app/a.js");
        var a = resolve("a");
        expect(a).toBe(5);
    });
});

If you have multiple tests with the same setup, you might prefer to use a Jasmine beforeEach:

// a.spec.js
require("bolus").loadTestGlobals();

describe("a", function () {
    var a;
    beforeEach(function () {
        registerPath("app/a.js");
        a = resolve("a");
    });
    
    it("should return 5", function () {
        expect(a).toBe(5);
    });
});

You can also easily provide mocks for dependencies to simplify your testing. Here's what a Jasmine test for 'b' could look like:

// b.spec.js
require("bolus").loadTestGlobals();

describe("b", function () {
    beforeEach(registerPath("app/b.js"));

    it("should return 10 when a is 3", function () {
        registerValue("a", 3);
        var b = resolve("b");
        expect(b).toBe(10);
    });

    it("should return 1 when a is -6", function () {
        registerValue("a", -6);
        var b = resolve("b");
        expect(b).toBe(1);
    });
});

Here, we have provided two different values for the dependency 'a'. Also, note the alternative form of the beforeEach injector usage.

Advanced Usage

The above usage should be enough for most use cases. However, there are some more advanced features available if needed.

Container Inspection

You can check if a dependency has been resolved with a given name using the isRegistered method.

injector.registerValue("foo", {});
injector.isRegistered("foo"); // true

You can also get the names of all registered names with getRegisteredNames.

injector.registerValue("foo", {});
injector.registerValue("bar", {});
injector.getRegisteredNames(); // ['$injector', 'foo', 'bar']

Using Classes

If you are using v4 or higher of Node.js, you can also create class module. This works similar to the functions but the dependencies are passed to the class constructor.

class SomeClass {
    constructor(a) {
        this._a = a;
    }

    someMethod() {
        console.log(this._a);
    }
}

module.exports = SomeClass;

Advanced Resolving

In addition to resolving a single module with the resolve method, you can also resolve more that one at a time.

var modules = injector.resolve(["a", "b"]);

Here, modules is an array whose values are the resolved modules corresponding to the given names. Using ES6 destructuring, this can be written

const [a, b] = injector.resolve(["a", "b"]);

Additionally, you can pass a function to resolve that will be invoked with the arguments resolved:

var result = injector.resolve(function (a, b) {
    return a - b;
});

Notice that the return value of the function is passed through. This function usage, along with underscore notation, is very helpful in unit tests:

describe("some test", function () {
    var something;
    
    // An underscore prefix and suffix is used to not hide the 'something' variable.
    // The injector will ignore the underscores.
    beforeEach(resolve(function (_something_) {
        something = _something_;
    });
    
    it("should do something", function () {
        // run tests on 'something' 
    });
});

You can resolve a function like this in a file in one call:

var result = injector.resolvePath("path/to/some/file.js");

When resolving with a function, you can also pass along a 'locals' object that includes variables to provide or override dependencies.

injector.resolvePath("path/to/some/file.js", {
    someKey: "someValue"
});

(This is used in the Express example here to pass along a different router for each route file.)

Optional Depdencies

Bolus also supports optional dependencies. You can use this by adding an 'optional' comment in front of a variable argument or appending a question mark to a name:

var a = resolve("a?"); // a === undefined

var fn = function (a) {
    // a === undefined
};
fn.$inject = ["a?"];

var fn = function (/* optional */ a) {
    // a === undefined
};

Accessing the Injector Within a Module

The injector itself is registered in the injector as '$injector' to allow for some more advanced usages. (See here and here in the Express example for a couple scenarios.) It can be injected like any other module:

module.exports = function ($injector) {
    ...
};

Warning: currently there is no check for circular dependencies when using resolve methods within a factory initialization. Be careful!

Development

Running Tests

Tests are run automatically on Travis CI. They can (and should) be triggered locally with:

$ npm test

Code Linting

JSHint and JSCS are used to ensure code quality. To run these, run:

$ npm run jshint
$ npm run jscs

Generating Documentation

The API reference documentation below is generated by jsdoc-to-markdown. To generate an updated README.md, run:

$ npm run docs

API Reference

Injector

Kind: global class

new Injector()

Initializes a new Injector.

Example

var injector = new Injector();

injector.register(name, fn)

Register a module.

Kind: instance method of Injector

ParamTypeDescription
namestringName of the module.
fnfunctionA module function to register.

Example

injector.register("foo", function (dependencyA, dependencyB) {
    // Do something with dependencyA and dependencyB to initialize foo.
    // Return any object.
});

injector.registerValue(name, value)

Register a fixed value.

Kind: instance method of Injector

ParamTypeDescription
namestringThe name of the module.
value*The value to register.

Example

// Register the value 5 with the name "foo".
injector.registerValue("foo", 5);

Example

// Register a function with the name "doubler".
injector.registerValue("doubler", function (arg) {
    return arg * 2;
});

injector.registerPath(patterns, nameMaker, mod)

Register module(s) with the given path pattern(s).

Kind: instance method of Injector

ParamTypeDescription
patternsstring | Array.<string>The pattern or patterns to match. This uses the glob-all module, which accepts negative patterns as well.
nameMakernameMakerCallbackA function that creates a name for a module registered by path.
modModuleThe module to run require on. Defaults to the Injector module, which should typically behave correctly. Setting this to the current module is useful if you are using tools like gulp-jasmine which clear the local require cache.

Example

// Register a single file.
injector.registerPath("path/to/module.js");

Example

// Register all JS files except spec files.
injector.registerPath(["**/*.js", "!**/*.spec.js"]);

Example

injector.registerPath("path/to/module.js", function (defaultName, realpath, fn) {
    return defaultName.toUpperCase();
});

injector.registerRequires(reqs, mod)

Requires modules and registers them with the name provided.

Kind: instance method of Injector

ParamTypeDescription
reqsObject.<string, string>Object with keys as injector names and values as module names to require.
modModuleThe module to run require on. Defaults to the Injector module, which should typically behave correctly.

Example

injector.registerRequires({
    fs: "fs",
    Sequelize: "sequelize"
});

injector.resolve(names, context) ⇒ * | Array.<*>

Resolve a module or multiple modules.

Kind: instance method of Injector
Returns: * | Array.<*> - The resolved value(s).

ParamTypeDescription
namesstring | Array.<string>Name or names to resolve.
contextstringOptional context to give for error messages.

Example

var log = injector.resolve("log");

Example

var resolved = injector.resolve(["fs", "log"]);
var fs = resolved[0];
var log = resolved[1];

injector.resolve(fn, locals, context) ⇒ *

Resolve a module or multiple modules.

Kind: instance method of Injector
Returns: * - The result of the executed function.

ParamTypeDescription
fnfunctionFunction to execute.
localsObject.<string, *>Local variables to inject into the function.
contextstringOptional context to give for error messages.

Example

// Resolve someNum and otherNum and set the result to the sum.
var result;
injector.resolve(function (someNum, otherNum) {
    result = someNum + otherNum;
});

Example

// This is essentially the same thing using a return in the function.
var result = injector.resolve(function (someNum, otherNum) {
    return someNum + otherNum;
});

Example

// You can also provide or override dependencies using the locals argument.
var result = injector.resolve(function (someNum, otherNum) {
    return someNum + otherNum;
}, { otherNum: 5 });

injector.resolvePath(p, locals, context) ⇒ *

Resolve a module with the given path.

Kind: instance method of Injector
Returns: * - The result of the executed function.

ParamTypeDescription
pstringThe path to resolve.
localsObject.<string, *>Local variables to inject into the function.
contextstringOptional context to give for error messages. If omitted, path will be used.

Example

var log = injector.resolvePath("path/to/log.js");

injector.isRegistered(name) ⇒ boolean

Checks whether a given name has been registered in the injector.

Kind: instance method of Injector
Returns: boolean - A flag indicating whether the name is registered.

ParamDescription
nameThe name to check.

injector.getRegisteredNames() ⇒ Array.<string>

Get an array of all names registered in the injector.

Kind: instance method of Injector
Returns: Array.<string> - An array of all registered names.

Injector.loadTestGlobals(before, after)

Load convenience methods on the global scope for testing. Will expose all of the standard injector methods on the global scope with the same name. Before each test an injector will be created and after each it will be thrown away. The global methods will execute on the injector in that scope.

Kind: static method of Injector

ParamTypeDescription
beforefunctionFunction to run before test case to create the injector. Defaults to global.beforeEach or global.setup to match Jasmine or Mocha.
afterfunctionFunction to run before test case to create the injector. Defaults to global.afterEach or global.teardown to match Jasmine or Mocha.

Injector~nameMakerCallback ⇒ string

A function that creates a name for a module registered by path.

Kind: inner typedef of Injector
Returns: string - The name to use (or falsy to use default).

ParamTypeDescription
defaultNamestringThe default name to use. This is equal to the value of the function's $name property or the basename of the file.
realpathstringThe full path of the loaded module.
fnfunctionThe actual module factory function.
0.5.1

8 years ago

0.5.0

8 years ago

0.4.0

8 years ago

0.3.0

8 years ago

0.2.4

8 years ago

0.2.3

8 years ago

0.2.2

8 years ago

0.2.1

8 years ago

0.2.0

8 years ago

0.1.3

8 years ago

0.1.2

8 years ago

0.1.1

8 years ago

0.1.0

8 years ago