@mdzzohrabi/container
Version:
Dependency Injection Container
353 lines • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@mdzzohrabi/common");
let { isObject, isArray, isFunction, isEmpty, isString, isClass, isClassObject } = common_1.is;
function isFactory(object) { return isObject(object) && 'factory' in object; }
function isService(object) { return object instanceof Function; }
exports.isService = isService;
function isIDefinition(object) { return (isObject(object) && ('target' in object || 'factory' in object)); }
exports.isIDefinition = isIDefinition;
function isInjectable(object) { return '$inject' in object; }
exports.isInjectable = isInjectable;
exports.Name = {
isService: (name) => name[0] !== '$',
isParameter: (name) => name[0] == '$' && name[1] !== '$',
isTag: (name) => name[0] == '$' && name[1] == '$'
};
let defaultContainer;
var ServiceType;
(function (ServiceType) {
ServiceType[ServiceType["DefinitionClass"] = 0] = "DefinitionClass";
ServiceType[ServiceType["DefinitionObject"] = 1] = "DefinitionObject";
ServiceType[ServiceType["Service"] = 2] = "Service";
ServiceType[ServiceType["FactoryFunction"] = 3] = "FactoryFunction";
ServiceType[ServiceType["FactoryClass"] = 4] = "FactoryClass";
})(ServiceType || (ServiceType = {}));
/**
* Dependency Injection Container
* @author Masoud Zohrabi <mdzzohrabi@gmail.com>
*/
class Container {
constructor(services = {}, parameters = {}, global = false) {
this.services = {};
this.parameters = {};
// Loaded services cache
this.loaded = {};
this.setAll(services);
this.setParams(parameters);
if (global)
this.setAsMain();
}
/** Clean loaded services cache */
fresh() {
this.loaded = {};
return this;
}
/** Reset container */
reset() {
this.services = {};
this.loaded = {};
this.parameters = {};
return this;
}
setParams(parameters) {
common_1.forEach(parameters, (value, name) => {
this.setParam(name, value);
});
return this;
}
/** Declare multiple services */
setAll(services) {
common_1.forEach(services, (service, name) => {
this.set(name, service);
});
return this;
}
/** Get service definition */
getDefinition(serviceName) {
if (!this.has(serviceName))
throw Error(`Service "${serviceName}" not found`);
return this.services[`$${serviceName}`];
}
/** Set a service */
set(serviceName, service) {
// Service definition
let def = {
shared: true,
tags: [],
factory: undefined,
target: undefined,
parameters: [],
calls: {}
};
if (isFunction(service)) {
let parsed = common_1.parseFunction(service);
if (parsed.arrow || parsed.name.endsWith('Factory'))
def.factory = service;
else
def.target = service;
def.parameters = this.getDependencies(service);
}
else if (isIDefinition(service)) {
def = Object.assign(def, service);
}
else {
throw Error(`Invalid service definition`);
}
this.loaded[`$${serviceName}`] = undefined;
this.services[`$${serviceName}`] = def;
return this;
}
/** Set a service factory */
setFactory(serviceName, factory, shared = true) {
return this.set(serviceName, { factory: factory, shared: shared });
}
/** Set container parameter */
setParam(name, value) {
this.parameters['$' + name] = value;
return this;
}
/** Check if a service has factory */
isFactory(name) {
return !!this.services['$' + name].factory;
}
/** Get a parameter */
getParam(name) {
return this.parameters['$' + name];
}
/** Check if a service exists */
has(serviceName) {
return this.services['$' + serviceName] !== undefined;
}
/** Build and get a service */
get(serviceName) {
// Check if service cached
if (this.loaded['$' + serviceName]) {
return this.loaded['$' + serviceName];
}
// Get service definition
let service = this.getDefinition(serviceName);
// Build service
let instance = this.buildService(service);
// Private instance
if (service.shared === false) {
return instance;
}
// Cache for next fetch
return this.loaded['$' + serviceName] = instance;
}
/**
* Resolve dependencies
* There are three type of dependencies :
* - Tag dependency [prefixed with $$]
* - Parameter dependency [prefixed with $]
* - Service dependency
* @example
* resolve([ '$dbName', '$$loggers', 'odm' ])
* @param deps Dependencies
*/
resolve(deps) {
let resolveDependency = (dep) => {
// Tag
if (exports.Name.isTag(dep)) {
let services = [];
let tagName = dep.substr(2);
common_1.forEach(this.services, (service, name) => {
if (service.tags.indexOf(tagName) >= 0) {
services.push(this.get(name.substr(1)));
}
});
return services;
}
// Parameter
if (exports.Name.isParameter(dep))
return this.parameters[dep];
// Service
return this.get(dep);
};
if (isString(deps)) {
return resolveDependency(deps);
}
return deps.map(resolveDependency);
}
invoke(func) {
if (func === undefined || !func)
return undefined;
if (typeof func === 'string') {
return this.resolve(func);
}
return this.buildService(func);
}
prepareDefinition(service) {
let def;
let parsedParams = [];
if (isString(service))
def = this.get(service);
else if (isFunction(service)) {
parsedParams = this.getDependencies(service);
def = {
target: service,
parameters: [],
calls: {},
factory: undefined,
tags: [],
properties: {}
};
}
else {
def = service;
}
let { target, parameters, properties, calls } = def;
// Inject decorator
if (target && isInjectable(target)) {
let { $inject } = target;
if ($inject.arguments)
def.parameters = parameters.length > 0 ? parameters : $inject.arguments;
if ($inject.properties)
def.properties = Object.assign({}, properties, $inject.properties);
if ($inject.calls)
def.calls = Object.assign({}, calls, $inject.calls);
}
// Use parsed parameters if no parameters injected by user
if (def.parameters.length == 0)
def.parameters = parsedParams;
return def;
}
/**
* Build a service
* @param service Service definition or function
*/
buildService(service) {
let { parameters, calls, factory, target, properties } = this.prepareDefinition(service);
// Service factory
if (factory) {
let resolvedParams = this.resolve(parameters);
let parsed = common_1.parseFunction(factory);
if (target)
resolvedParams = [target].concat(resolvedParams);
if (parsed.arrow)
return factory(...resolvedParams);
else
return factory.apply(this, resolvedParams);
// Service build using ServiceDefinition
}
else if (target) {
let parsed = common_1.parseFunction(target);
let resolvedParams = this.resolve(parameters);
if (parsed.arrow)
return target(...resolvedParams);
let instance = new target(...resolvedParams);
// Properties
common_1.forEach(properties, (service, name) => {
instance[name] = this.resolve(service);
});
// Calls
common_1.forEach(calls, (params, method) => {
instance[method].call(instance, this.resolve(params));
});
return instance;
}
throw Error(`Invalid service`);
}
invokeLater(func, method) {
if (isFunction(func)) {
let instance;
return common_1.createFunction(func.name + '$' + method, (...params) => {
return (instance || (instance = this.invoke(func)))[method](...params);
});
}
if (isObject(func)) {
return (...params) => {
return func[method](...params);
};
}
}
getDependencies(target) {
return common_1.parseFunction(target).args;
}
setAsMain() {
defaultContainer = this;
return this;
}
get isGlobal() { return defaultContainer === this; }
static get main() { return defaultContainer; }
}
exports.Container = Container;
defaultContainer = new Container;
//#region Decorators
function Service(name, options = {}) {
return (target) => {
let definition = { target: target };
definition.shared = options.shared || true;
definition.parameters = options.arguments || [];
definition.calls = options.calls || {};
definition.tags = options.tags || [];
let container = options.container || defaultContainer;
container.set(name, definition);
};
}
exports.Service = Service;
function $getInject(target) {
if (isClassObject(target)) {
target = target.constructor;
}
target.$inject = target.$inject || {
properties: {},
calls: {},
arguments: []
};
return target;
}
function classDecorator(injects, target) {
let { $inject } = $getInject(target);
let def = {};
if (isArray(injects)) {
def.arguments = injects;
}
$inject = Object.assign($inject, def);
}
function propertyDecorator(injects, target, propertyKey) {
let { $inject } = $getInject(target);
$inject.properties[propertyKey] = injects;
}
function Call(...params) {
return (target, key, descriptor) => {
let { $inject } = $getInject(target);
$inject.calls[key] = params;
};
}
exports.Call = Call;
exports.Decorator = {
isClass: (args) => args.length == 1,
isProperty: (args) => args.length == 2 || (args.length == 3 && args[2] === undefined),
isMethod: (args) => args.length == 3 && typeof args[2] === 'object',
isParameter: (args) => args.length == 3 && typeof args[2] === 'number'
};
function Inject(injects) {
return function (...params) {
let { isClass, isMethod, isParameter, isProperty } = exports.Decorator;
if (isClass(arguments)) {
let [target] = params;
classDecorator(injects, target);
}
else if (isProperty(arguments)) {
let [target, key] = params;
propertyDecorator(injects, target, key);
}
else if (isParameter(arguments)) {
let [target, method, index] = arguments;
let { $inject } = $getInject(target);
// Constructor
if (method == undefined) {
$inject.arguments = Array.isArray($inject.arguments) ? $inject.arguments : [];
$inject.arguments[index] = injects;
}
else {
throw Error(`Method parameter decorator not allowed`);
}
}
};
}
exports.Inject = Inject;
//#endregion
//# sourceMappingURL=container.js.map