mainliner
Version:
inversion of control (ioc) container and dependency injection for node6 spiced with talent composition
305 lines (233 loc) • 9.09 kB
JavaScript
const Composer = require("talentcomposer");
/**
* Represents the ioc container instance
* @class
* @type {Container}
*/
module.exports = class Container {
/**
* Constructs the instance
* @constructor
* @param {Graph} graph The graph instance
* @param {Object} modifiers The vertex modifiers like instead of the vertex a factory which can create the vertex must be returned or the the vertex is optional
*/
constructor(graph, modifiers) {
this.graph_ = graph;
this.modifiers_ = modifiers;
this.singletonCache_ = new Map();
}
/**
* Returns the vertex data and tampers it using modifiers if necessary
* @param {string} current The name of the current vertex in traversal
* @private
* @returns {Object} The data which belongs to the current vertex
*/
getTamperedVertexData_(current) {
if (this.modifiers_.optional.isOptional(current)) {
const choppedOptional = this.modifiers_.optional.chop(current);
return this.graph_.getVertexData(choppedOptional) || {"type": "optional"};
}
if (this.modifiers_.factory.isFactory(current)) {
const choppedCurrent = this.modifiers_.factory.chop(current);
const vertexData = this.graph_.getVertexData(choppedCurrent);
if (vertexData && vertexData.type !== "class") {
throw new Error("Only classes can be factorized");
}
if (vertexData) {
vertexData.type = "factory";
}
return vertexData;
}
return this.graph_.getVertexData(current);
}
/**
* Parses the talent names as clauses
* @param {string} talentName The talent clause
* @private
* @returns {Object} The name, optional alias and the optional exclusion marker for the talent
*/
parseTalentName_(talentName) {
const re1 = /[^:]*/;
const re2 = /[^:]*$/;
const re3 = /\,/;
const re4 = /\>/;
const re5 = /\-/;
const re6 = /[^-]*/;
const name = re1[Symbol.match](talentName)[0].trim();
let resolutions = re2[Symbol.match](talentName)[0].trim();
resolutions = re3[Symbol.split](resolutions).map(match => match.trim());
const aliases = resolutions.reduce((accu, resolution) => {
if (re4.test(resolution)) {
accu.push(re4[Symbol.split](resolution).map(match => match.trim()));
}
return accu;
}, []);
const excludes = resolutions.reduce((accu, resolution) => {
if (re5.test(resolution)) {
accu.push(re6[Symbol.match](resolution).map(match => match.trim()));
}
return accu;
}, []);
return {name, aliases, excludes};
}
/**
* Composes the instance with talents using talentcomposer library
* @param {Object} vertexData The data belonging to the vertex
* @param {Object} instance The instance to compose with
* @private
* @returns {*} Whatever talentcomposer returns (Hopefully a composed instance)
*/
compose_(vertexData, instance) {
if (!vertexData.vertex.$compose) {
return instance;
}
if (!Array.isArray(vertexData.vertex.$compose)) {
throw new Error("The \"$compose\" list should be an array of strings");
}
const talents = vertexData.vertex.$compose.map(talentName => {
if (typeof talentName !== "string") {
throw new Error("The \"$compose\" list should be an array of strings");
}
const {name, aliases, excludes} = this.parseTalentName_(talentName);
const talentData = this.graph_.getVertexData(name);
if (!talentData) {
throw new Error(`The talent "${talentName}" is not registered`);
}
if (talentData.type !== "passThrough") {
throw new Error(`The talent "${talentName}" has to be a talent created by the "#createTalent" method`);
}
let ret = talentData.vertex;
if (aliases.length) {
for (const alias of aliases) {
ret = Composer.alias(ret, alias[0], alias[1]);
}
}
if (excludes.length) {
for (const exclude of excludes) {
ret = Composer.exclude(ret, exclude[0]);
}
}
return ret;
});
return Composer.composeWithTalents(instance, ...talents);
}
/**
* Prepares injection for a class
* @param {Object} vertexData The data belonging to the vertex
* @param {Array.<*>} args The dependencies to inject and extra params together
* @param {string} current The name of the current vertex in traversal
* @param {Object} perRequestCache The cache where the per request singletons are
* @private
* @returns {Object} The prepared instance ready to be injected/returned
*/
createInjectionForClass_(vertexData, args, current, perRequestCache) {
switch (vertexData.lifeCycle) {
case "unique":
return this.compose_(vertexData, Reflect.construct(vertexData.vertex, args));
case "singleton":
if (this.singletonCache_.has(current)) {
return this.singletonCache_.get(current);
}
const singletonInstance = this.compose_(vertexData, Reflect.construct(vertexData.vertex, args));
this.singletonCache_.set(current, singletonInstance);
return singletonInstance;
default:
if (perRequestCache.has(current)) {
return perRequestCache.get(current);
}
const perRequestInstance = this.compose_(vertexData, Reflect.construct(vertexData.vertex, args));
perRequestCache.set(current, perRequestInstance);
return perRequestInstance;
}
}
/**
* Prepares injection for a generic vertex
* @param {string} current The name of the current vertex in traversal
* @param {string} dependencies The returned/instantiated dependencies for this vertex
* @param {Object} perRequestCache The cache where the per request singletons are
* @param {...*} extraParams The extra parameters specified runtime
* @private
* @returns {Object} The prepared vertex ready to be injected/returned
*/
createInjection_(current, dependencies, perRequestCache, ...extraParams) {
const vertexData = this.getTamperedVertexData_(current);
const args = [...dependencies, ...extraParams];
if (!vertexData) {
throw new Error(`${current} hasn't been registered`);
}
switch (vertexData.type) {
case "optional":
return null;
case "factory":
return {
get(...factoryParams) {
return Reflect.construct(vertexData.vertex, [...args, ...factoryParams]);
}
};
case "class":
return this.createInjectionForClass_(vertexData, args, current, perRequestCache);
case "function":
return Reflect.apply(vertexData.vertex, null, args);
case "passThrough":
return vertexData.vertex;
}
}
/**
* The !!!core!!! The implemented dfs(depth first) traverse algorythm
* @param {string} current The name of the current vertex in traversal
* @param {Array.<Object>} exploring The vertexes under exploration
* @param {Object} perRequestCache The cache where the per request singletons are
* @param {...*} extraParams The extra parameters specified runtime
* @private
* @returns {*} The result of traversal
*/
dfs_(current, exploring, perRequestCache, ...extraParams) {
exploring.push(current);
const childVertexes = this.graph_.getAdjacentVertexes(current);
const dependencies = Array.from(childVertexes).map(childVertex => {
if (exploring.includes(childVertex)) {
throw new Error("A cycle has been detected");
}
return this.dfs_(childVertex, exploring, perRequestCache);
});
exploring.pop();
return this.createInjection_(current, dependencies, perRequestCache, ...extraParams);
}
/**
* Returns a prepared vertex fro the graph (The actual request)
* @param {string} name The name of the vertex
* @param {...*} extraParams The extra parameters specified runtime
* @public
* @returns {*} The result of the request
*/
get(name, ...extraParams) {
const vertexData = this.graph_.getVertexData(this.modifiers_.factory.chop(name));
if (!vertexData) {
throw new Error(`${name} hasn't been registered`);
}
return this.dfs_(name, [], new Map(), ...extraParams);
}
/**
* Registers a vertex on the container and put it into the graph
* @param {string} name The name of the vertex on the container
* @param {*} vertex The actual physical vertex to register
* @param {"unique"|"perRequest"|"singleton"} lifeCycle The life cycle specifier for an instance
* @public
* @returns {undefined}
*/
register(name, vertex, lifeCycle) {
this.graph_.addVertex(name, vertex, lifeCycle);
if (!vertex.$inject) {
return;
}
if (!Array.isArray(vertex.$inject)) {
throw new Error("The \"$inject\" list should be an array of strings");
}
for (const inject of vertex.$inject) {
if (typeof inject !== "string") {
throw new Error("The \"$inject\" list should be an array of strings");
}
this.graph_.addEdge([name, inject]);
}
}
};