lib/container.js
// Load modules.
var EventEmitter = require('events')
, util = require('util')
, path = require('canonical-path')
, Promise = require('promise')
, Assembly = require('./assembly')
, FactoryComponent = require('./patterns/factory')
, ConstructorComponent = require('./patterns/constructor')
, LiteralComponent = require('./patterns/literal')
, InjectedContainer = require('./injectedcontainer')
, ImplNotFoundError = require('./errors/implnotfound')
, ComponentNotFoundError = require('./errors/componentnotfound')
, ComponentCreateError = require('./errors/componentcreate')
, pMultiFilter = require('./p-multifilter')
, debug = require('debug')('electrolyte');
/**
* Manages objects within an application.
*
* A container creates and manages the set of objects within an application.
* Using inversion of control principles, a container automatically instantiates
* and assembles these objects.
*
* Objects are created according to a component specification. A component
* specification defines the requirements necessary to create an object. Such
* requirements include parameters and other objects required by the object
* being created. When an object requires other objects, the required objects
* will be created prior to the requiring object, and so on, transitively
* assembling the complete graph of objects as necessary. Component
* specifications, and the objects created from them, are often colloquially
* referred to as simply "components."
*
* A component specification can request objects that conform to an interface.
* An interface declares the abstract behavior of an object, without concern
* for implementation details. The container will create an object that
* conforms to the interface according to the runtime environment and
* configuration. This component-based approach to software development
* increases modularity of the system.
*
* @constructor
* @api public
*/
function Container() {
EventEmitter.call(this);
this._assemblies = {};
this._order = [];
this._components = {};
this._resolvers = [];
this._filters = [];
this._sorters = [];
this._variables = {};
this._wrappers = {};
this.resolver(require('./resolvers/id')());
}
// Inherit from `EventEmitter`.
util.inherits(Container, EventEmitter);
/**
* Utilize an assembly.
*
* The container creates objects from assemblies. An assembly is a collection
* of components that are built to work together and form a logical unit of
* functionality. An assembly is typically distributed as a package,
* facilitating resuse across applications. For application-specific components
* that are not reusable, an assembly can simply consist of a directory on the
* file system.
*
* @param {string?} ns - optional namespace under which to mount the assembly.
* @param {function|object} asm - assembly of components.
* @public
*/
Container.prototype.use = function(ns, asm) {
if (typeof ns !== 'string') {
// force ns to be an empty string if not specified
asm = ns;
ns = '';
}
asm = asm || {};
//console.log('USE');
//console.log(asm);
// TODO: Clean this up
if (asm.use) {
//console.log('');
asm.use(this);
}
// accept either object or loader function
if (typeof asm == 'function') {
asm = { load: asm, export: true };
}
if (typeof asm.load != 'function') {
throw new TypeError("Container#use requires `asm` to be either a function or an object with a `load` function, '" + (typeof asm.load) + "' has been passed");
}
var h = this._order.length;
asm = new Assembly(h, ns, asm);
this._assemblies[h] = asm;
this._order.unshift(h);
var ids = asm.components
, comp, i, len;
for (i = 0, len = ids.length; i < len; ++i) {
comp = asm.load(ids[i]);
if (!comp) {
throw new ComponentNotFoundError("Cannot find component '" + ids[i] + "'");
}
this._registerComponent(ids[i], comp, asm);
}
return this;
}
/**
* Create an object.
*
* Creates an object from the assemblies registered with the container. When
* the object being created requires other objects, the required objects will
* automatically be created and injected into the requiring object. In this
* way, complex graphs of objects can be created in a single single line of
* code, eliminating extraneous boilerplate.
*
* A component specification can declare an object to be a singleton. In such
* cases, only one instance of the object will be created. Subsequent calls to
* create the object will return the singleton instance.
*
* Examples:
*
* var foo = IoC.create('foo');
*
* var boop = IoC.create('beep/boop');
*
* @param {string} id - The id or interface of the object to create.
* @param {Component} [parent] - (private) The parent component requiring the object.
* @param {Component} [ecomp] - (private) The component to create.
* @returns {Promise}
* @public
*/
Container.prototype.create = function(id, parent, ecomp, options) {
//if (id == 'http://i.bixbyjs.org/http/middleware/session') {
if (id == 'http://i.bixbyjs.org/http/middleware/authenticate') {
console.log('!!! ATTEMPTED TO CREATE AUTH MIDDLEWARE: ' + parent.id);
}
// TODO: Implement feature to create from function???
/*
if (typeof id == 'function') {
var fc = new FactoryComponent('__anonymous__', id);
var c = fc.create(this);
c.then(function(i) {
console.log('*******________ RESOLVED')
console.log(i);
})
return c;
}
*/
if (id[id.length - 1] == '?') {
console.log('**** IT IS OPTIONAL ***');
console.log(id);
}
var optional = false;
if (id[id.length - 1] == '?') {
optional = true;
id = id.slice(0, id.length - 1);
}
// built-ins
switch (id) {
case '!container':
return Promise.resolve(new InjectedContainer(this, parent));
//case '$location':
//console.log('*** CREATE LOCATION ****');
// TODO: Factor this better
//return require('./resolvers/location')(this, parent);
}
if (this._variables[id]) {
console.log('IT IS A VARIABLE ****');
return this._variables[id](this, parent);
}
// FIXME: self should be defined here, not in the promise
return new Promise(function(resolve, reject) {
var self = this;
function create(comp) {
if (!parent || comp._assembly.h == parent._assembly.h) {
// The object is being created by another object within the same
// assembly, or by the main script.
return resolve(comp.create(self, options));
} else {
// The object is being created by an object from another assembly.
// Most typically, a component interface is requested, allowing dynamic
// object creation according to the runtime environment and
// configuration.
//
// Assemblies declare the components they export for use by other
// assemblies. Any non-exported components are considered private, and
// usable only within the assembly itself. Restrictions are put in
// place to prevent private components from being used outside of their
// assembly.
if (!comp._assembly.isExported(comp.id)) {
return reject(new ComponentCreateError("Private component '" + comp.id + "' required by '" + parent.id + "'"));
}
return resolve(comp.create(self, options));
}
}
if (ecomp) {
// A component has been exposed via introspection, and an object is being
// created from the component specification. This is a fast path,
// implemented as an optimization to avoid redundant resolution
// operations.
return create(ecomp);
}
if (parent && id[0] == '.') {
// resolve relative component ID
id = path.join(path.dirname(parent.id), id);
}
id = this.resolve(id, parent, optional);
if (!id) {
// optional
return resolve(undefined);
}
if (Array.isArray(id)) {
pMultiFilter(id, self._filters)
.then(function(candidates) {
// TODO: handle 0-length array
if (candidates.length > 1) {
// FIXME: hack to auto-select "app/" prefixed components. This should be factored
// out to a bixby-based resolver
var countAppProvided = 0
, iAppProvided, i, len;
for (i = 0, len = candidates.length; i < len; ++i) {
if (candidates[i].id.indexOf('app/') == 0) {
countAppProvided++;
iAppProvided = i;
}
}
if (countAppProvided == 1) {
return create(candidates[iAppProvided]);
}
// TODO: Make this error string to candidate ids
// FIXME: this is giving [object Object],[object Object] in string where the map is
reject(new Error("Multiple components provide interface '" + id + "' required by '" + (parent || 'unknown') + "'. Configure one of: " + candidates.map(function(c) { return c.id }).join(', ')));
}
var c = candidates[0];
return create(c);
});
return;
}
if (typeof id == 'object') {
// TODO: In what cases do we fall below and need to regiser components?
//console.log('**** RETURNED A COMPONENT, OPTIMIZE FAST PATH');
return create(id);
}
// TODO: Eliminate this below here by always returning a component from a resolver.
var comp = this._components[id];
if (comp) {
return create(comp);
} else {
var self = this;
this._loadComponent(id, function(err, comp) {
if (err instanceof ComponentNotFoundError) {
// Reject with a more informative error message that indicates the
// requiring component. This assists the developer in finding and
// fixing the cause of error.
reject(new ComponentNotFoundError("Unable to create component '" + id + "' required by '" + (parent && parent.id || 'unknown') + "'", id));
} else if (err) {
return reject(err);
}
return create(comp);
});
}
}.bind(this));
}
Container.prototype.components = function() {
var ids = Object.keys(this._components)
, comps = []
, i, len;
for (i = 0, len = ids.length; i < len; ++i) {
comps.push(this._components[ids[i]]);
}
return comps;
}
Container.prototype.resolve = function(id, parent, optional) {
var resolvers = this._resolvers
, fn, rid, i, len;
for (i = 0, len = resolvers.length; i < len; ++i) {
fn = resolvers[i];
rid = fn(id, parent && parent.id);
/*
if (rid) {
if (Array.isArray(rid)) {
console.log('NEED TO FILTER THIS...');
return;
}
}
*/
if (rid) { return rid; }
}
if (optional) { return undefined; }
throw new ImplNotFoundError("Cannot find implementation of '" + id + "' required by '" + (parent && parent.id || 'unknown') + "'", id);
}
Container.prototype.resolver = function(fn) {
this._resolvers.push(fn);
}
Container.prototype.filter = function(fn) {
this._filters.push(fn);
}
Container.prototype.sorter = function(fn) {
this._sorters.push(fn);
}
Container.prototype.variable = function(name, fn) {
this._variables['$' + name] = fn;
}
Container.prototype.wrap = function(name, fn) {
this._wrappers[name] = fn;
}
/**
* Load component specification.
*
* As a prerequisite for creating an object, a component specification must be
* available. The specification declares instructions about how to create an
* object, such as whether the object should be a singleton instance and any
* other objects required by the object to be created.
*
* Object instances will be created by invoking the component's factory
* function. A factory function is typically a function that returns the object
* or a constructor that is invoked using the `new` operator.
*
* @param {string} id - The id of the component specification to load.
* @private
*/
Container.prototype._loadComponent = function(id, cb) {
debug('autoload %s', id);
var order = this._order
, asm, comp, rid
, i, len;
for (i = 0, len = order.length; i < len; ++i) {
asm = this._assemblies[order[i]];
rid = path.relative(asm.namespace, id);
if (rid.indexOf('../') == 0) { continue; }
comp = asm.load(id);
if (comp) {
spec = this._registerComponent(id, comp, asm);
return cb(null, spec);
}
}
return cb(new ComponentNotFoundError("Cannot find component '" + id + "'", id));
}
/**
* Register object specification.
*
* When a specification is registered, the creational pattern used to create
* instances of the object will be determined. Creational patterns include
* factory functions which return an object and constructors that are invoked
* using the `new` operator.
*
* Additionally, common annotations needed when creating the instance are
* examined. Such annotations include `@require`, which is used to declare
* other objects required by this object and `@singleton` which is set to
* `true` to indicate that only a single instance of the object should be
* created. Other annotations may be declared, but such annotations are not
* interpreted by the IoC container and are intended to be used by higher-level
* frameworks.
*
* @param {string} id - The id of the specification to load.
* @param {object} mod - The module containing the object factory.
* @param {number} hs - The handle of the source from which the spec was loaded.
* @private
*/
Container.prototype._registerComponent = function(id, mod, source) {
var spec, pattern;
if (mod['@literal']) {
pattern = 'literal';
} else if (typeof mod == 'function') {
// The module exports a function. If the function name begins with a
// capital letter, it will be treated as a constructor. Otherwise, it will
// be treated as a factory function;
var name = mod.name || 'anonymous';
if (name[0] == name[0].toUpperCase()) {
pattern = 'constructor';
} else {
pattern = 'factory';
}
}
switch (pattern) {
case 'factory':
debug('register factory %s', id);
spec = new FactoryComponent(id, mod, source);
break;
case 'constructor':
debug('register constructor %s', id);
spec = new ConstructorComponent(id, mod, source);
break;
case 'literal':
default:
debug('register literal %s', id);
spec = new LiteralComponent(id, mod, source);
break;
}
this._components[spec.id] = spec;
return spec;
}
// Expose constructor.
module.exports = Container;