Skip to content

Commit

Permalink
Merge 45e07ef into 9cf4679
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce committed Jun 23, 2016
2 parents 9cf4679 + 45e07ef commit bc8517e
Show file tree
Hide file tree
Showing 14 changed files with 264 additions and 17 deletions.
27 changes: 23 additions & 4 deletions lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
var EventEmitter = require('events')
, util = require('util')
, path = require('canonical-path')
, Promise = require('bluebird')
, FactorySpec = require('./patterns/factory')
, ConstructorSpec = require('./patterns/constructor')
, LiteralSpec = require('./patterns/literal')
Expand Down Expand Up @@ -102,6 +103,14 @@ Container.prototype.use = function(ns, fn, options) {
* @public
*/
Container.prototype.create = function(id, parent) {
return this._create(id, parent, false);
};

Container.prototype.createAsync = function(id, parent) {
return this._create(id, parent, true);
};

Container.prototype._create = function(id, parent, isAsync) {
if (parent && id[0] == '.') {
// resolve relative component ID
id = path.join(path.dirname(parent.id), id);
Expand All @@ -114,7 +123,6 @@ Container.prototype.create = function(id, parent) {
return new InjectedContainer(this, parent, psource && psource.namespace);
}


id = this.resolve(id, parent);

var spec = this._specs[id];
Expand All @@ -133,9 +141,20 @@ Container.prototype.create = function(id, parent) {
throw new Error('Unable to create object "' + id + '" required by: ' + (parent && parent.id || 'unknown'));
}

var obj = spec.create(this);
this.emit('create', obj, spec);
return obj;
if (isAsync) {
return Promise.resolve(spec.create(this)).then(function (obj) {
this.emit('create', obj, spec);
return obj;
}.bind(this));
} else {
if (spec.isAsync) {
throw new Error('Container#create cannot be called on async dependency: "' + id + '"');
}

var obj = spec.create(this);
this.emit('create', obj, spec);
return obj;
}
}

Container.prototype.specs = function() {
Expand Down
29 changes: 21 additions & 8 deletions lib/spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Load modules.
var debug = require('debug')('electrolyte');
var debug = require('debug')('electrolyte')
, Promise = require('bluebird');


/**
Expand Down Expand Up @@ -40,6 +41,7 @@ function Spec(id, mod, source) {
var keys, i, len;

this.id = id;
this.isAsync = mod['@async'];
this.dependencies = mod['@require'] || [];
this.singleton = mod['@singleton'];
this.implements = mod['@implements'] || [];
Expand Down Expand Up @@ -73,16 +75,27 @@ Spec.prototype.create = function(container) {

var deps = this.dependencies
, args = []
, inst, sfn;
for (var i = 0, len = deps.length; i < len; ++i) {
inst = container.create(deps[i], this);
args.push(inst);
, inst;

var createDependency = function (dep) {
if (this.isAsync) {
return container.createAsync(dep, this);
} else {
return container.create(dep, this);
}
}.bind(this);

if (this.isAsync) {
args = Promise.mapSeries(deps, createDependency);
inst = args.spread(this.instantiate.bind(this));
} else {
args = deps.map(createDependency);
inst = this.instantiate.apply(this, args);
}

var i = this.instantiate.apply(this, args);
// Cache the instance if the component was annotated as being a singleton.
if (this.singleton) { this._instance = i; }
return i;
if (this.singleton) { this._instance = inst; }
return inst;
}

/**
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,19 @@
],
"main": "./lib",
"dependencies": {
"bluebird": "^3.4.1",
"canonical-path": "0.0.2",
"debug": "^2.2.0",
"depd": "^1.0.1",
"scripts": "0.1.x"
},
"devDependencies": {
"chai": "3.x.x",
"chai-express-handler": "git://github.com/jaredhanson/chai-express-handler.git",
"make-node": "0.3.x",
"mocha": "2.x.x",
"chai": "3.x.x",
"chai-express-handler": "git://github.com/jaredhanson/chai-express-handler.git"
"sinon": "^1.17.4",
"sinon-chai": "^2.8.0"
},
"engines": {
"node": "*"
Expand Down
1 change: 1 addition & 0 deletions test/bootstrap/node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var chai = require('chai');

chai.use(require('chai-express-handler'));
chai.use(require('sinon-chai'));

global.expect = chai.expect;
60 changes: 58 additions & 2 deletions test/container.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* global describe, it, expect */

var Promise = require('bluebird');
var Container = require('../lib/container');


Expand Down Expand Up @@ -28,6 +28,23 @@ describe('Container', function() {
});
});

describe('async component', function() {
var container = new Container();
container.use(require('./fixtures/sources/async'));

it('should throw an error when created synchronously', function() {
expect(function() {
var obj = container.create('asyncB');
}).to.throw(Error, 'Container#create cannot be called on async dependency: "asyncB"');
});

it('should throw an error when created synchronously via dependency', function() {
expect(function() {
var obj = container.create('asyncFailure');
}).to.throw(Error, 'Container#create cannot be called on async dependency: "asyncB"');
});
});

describe('patterns', function() {

var container = new Container();
Expand Down Expand Up @@ -161,7 +178,7 @@ describe('Container', function() {
var logger2 = container.create('logger');
expect(logger1).to.be.equal(logger2);
});

it('should throw an error when creating unknown object', function() {
expect(function() {
container.create('fubar');
Expand All @@ -181,6 +198,45 @@ describe('Container', function() {
});
});


describe('using async source', function() {
var asyncSource = require('./fixtures/sources/async');

var container = new Container();
container.use(asyncSource);

it('should not have any registered specs prior to creating object', function() {
var specs = container.specs();
expect(specs).to.be.an('array');
expect(specs).to.have.length(0);
});

it('should create asyncB', function() {
return container.createAsync('asyncB').then(function (obj) {
expect(obj).to.eql({waited: {done: 'B'}});
});
});

it('should create singleton instance of asyncB', function() {
return Promise.all([
container.createAsync('asyncB'),
container.createAsync('asyncB')
]).spread(function (asyncB1, asyncB2) {
expect(asyncB1).to.be.equal(asyncB2);
});
});

it('should create asyncA', function() {
return container.createAsync('asyncA').then(function (obj) {
expect(obj).to.eql([
[{done: 'B'}, {done: 'C'}],
{waited: {done: 'B'}},
{waited: {done: 'C'}}
]);
});
});
});

describe('using Memory cache source', function() {
var memory = require('./fixtures/sources/cache-memory');

Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/sources/async/asyncA.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
exports = module.exports = function (A, B, C) {
return [A, B, C];
}

exports['@async'] = true;
exports['@require'] = ['syncA', 'asyncB', 'asyncC'];
13 changes: 13 additions & 0 deletions test/fixtures/sources/async/asyncB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var Promise = require('bluebird');

exports = module.exports = function (dep) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({waited: dep});
}, 5);
});
}

exports['@async'] = true;
exports['@singleton'] = true;
exports['@require'] = ['syncB'];
12 changes: 12 additions & 0 deletions test/fixtures/sources/async/asyncC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
var Promise = require('bluebird');

exports = module.exports = function (dep) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({waited: dep});
}, 10);
});
}

