diff --git a/README.md b/README.md index 761ebc1..1032d19 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Pluto.js -======== +# Pluto.js _"JavaScript dependency injection that's so small, it almost doesn't count."_ @@ -8,128 +7,127 @@ _"JavaScript dependency injection that's so small, it almost doesn't count."_ | Master | [![Build Status](https://travis-ci.org/ecowden/pluto.js.png?branch=master)](https://travis-ci.org/ecowden/pluto.js) [![Coverage Status](https://coveralls.io/repos/github/ecowden/pluto.js/badge.svg?branch=master)](https://coveralls.io/github/ecowden/pluto.js?branch=master) | | All | [![Build Status](https://travis-ci.org/ecowden/pluto.js.png)](https://travis-ci.org/ecowden/pluto.js) | -What is Pluto? --------------- +## What is Pluto? + Pluto is a JavaScript dependency injection tool. Dependency injection is a spiffy way to assemble your applications. It decouples the various bits and makes your app testable. An introduction to dependency injection principles is currently beyond the scope of this guide. -Installing Pluto ----------------- +## Installing Pluto + Pluto is designed to be used with [Node](http://nodejs.org/) and [NPM](http://npmjs.org/). From the root of a Node -project, execute +project, execute: ``` -npm install pluto +$ npm install pluto --save ``` -Alternately, add a line to the `dependencies` section of your `package.json` and then run `npm install` in your -project directory. - -``` -{ - "name": "my-awesome-application", - "dependencies": { - "pluto": "0.4.0" - } -} -``` +## How to Pluto? -_Note: I'll try to keep the above version up to date, but you may want to check the -[Pluto NPM Page](https://npmjs.org/package/pluto) for the most recent version._ +A binder is the basic unit of Pluto's dependency injection. It maps names to objects you want. -How to Pluto? -------------- -A module is the basic unit of Pluto's dependency injection. It maps names to objects you want. +Pluto's injection is done in a few steps: -Pluto's injection is done in two steps. First, create a module. When you do this, you bind names to any combination of objects, factory functions and constructor functions. Second, call module.get(...) and pass a name. Pluto will give you the thing mapped to that name. Along the way, it will inject parameters that match other names bound in the module. +1. Create bindings. When you do this, you bind names to any combination of objects, factory functions and constructor functions. +2. Optionally, call `.get(...)`. Pluto will give you the thing mapped to that name. Along the way, it will inject parameters that match other names bound in the binder and resolve Promises as appropriate. +3. Alternately, call `.bootstrap()` to run all your factory functions and constructors, and resolve all promises. This is handy if you're trying to start up an application with a bunch of moving parts, and more common than using `.get(...)` for each part individually. There are three things you can bind to a name: an object instance, a constructor function and a factory function. +### Promises + +If you pass Pluto a promise, it will resolve it. If your factory or constructor function returns a promise, Pluto will resolve it before injecting the result into other components. + +### Instance Binding + The simplest binding is to bind a name to an instance: ``` js -var anInstance = {}; // can be any JavaScript object -var module = pluto.createModule(function (bind) { - bind("myInstance").toInstance(anInstance); -}); - -expect(module.get("myInstance")).toBe(anInstance); +const anInstance = {} // can be any JavaScript object, or a Promise +const bind = pluto() +bind('myInstance').toInstance(anInstance) + +// bind.get(...) gives us a Promise that resolves to our instance +bind.get('myInstance').then((myInstance) => { + t.is(myInstance, anInstance) +}) ``` -You can also bind to a constructor function (i.e., a function that is meant to be used with the "new" keyword to create a new object). When you call module.get(...), Pluto will invoke the Constructor using "new" and return the result. If the constructor has any parameters, Pluto will consult its bindings and pass them into the constructor: +### Constructor Binding -``` js -var aGreeting = "Hello, world!"; -var Greeter = function (greeting) { - this.greeting = greeting; -}; +You can also bind to a constructor function (i.e., a function that is meant to be used with the `new` keyword to create a new object). When you call `.get(...)`, Pluto will invoke the Constructor using `new` and return the result. If the constructor has any parameters, Pluto will consult its bindings and pass them into the constructor: -Greeter.prototype.greet = function () { - return this.greeting; -}; +```js +function Greeter(greeting, name) { + this.greeting = greeting + this.name = name +} -var module = pluto.createModule(function (bind) { - bind("greeting").toInstance(aGreeting); - bind("greeter").toConstructor(Greeter); -}); +Greeter.prototype.greet = function () { + return `${this.greeting}, ${this.name}!` +} -var theGreeter = module.get("greeter"); +const bind = pluto() +bind('greeting').toInstance('Hello') +bind('name').toInstance(Promise.resolve('World')) // A promise will work, too +bind('greeter').toConstructor(Greeter) -expect(theGreeter.greet()).toBe("Hello, world!"); +bind.get('greeter').then((myGreeter) => { + t.is(myGreeter.greet(), 'Hello, World!') +}) ``` -Similarly, you can bind to a factory function -- that is, a function that creates some other object. When you call module.get(...), Pluto will invoke the function and return the result. Just like with a constructor, if the factory function has any parameters, Pluto will consult its bindings and pass them into the factory: +### Factory Function Binding -``` js -var aGreeting = "Hello, world!"; -var greeterFactory = function (greeting) { - return function () { - return greeting; - }; -}; +Similarly, you can bind to a factory function -- that is, a function that creates some other object. When you call `.get(...)`, Pluto will invoke the function and return the result. Just like with a constructor, if the factory function has any parameters, Pluto will consult its bindings and pass them into the factory: -var module = pluto.createModule(function (bind) { - bind("greeting").toInstance(aGreeting); - bind("greeter").toFactory(greeterFactory); -}); +```js +function greeterFactory(greeting, name) { + return function greet() { + return `${greeting}, ${name}!` + } +} -var theGreeter = module.get("greeter"); +const bind = pluto() +bind('greeting').toInstance('Hello') +bind('name').toInstance(Promise.resolve('World')) // A promise will work, too +bind('greet').toFactory(greeterFactory) -expect(theGreeter()).toBe("Hello, world!"); +bind.get('greet').then((greet) => { + t.is(greet(), 'Hello, World!') +}) ``` -Injected objects are singletons -------------------------------- +**Author's note**: _Factory functions a super useful. I find that I use them more than any other type of binding._ -Note that a factory function or constructor function is only called once. Each call to `get(...)` will return the -same instance. +### Eager Bootstrapping -Remember that singletons are only singletons within a single module, though. Different module instances -- for instance, -created for separate test methods -- will each have their own singleton instance. +By default, Pluto will only create your objects lazily. That is, factory and constructor functions will only get called when you ask for them with `.get(...)`. -Lazy vs. Eager Loading ----------------------- +You may instead want them to be eagerly invoked to bootstrap your project. For instance, you may have factory functions which set up Express routes or which perform other application setup. -By default, Pluto will only create your objects lazily. That is, factory and constructor functions will only get called -when you ask for them with `module.get(...)`. +Invoke `.bootstrap()` after creating your bindings to eagerly bootstrap your application. The result is a promise which resolves to a `Map` holding all bindings by name, fully resolved and injected. -You may instead want them to be eagerly invoked to bootstrap your project. For instance, you may have factory functions -which set up Express routes or which perform other application setup. +```js +function greeterFactory(greeting, name) { + return function greet() { + return `${greeting}, ${name}!` + } +} -Invoke `module.eagerlyLoadAll()` after creating your module to eagerly bootstrap your application. +const bind = pluto() +bind('greeting').toInstance('Hello') +bind('name').toInstance(Promise.resolve('World')) // A promise will work, too +bind('greet').toFactory(greeterFactory) +bind.bootstrap().then(app => { + const greet = app.get('greet') // Note: it's synchronous. Everything is ready. + t.is(greet(), 'Hello, World!') +}) ``` -var Constructor = jasmine.createSpy('test Constructor function'); -var factory = jasmine.createSpy('test factory function'); -var instance = pluto.createModule(function (bind) { - bind('Constructor').toConstructor(Constructor); - bind('factory').toFactory(factory); -}); +### Injected Objects are Singletons -instance.eagerlyLoadAll(); +Note that a factory function or constructor function is only called once. Each call to `get(...)` will return the same instance. -expect(Constructor).toHaveBeenCalled(); -expect(factory).toHaveBeenCalled(); -``` +Remember that singletons are only singletons within a single binder, though. Different binders -- for instance, created for separate test methods -- will each have their own singleton instance. diff --git a/lib/examplesSpec.js b/lib/examplesSpec.js index dacb560..0ff117a 100644 --- a/lib/examplesSpec.js +++ b/lib/examplesSpec.js @@ -6,45 +6,66 @@ const pluto = require('./pluto') test('bind to instance', function* (t) { const anInstance = {} // can be any JavaScript object - const module = pluto.createModule(function (bind) { - bind('myInstance').toInstance(anInstance) - }) + const bind = pluto() + bind('myInstance').toInstance(anInstance) - t.is(module.get('myInstance'), anInstance) + // bind.get will return a Promise, since we may have asynchronous resolution to do + bind.get('myInstance').then((myInstance) => { + t.is(myInstance, anInstance) + }) }) test('bind to constructor', function* (t) { - function Greeter(greeting) { + function Greeter(greeting, name) { this.greeting = greeting + this.name = name } Greeter.prototype.greet = function () { - return this.greeting + return `${this.greeting}, ${this.name}!` } - const module = pluto.createModule(function (bind) { - bind('greeting').toInstance('Hello, world!') - bind('greeter').toConstructor(Greeter) - }) - - const theGreeter = module.get('greeter') + const bind = pluto() + bind('greeting').toInstance('Hello') + bind('name').toInstance(Promise.resolve('World')) // A promise will work, too + bind('greeter').toConstructor(Greeter) - t.is(theGreeter.greet(), 'Hello, world!') + bind.get('greeter').then((myGreeter) => { + t.is(myGreeter.greet(), 'Hello, World!') + }) }) test('bind to factory function', function* (t) { - function greeterFactory(greeting) { - return function () { - return greeting + function greeterFactory(greeting, name) { + return function greet() { + return `${greeting}, ${name}!` } } - const module = pluto.createModule(function (bind) { - bind('greeting').toInstance('Hello, world!') - bind('greeter').toFactory(greeterFactory) + const bind = pluto() + bind('greeting').toInstance('Hello') + bind('name').toInstance(Promise.resolve('World')) // A promise will work, too + bind('greet').toFactory(greeterFactory) + + bind.get('greet').then((greet) => { + t.is(greet(), 'Hello, World!') }) +}) - const theGreeter = module.get('greeter') +test('bootstrapping', function* (t) { + function greeterFactory(greeting, name) { + return function greet() { + return `${greeting}, ${name}!` + } + } - t.is(theGreeter(), 'Hello, world!') + const bind = pluto() + bind('greeting').toInstance('Hello') + bind('name').toInstance(Promise.resolve('World')) + bind('greet').toFactory(greeterFactory) + + bind.bootstrap().then(app => { + const greet = app.get('greet') // Note: it's synchronous. Everything is ready. + t.is(greet(), 'Hello, World!') + }) }) diff --git a/lib/pluto.js b/lib/pluto.js index e2665ae..d3ce3aa 100644 --- a/lib/pluto.js +++ b/lib/pluto.js @@ -1,148 +1,133 @@ -const _ = require('underscore') -const MAX_CONSTRUCTOR_ARGUMENTS = 8 +'use strict' + +const co = require('co') +const memoize = require('lodash.memoize') + +function isPromise(obj) { + return obj && obj.then && typeof obj.then === 'function' +} + +function pluto() { + const namesToResolvers = new Map() -function createModule(createModuleCallback) { function createInstanceResolver(instance) { return function () { - return instance + return Promise.resolve(instance) } } function getArgumentNames(func) { const funStr = func.toString() - return funStr.slice(funStr.indexOf('(') + 1, funStr.indexOf(')')).match(/([^\s,]+)/g) + const argumentNames = funStr.slice(funStr.indexOf('(') + 1, funStr.indexOf(')')).match(/([^\s,]+)/g) + + // the above can return `null` when there are no argumentNames + return argumentNames || [] } function createFactoryResolver(factory) { - return function () { - const argumentNames = getArgumentNames(factory) - if (!argumentNames || argumentNames.length === 0) { - return factory() + return co.wrap(function* () { + if (isPromise(factory)) { + factory = yield factory } - const args = getAll(argumentNames) + const argumentNames = getArgumentNames(factory) + const args = yield getAll(argumentNames) return factory.apply(factory, args) - } + }) } function createConstructorResolver(Constructor) { - return function () { - /* - * It turns out that dynamically invoking constructor functions is a bit tricky. I have decided to - * manually invoke them for now until I can do further research and choose the best alternative. - * For now, constructor injection will be limited to eight arguments. - */ - const argumentNames = getArgumentNames(Constructor) - if (!argumentNames || argumentNames.length === 0) { - return new Constructor() - } - - const argumentCount = argumentNames.length - const args = getAll(argumentNames) - if (argumentCount === 1) { - return new Constructor(args[0]) - } - - if (argumentCount === 2) { - return new Constructor(args[0], args[1]) - } - - if (argumentCount === 3) { - return new Constructor(args[0], args[1], args[2]) - } - - if (argumentCount === 4) { - return new Constructor(args[0], args[1], args[2], args[3]) - } - - if (argumentCount === 5) { - return new Constructor(args[0], args[1], args[2], args[3], args[4]) - } - - if (argumentCount === 6) { - return new Constructor(args[0], args[1], args[2], args[3], args[4], args[5]) - } - - if (argumentCount === 7) { - return new Constructor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) + return co.wrap(function* () { + if (isPromise(Constructor)) { + Constructor = yield Constructor } + const argumentNames = getArgumentNames(Constructor) + const args = yield getAll(argumentNames) - if (argumentCount === 8) { - return new Constructor(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) - } + // For future reference, + // this can be done with the spread operator in Node versions >= v5. e.g., + // + // return new Constructor(...args) + // + // For now, this workaround is a good middle ground. - const msg = 'Pluto cannot inject constructor functions with ' + MAX_CONSTRUCTOR_ARGUMENTS + ' or more arguments ' + - "at this time (it's a long story). Please use a non-constructor factory function instead or consider injecting fewer dependencies." - throw Error(msg) - } + // eslint-disable-next-line + return new(Constructor.bind.apply(Constructor, [null].concat(args))) + }) } - const namesToResolvers = {} - - const get = _.memoize(function get(name) { - const resolver = namesToResolvers[name] - if (!resolver) { - throw Error("nothing is mapped for name '" + name + "'") - } - return resolver() + const get = memoize((name) => { + return new Promise((resolve, reject) => { + const resolver = namesToResolvers.get(name) + if (!resolver) { + reject(new Error(`nothing is mapped for name '${name}'`)) + } + resolve(resolver()) + }) }) function getAll(names) { - return names.map(function (name) { + const promises = names.map(function (name) { return get(name) }) + return Promise.all(promises) } - function eagerlyLoadAll() { - _.keys(namesToResolvers).forEach(function (name) { - get(name) + function bootstrap() { + const result = new Map() + const promises = [] + for (let name of namesToResolvers.keys()) { + promises.push(get(name).then((value) => { + result.set(name, value) + })) + } + return Promise.all(promises).then(() => { + return result }) } - createModuleCallback(function bind(name) { + function bind(name) { function validateBinding(target) { - if (_.has(namesToResolvers, name)) { - throw Error('module already contains a mapping with the name \'' + name + '\'') + if (namesToResolvers.has(name)) { + throw Error(`module already contains a mapping with the name '${name}'`) } - if (_.isUndefined(target)) { - throw Error('cannot bind \'' + name + '\' because the specified target is undefined.') + if (typeof target === 'undefined') { + throw Error(`cannot bind '${name}' because the specified target is undefined.`) } - if (_.isNull(target)) { - throw Error('cannot bind \'' + name + '\' because the specified target is null.') + if (target === null) { + throw Error(`cannot bind '${name}' because the specified target is null.`) } } - function validateTargetIsAFunction(factory) { - if (!_.isFunction(factory)) { - throw Error('cannot bind \'' + name + '\' because the specified target is not a function.') + function validateTargetIsAFunctionOrPromise(factory) { + if (typeof factory !== 'function' && !isPromise(factory)) { + throw Error(`cannot bind '${name}' because the specified target is not a function or Promise.`) } } return { toInstance: function (instance) { validateBinding(instance) - namesToResolvers[name] = createInstanceResolver(instance) + namesToResolvers.set(name, createInstanceResolver(instance)) }, toFactory: function (factory) { validateBinding(factory) - validateTargetIsAFunction(factory) - namesToResolvers[name] = createFactoryResolver(factory) + validateTargetIsAFunctionOrPromise(factory) + namesToResolvers.set(name, createFactoryResolver(factory)) }, toConstructor: function (constructor) { validateBinding(constructor) - validateTargetIsAFunction(constructor) - namesToResolvers[name] = createConstructorResolver(constructor) + validateTargetIsAFunctionOrPromise(constructor) + namesToResolvers.set(name, createConstructorResolver(constructor)) } } - }) - - return { - eagerlyLoadAll: eagerlyLoadAll, - get: get, - getAll: getAll } -} -exports = module.exports = { - createModule: createModule + bind.get = get + bind.getAll = getAll + bind.bootstrap = bootstrap + + return bind } + +exports = module.exports = pluto diff --git a/lib/plutoSpec.js b/lib/plutoSpec.js index 15ce3f9..6873d14 100644 --- a/lib/plutoSpec.js +++ b/lib/plutoSpec.js @@ -4,108 +4,143 @@ const test = require('ava') const pluto = require('./pluto') -test('pluto.createModule( ... ) invokes the callback with a "bind" function', function* (t) { - let actualBind - - pluto.createModule(function (bind) { - actualBind = bind - }) - - t.is(typeof actualBind, 'function') +test('pluto() returns a `bind` function', function* (t) { + const bind = pluto() + t.is(typeof bind, 'function') }) test('bind(name).toInstance(instance) throws if instance is null', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toInstance(null) - }, Error) - }) + const bind = pluto() + bind('$injected').toInstance(null) + }, Error) + t.is(error.message, "cannot bind '$injected' because the specified target is null.") }) test('bind(name).toInstance(instance) throws if instance parameter is undefined', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toInstance(undefined) - }, Error) - }) + const bind = pluto() + bind('$injected').toInstance(undefined) + }, Error) + t.is(error.message, "cannot bind '$injected' because the specified target is undefined.") }) test('bind(name).toInstance(instance) throws if the instance name is a duplicate', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toInstance({}) - bind('$injected').toInstance({}) - }, Error) - }) + const bind = pluto() + bind('$injected').toInstance({}) + bind('$injected').toInstance({}) + }, Error) + t.is(error.message, "module already contains a mapping with the name '$injected'") }) -test('after bind(name).toInstance(instance), module.get(name) returns the instance', function* (t) { +test('after bind(name).toInstance(instance), bind.get(name) resolves to the instance', function* (t) { const expected = {} - const module = pluto.createModule(function (bind) { - bind('$injected').toInstance(expected) - }) + const bind = pluto() + bind('$injected').toInstance(expected) - const actual = module.get('$injected') + const actual = yield bind.get('$injected') t.is(actual, expected) }) test('bind(name).toFactory(factory) throws if factory is null', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toFactory(null) - }, Error) - }) + const bind = pluto() + bind('$injected').toFactory(null) + }, Error) + t.is(error.message, "cannot bind '$injected' because the specified target is null.") }) test('bind(name).toFactory(factory) throws if factory parameter is undefined', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toFactory(undefined) - }, Error) - }) + const bind = pluto() + bind('$injected').toFactory(undefined) + }, Error) + t.is(error.message, "cannot bind '$injected' because the specified target is undefined.") }) test('bind(name).toFactory(instance) throws if the name is a duplicate', function* (t) { function factory() {} const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toFactory(factory) - bind('$injected').toFactory(factory) - }, Error) - }) + const bind = pluto() + bind('$injected').toFactory(factory) + bind('$injected').toFactory(factory) + }, Error) + t.is(error.message, "module already contains a mapping with the name '$injected'") }) -test('bind(name).toFactory(instance) throws if the factory is not a function', function* (t) { +test('bind(name).toFactory(instance) throws if the factory is not a function or Promise', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toFactory({}) - }, Error) - }) - t.is(error.message, "cannot bind '$injected' because the specified target is not a function.") + const bind = pluto() + bind('$injected').toFactory({}) + }, Error) + + t.is(error.message, "cannot bind '$injected' because the specified target is not a function or Promise.") }) -test('when the factory function has zero parameters, module.get(name) returns the result of the factory\'s invocation', function* (t) { +test('when the factory function has zero parameters, bind.get(name) returns the result of the factory\'s invocation', function* (t) { const expected = {} function factory() { return expected } - const module = pluto.createModule(function (bind) { - bind('$injected').toFactory(factory) - }) + const bind = pluto() + bind('$injected').toFactory(factory) + + const actual = yield bind.get('$injected') + t.is(actual, expected) +}) + +test('when the factory returns a Promise, resolves the promise', function* (t) { + const expected = {} + + function factory() { + return Promise.resolve(expected) + } + + const bind = pluto() + bind('$injected').toFactory(factory) + + const actual = yield bind.get('$injected') + t.is(actual, expected) +}) - const actual = module.get('$injected') +test('when the factory returns a Promise that rejects with an error, rejects with the thrown error', function* (t) { + const innerError = new Error('inner rejection') + + function factory() { + return Promise.reject(innerError) + } + + const bind = pluto() + bind('$injected').toFactory(factory) + + const error = yield t.throws(bind.get('$injected')) + t.is(error.message, 'inner rejection') +}) + +test('when the factory is a Promise, resolves the promise and then invokes the factory', function* (t) { + const expected = {} + + function factory() { + return Promise.resolve(expected) + } + + const bind = pluto() + bind('$injected').toFactory(Promise.resolve(factory)) + + const actual = yield bind.get('$injected') t.is(actual, expected) }) -test('module.get(name) injects the factory function\'s parameters, then returns the result from the factory\'s invocation', function* (t) { +test('bind.get(name) injects the factory function\'s parameters, then returns the result from the factory\'s invocation', function* (t) { const expectedParam = {} function factory($param) { @@ -114,12 +149,11 @@ test('module.get(name) injects the factory function\'s parameters, then returns } } - const module = pluto.createModule(function (bind) { - bind('$root').toFactory(factory) - bind('$param').toInstance(expectedParam) - }) + const bind = pluto() + bind('$root').toFactory(factory) + bind('$param').toInstance(expectedParam) - const actual = module.get('$root') + const actual = yield bind.get('$root') t.is(actual.param, expectedParam) }) @@ -131,31 +165,30 @@ test('memoizes invocation so that the factory function is only invoked once', fu return 'dummy' } - const module = pluto.createModule(function (bind) { - bind('$factory').toFactory(factory) - }) + const bind = pluto() + bind('$factory').toFactory(factory) - module.get('$factory') - module.get('$factory') + yield bind.get('$factory') + yield bind.get('$factory') t.is(invocationCount, 1) }) test('bind(name).toConstructor(constructor) throws if constructor is null', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toConstructor(null) - }, Error) - }) + const bind = pluto() + bind('$injected').toConstructor(null) + }, Error) + t.is(error.message, "cannot bind '$injected' because the specified target is null.") }) test('bind(name).toConstructor(constructor) throws if constructor is undefined', function* (t) { const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toConstructor(undefined) - }, Error) - }) + const bind = pluto() + bind('$injected').toConstructor(undefined) + }, Error) + t.is(error.message, "cannot bind '$injected' because the specified target is undefined.") }) @@ -163,316 +196,125 @@ test('bind(name).toConstructor(constructor) throws if the name is a duplicate', function Constructor() {} const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toConstructor(Constructor) - bind('$injected').toConstructor(Constructor) - }, Error) - }) + const bind = pluto() + bind('$injected').toConstructor(Constructor) + bind('$injected').toConstructor(Constructor) + }, Error) + t.is(error.message, "module already contains a mapping with the name '$injected'") }) -test('bind(name).toConstructor(instance) throws if the constructor is not a function', function* (t) { +test('bind(name).toConstructor(instance) throws if the constructor is not a function or Promise', function* (t) { const notAFunction = {} const error = t.throws(() => { - pluto.createModule(function (bind) { - bind('$injected').toConstructor(notAFunction) - }, Error) - }) - t.is(error.message, "cannot bind '$injected' because the specified target is not a function.") + const bind = pluto() + bind('$injected').toConstructor(notAFunction) + }, Error) + + t.is(error.message, "cannot bind '$injected' because the specified target is not a function or Promise.") }) -test('when a constructor has zero parameters, module.get(name) returns the new Constructor()', function* (t) { +test('when a constructor has zero parameters, bind.get(name) returns the new Constructor()', function* (t) { const Constructor = function () {} - const module = pluto.createModule(function (bind) { - bind('$injected').toConstructor(Constructor) - }) + const bind = pluto() + bind('$injected').toConstructor(Constructor) - const actual = module.get('$injected') + const actual = yield bind.get('$injected') t.truthy(actual instanceof Constructor) }) -test('when a constructor has one parameter, module.get(name) returns the new Constructor() with the parameter injected', function* (t) { - const Root = function ($param1) { - this.param1 = $param1 - } - - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - }) - - const actual = module.get('$Root') - - t.truthy(actual instanceof Root) - t.is(actual.param1, 'the first injected parameter') -}) - -test('when a constructor has two parameters, module.get(name) returns the new Constructor() with the parameters injected', function* (t) { - const Root = function ($param1, $param2) { - this.param1 = $param1 - this.param2 = $param2 - } - - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - }) - - const actual = module.get('$Root') - - t.truthy(actual instanceof Root) - t.is(actual.param1, 'the first injected parameter') - t.is(actual.param2, 'the second injected parameter') -}) - -test('when a constructor has three parameters, module.get(name) returns the new Constructor() with the parameters injected', function* (t) { - const Root = function ($param1, $param2, $param3) { - this.param1 = $param1 - this.param2 = $param2 - this.param3 = $param3 - } - - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - bind('$param3').toInstance('the third injected parameter') - }) - - const actual = module.get('$Root') - - t.truthy(actual instanceof Root) - t.is(actual.param1, 'the first injected parameter') - t.is(actual.param2, 'the second injected parameter') - t.is(actual.param3, 'the third injected parameter') -}) - -test('when a constructor has four parameters, module.get(name) returns the new Constructor() with the parameters injected', function* (t) { - const Root = function ($param1, $param2, $param3, $param4) { - this.param1 = $param1 - this.param2 = $param2 - this.param3 = $param3 - this.param4 = $param4 - } - - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - bind('$param3').toInstance('the third injected parameter') - bind('$param4').toInstance('the fourth injected parameter') - }) - - const actual = module.get('$Root') - - t.truthy(actual instanceof Root) - t.is(actual.param1, 'the first injected parameter') - t.is(actual.param2, 'the second injected parameter') - t.is(actual.param3, 'the third injected parameter') - t.is(actual.param4, 'the fourth injected parameter') -}) - -test('when a constructor has five parameters, module.get(name) returns the new Constructor() with the parameters injected', function* (t) { - const Root = function ($param1, $param2, $param3, $param4, $param5) { - this.param1 = $param1 - this.param2 = $param2 - this.param3 = $param3 - this.param4 = $param4 - this.param5 = $param5 - } - - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - bind('$param3').toInstance('the third injected parameter') - bind('$param4').toInstance('the fourth injected parameter') - bind('$param5').toInstance('the fifth injected parameter') - }) - - const actual = module.get('$Root') - - t.truthy(actual instanceof Root) - t.is(actual.param1, 'the first injected parameter') - t.is(actual.param2, 'the second injected parameter') - t.is(actual.param3, 'the third injected parameter') - t.is(actual.param4, 'the fourth injected parameter') - t.is(actual.param5, 'the fifth injected parameter') -}) - -test('when a constructor has six parameters, module.get(name) returns the new Constructor() with the parameters injected', function* (t) { - const Root = function ($param1, $param2, $param3, $param4, $param5, $param6) { - this.param1 = $param1 - this.param2 = $param2 - this.param3 = $param3 - this.param4 = $param4 - this.param5 = $param5 - this.param6 = $param6 +test('when a constructor returns a Promise, resolves the Promise', function* (t) { + // Note that this is "incorrect" usage of a Constructor function, but + // since it's possible, we should handle it gracefully. + const expected = {} + const Constructor = function () { + return Promise.resolve(expected) } - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - bind('$param3').toInstance('the third injected parameter') - bind('$param4').toInstance('the fourth injected parameter') - bind('$param5').toInstance('the fifth injected parameter') - bind('$param6').toInstance('the sixth injected parameter') - }) + const bind = pluto() + bind('$injected').toConstructor(Constructor) - const actual = module.get('$Root') + const actual = yield bind.get('$injected') - t.truthy(actual instanceof Root) - t.is(actual.param1, 'the first injected parameter') - t.is(actual.param2, 'the second injected parameter') - t.is(actual.param3, 'the third injected parameter') - t.is(actual.param4, 'the fourth injected parameter') - t.is(actual.param5, 'the fifth injected parameter') - t.is(actual.param6, 'the sixth injected parameter') + t.is(actual, expected) }) -test('when a constructor has seven parameters, module.get(name) returns the new Constructor() with the parameters injected', function* (t) { - const Root = function ($param1, $param2, $param3, $param4, $param5, $param6, $param7) { - this.param1 = $param1 - this.param2 = $param2 - this.param3 = $param3 - this.param4 = $param4 - this.param5 = $param5 - this.param6 = $param6 - this.param7 = $param7 - } +test('when a constructor is a Promise, resolves the Promise and then the Constructor', function* (t) { + const Constructor = function () {} - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - bind('$param3').toInstance('the third injected parameter') - bind('$param4').toInstance('the fourth injected parameter') - bind('$param5').toInstance('the fifth injected parameter') - bind('$param6').toInstance('the sixth injected parameter') - bind('$param7').toInstance('the seventh injected parameter') - }) + const bind = pluto() + bind('$injected').toConstructor(Promise.resolve(Constructor)) - const actual = module.get('$Root') + const actual = yield bind.get('$injected') - t.truthy(actual instanceof Root) - t.is(actual.param1, 'the first injected parameter') - t.is(actual.param2, 'the second injected parameter') - t.is(actual.param3, 'the third injected parameter') - t.is(actual.param4, 'the fourth injected parameter') - t.is(actual.param5, 'the fifth injected parameter') - t.is(actual.param6, 'the sixth injected parameter') - t.is(actual.param7, 'the seventh injected parameter') + t.truthy(actual instanceof Constructor) }) -test('when a constructor has eight parameters, module.get(name) returns the new Constructor() with the parameters injected', function* (t) { - const Root = function ($param1, $param2, $param3, $param4, $param5, $param6, $param7, $param8) { +test('when a constructor has one parameter, bind.get(name) returns the new Constructor() with the parameter injected', function* (t) { + const Root = function ($param1) { this.param1 = $param1 - this.param2 = $param2 - this.param3 = $param3 - this.param4 = $param4 - this.param5 = $param5 - this.param6 = $param6 - this.param7 = $param7 - this.param8 = $param8 } - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - bind('$param3').toInstance('the third injected parameter') - bind('$param4').toInstance('the fourth injected parameter') - bind('$param5').toInstance('the fifth injected parameter') - bind('$param6').toInstance('the sixth injected parameter') - bind('$param7').toInstance('the seventh injected parameter') - bind('$param8').toInstance('the eighth injected parameter') - }) + const bind = pluto() + bind('$Root').toConstructor(Root) + bind('$param1').toInstance('the first injected parameter') - const actual = module.get('$Root') + const actual = yield bind.get('$Root') t.truthy(actual instanceof Root) t.is(actual.param1, 'the first injected parameter') - t.is(actual.param2, 'the second injected parameter') - t.is(actual.param3, 'the third injected parameter') - t.is(actual.param4, 'the fourth injected parameter') - t.is(actual.param5, 'the fifth injected parameter') - t.is(actual.param6, 'the sixth injected parameter') - t.is(actual.param7, 'the seventh injected parameter') - t.is(actual.param8, 'the eighth injected parameter') }) -test('when a constructor has nine parameters, module.get(name) throws an exception', function* (t) { - // TODO: this is an issue that should be easy to work around nowadays - const Root = function ($param1, $param2, $param3, $param4, $param5, $param6, $param7, $param8, $param9) {} - - const module = pluto.createModule(function (bind) { - bind('$Root').toConstructor(Root) - bind('$param1').toInstance('the first injected parameter') - bind('$param2').toInstance('the second injected parameter') - bind('$param3').toInstance('the third injected parameter') - bind('$param4').toInstance('the fourth injected parameter') - bind('$param5').toInstance('the fifth injected parameter') - bind('$param6').toInstance('the sixth injected parameter') - bind('$param7').toInstance('the seventh injected parameter') - bind('$param8').toInstance('the eighth injected parameter') - bind('$param9').toInstance('the ninth injected parameter') - }) - - const error = t.throws(function () { - module.get('$Root') - }, Error) - t.is(error.message, "Pluto cannot inject constructor functions with 8 or more arguments at this time (it's a long story). Please use a non-constructor factory function instead or consider injecting fewer dependencies.") -}) +test('bind.get(name) rejects if the specified name is not mapped', function* (t) { + const bind = pluto() + const error = yield t.throws(bind.get('totally bogus key'), Error) -test('Module.get(name) throws if the specified name is not mapped', function* (t) { - const instance = pluto.createModule(function () {}) - const error = t.throws(function () { - instance.get('totally bogus key') - }, Error) t.is(error.message, "nothing is mapped for name 'totally bogus key'") }) -test('Module.getAll([names]) accepts an array of names and returns a matching array of instances', function* (t) { - const instance = pluto.createModule(function (bind) { - bind('a').toInstance('A') - bind('b').toInstance('B') - }) +test('bind.getAll([names]) accepts an array of names and returns a matching array of instances', function* (t) { + const bind = pluto() + bind('a').toInstance('A') + bind('b').toInstance('B') - const actual = instance.getAll(['a', 'b']) + const actual = yield bind.getAll(['a', 'b']) t.deepEqual(actual, ['A', 'B']) }) -test('Module.getAll([names]) throws if a name is unmapped', function* (t) { - const instance = pluto.createModule(function (bind) { - bind('a').toInstance('A') - }) +test('bind.getAll([names]) throws if a name is unmapped', function* (t) { + const bind = pluto() + bind('a').toInstance('A') + + const error = yield t.throws(bind.getAll(['a', 'totally bogus key']), Error) - const error = t.throws(function () { - instance.getAll(['a', 'totally bogus key']) - }, Error) t.is(error.message, "nothing is mapped for name 'totally bogus key'") }) -test('Module.eageryLoadAll executes all factory and constructor functions', function* (t) { +test('bind.eageryLoadAll executes all factory and constructor functions', function* (t) { let constructorCalled = false let factoryCalled = false - const instance = pluto.createModule(function (bind) { - bind('Constructor').toConstructor(function FakeConstructor() { - constructorCalled = true - }) - bind('factory').toFactory(function fakeFactory() { - factoryCalled = true - }) - }) + function FakeConstructor() { + constructorCalled = true + } - instance.eagerlyLoadAll() + function fakeFactory() { + factoryCalled = true + return Promise.resolve('fake-factory-result') + } + + const bind = pluto() + bind('Constructor').toConstructor(FakeConstructor) + bind('factory').toFactory(fakeFactory) + + const app = yield bind.bootstrap() t.truthy(constructorCalled, 'constructor is called') t.truthy(factoryCalled, 'factory function is called') + + t.truthy(app.get('Constructor') instanceof FakeConstructor) + t.is(app.get('factory'), 'fake-factory-result') }) diff --git a/package.json b/package.json index c828d0b..d2a9f09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pluto", - "version": "0.6.1", + "version": "1.0.0", "description": "Dependency injection that's so small, it almost doesn't count.", "homepage": "https://github.com/ecowden/pluto.js", "keywords": [ @@ -21,13 +21,14 @@ "node": ">=4.0.0" }, "dependencies": { - "eslint": "^3.14.1", - "underscore": "^1.8.3" + "co": "^4.6.0", + "lodash.memoize": "^4.1.2" }, "devDependencies": { "ava": "^0.17.0", "babel-preset-es2015-node6": "^0.4.0", "coveralls": "^2.11.15", + "eslint": "^3.14.1", "eslint-config-standard": "^6.2.1", "eslint-plugin-promise": "^3.4.0", "eslint-plugin-standard": "^2.0.1",