exports['@async'] = true;
exports['@require'] = ['syncC'];
5 changes: 5 additions & 0 deletions test/fixtures/sources/async/asyncFailure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
exports = module.exports = function () {
return 'Should never exist';
}

exports['@require'] = ['asyncB'];
3 changes: 3 additions & 0 deletions test/fixtures/sources/async/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports = module.exports = function common(id) {
return require('./' + id);
};
5 changes: 5 additions & 0 deletions test/fixtures/sources/async/syncA.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
exports = module.exports = function (B, C) {
return [B, C];
};

exports['@require'] = ['syncB', 'syncC'];
6 changes: 6 additions & 0 deletions test/fixtures/sources/async/syncB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
exports = module.exports = function () {
return {done: 'B'};
};

exports['@singleton'] = true;
exports['@require'] = ['syncC'];
3 changes: 3 additions & 0 deletions test/fixtures/sources/async/syncC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports = module.exports = function () {
return {done: 'C'};
};
104 changes: 103 additions & 1 deletion test/spec.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* global describe, it, expect */

var sinon = require('sinon');
var Spec = require('../lib/spec');


Expand All @@ -16,4 +16,106 @@ describe('Spec', function() {

});

describe('#create', function() {
var containerApi = {create: function() {}, createAsync: function() {}}
, sandbox;

beforeEach(function() {
sandbox = sinon.sandbox.create();
});

afterEach(function() {
sandbox.restore();
});

context('when synchronous', function() {
var spec = new Spec('foo', {'@require': ['dep1', 'dep2']});

it('should create required dependencies', function() {
sandbox.stub(spec, 'instantiate');
var mockCreate = sandbox.stub(containerApi, 'create');

spec.create(containerApi);
expect(mockCreate).to.have.been.calledTwice;
expect(mockCreate).to.have.been.calledWith('dep1');
expect(mockCreate).to.have.been.calledWith('dep2');
});

it('should pass dependencies to instantiate', function() {
var mockInstantiate = sandbox.stub(spec, 'instantiate');
var mockCreate = sandbox.stub(containerApi, 'create');
mockCreate.onFirstCall().returns('foo');
mockCreate.onSecondCall().returns('bar');

spec.create(containerApi);
expect(mockCreate).to.have.been.calledTwice;
expect(mockInstantiate).to.have.been.calledWith('foo', 'bar');
});

it('should return the resulting value', function() {
var expected = {my: {fancy: 'obj'}};
var mockInstantiate = sandbox.stub(spec, 'instantiate').returns(expected);

var result = spec.create(containerApi);
expect(result).to.equal(expected);
});
});

context('when asynchronous', function() {
var spec = new Spec('foo', {'@async': true, '@require': ['dep1', 'dep2']});

it('should create required dependencies', function() {
sandbox.stub(spec, 'instantiate');
var mockCreate = sandbox.stub(containerApi, 'createAsync');

return spec.create(containerApi).then(function () {
expect(mockCreate).to.have.been.calledTwice;
expect(mockCreate).to.have.been.calledWith('dep1');
expect(mockCreate).to.have.been.calledWith('dep2');
});
});

it('should pass dependencies to instantiate', function() {
var mockInstantiate = sandbox.stub(spec, 'instantiate');
var mockCreate = sandbox.stub(containerApi, 'createAsync');
mockCreate.onFirstCall().returns(Promise.resolve('foo'));
mockCreate.onSecondCall().returns('bar');

return spec.create(containerApi).then(function(result) {
expect(mockInstantiate).to.have.been.calledWith('foo', 'bar');
});
});

it('should return the resulting value', function() {
var expected = {my: {fancy: 'obj'}};
var mockInstantiate = sandbox.stub(spec, 'instantiate').returns(expected);

return spec.create(containerApi).then(function(result) {
expect(result).to.equal(expected);
});
});

it('should wait for dependencies to resolve', function() {
var resolveDependency;

var mockFulfill = sandbox.stub();
var mockInstantiate = sandbox.stub(spec, 'instantiate');
var mockCreate = sandbox.stub(containerApi, 'createAsync');
mockCreate.onFirstCall().returns('foo');
mockCreate.onSecondCall().returns(new Promise(function (resolve) {
resolveDependency = function (v) {
mockFulfill();
return resolve(v);
};
}));

setTimeout(resolveDependency.bind(null, 'bar'), 10);
return spec.create(containerApi).then(function(result) {
expect(mockFulfill).to.have.been.called;
expect(mockInstantiate).to.have.been.calledWith('foo', 'bar');
});
});
});
});

});

0 comments on commit bc8517e

Please sign in to comment.