tsreflect-ioc
Version:
Inversion of control and Dependency injection framework for typescript based on the tsreflect-compiler package.
478 lines (428 loc) • 21.9 kB
text/typescript
/// <reference path="_references.ts" />
import compiler = require('./compiler');
import cmp = require('tsreflect');
import reflector = require('./reflector');
var fs = require('fs');
import helper = require('./helper');
var objectExt = require('./object'),
log4js = require('log4js'),
extend = require('extend');
export module ioc {
/**
* Configuration which is used for initalizing the container.
**/
export interface IContainerConfig {
/**
* When set to true (standard), the classes will be automatically registered by using the @registerType directive.
**/
autoRegister?: boolean;
/**
* When set to true (standard), the container will recompile needed .ts sources and .d.ts defintions.
**/
compileOnStart?: boolean;
/**
* Configuration which is used for the reflector.
**/
reflectorConfig?: reflector.ioc.IReflectorConfig;
/**
* Configuration for the compiler.
**/
compilerConfig?: compiler.ioc.ICompilerConfig;
}
/**
* Standard container configuration which can be used for extending to your own configuration.
**/
export class StandardContainerConfig implements IContainerConfig {
/**
* When set to true (standard), the classes will be automatically registered by using the @registerType directive.
**/
autoRegister: boolean = true;
/**
* When set to true (standard), the container will recompile needed .ts sources and .d.ts defintions.
**/
compileOnStart: boolean = true;
/**
* Configuration from the reflector.
**/
reflectorConfig: reflector.ioc.IReflectorConfig = {
sourceFiles: []
};
/**
* Configuration for the compiler.
**/
compilerConfig: compiler.ioc.ICompilerConfig = new compiler.ioc.StandardCompilerConfig();
}
/**
* Interface which is used for the container instance.
**/
export interface IContainer {
/**
* Resolves the needed instance from the container.
* @param {String} registeredAs The registered interface name where we want the instance.
* @param {Boolean} collection If set to true (standard is false) a collection is returned. If set to false, only one registration is allowed. Otherwise an error is thrown.
**/
resolve(registeredAs: string, collection?: boolean): any;
/**
* Checks if the given name was registered for resolving.
* @returns {Boolean} True, if you can use the given name with resolveByName, otherwise false.
* @param {Boolean} collection If set to true (standard is false) a collection is returned. If set to false, only one registration is allowed. Otherwise an error is thrown.
**/
isNameRegistered(name: string, collection?: boolean): boolean;
/**
* Initialized the container.
* @param {IContainerConfig} config? Initialized the container with this configuration.
*/
init(config?: IContainerConfig): void;
/**
* Registers the type on the container as named.
* @throws {Error} Throws an error whenever registration failed because of several reasons.
**/
registerNamed(name: string, symbol: cmp.Symbol);
/**
* Registers the type on the container as named.
* @throws {Error} Throws an error whenever registration failed because of several reasons.
**/
registerNamed(name: string, className: string);
/**
* Registers the type on the container.
* @throws {Error} Throws an error whenever registration failed because of several reasons.
**/
registerType(registerAs: string, symbol: cmp.Symbol);
/**
* Registers the type on the container.
* @throws {Error} Throws an error whenever registration failed because of several reasons.
**/
registerType(registerAs: string, className: string);
/**
* Registers the type on the container.
* @param {cmp.Symbol} symbol The tsreflect class symbol for the class to register.
* @throws {Error} Throws an error whenever registration failed because of several reasons.
**/
registerType(symbol: cmp.Symbol);
/**
* Checks the registrations and their constructor injections if the container can resolve it correctly.
*/
checkRegistrations(): void;
/**
* Resolves the needed instance from the container.
* @param {String} registeredAs The registered interface name where we want the instance.
**/
resolve(registeredAs: string): any;
/**
* Resolves the needed named instance from the container.
* @param {String} name The registered registgration name where we want the instance.
**/
resolveByName(name: string): any;
}
/**
* Container implementation.
* @registerType
**/
export class Container implements IContainer {
/**
* The main instance of the root container.
* @dependency
**/
private static rootContainer: IContainer;
// Stores the registrations in this container.
private _registrations: any = {};
/**
* The current log4js logger.
*/
logger: any;
/**
* The used reflector.
*/
private static _reflector: reflector.ioc.Reflector;
/**
* The used compiler.
*/
private static _compiler: compiler.ioc.Compiler;
/**
* Name of the annotation which is used to mark a type as registerable class.
* @private
*/
private registerTypeAnnotation: string = 'registerType';
/**
* Resolves the needed instance from the container.
* @param {String} registeredAs The registered interface name where we want the instance.
* @param {Boolean} collection If set to true (standard is false) a collection is returned. If set to false, only one registration is allowed. Otherwise an error is thrown.
**/
resolve(registeredAs: string, collection: boolean = false): any {
var registrations = this._registrations[registeredAs];
if (registrations === undefined) {
// todo: custom error
throw new Error(
(registeredAs.startsWith('name:') ?
'Can\'t resolve type, no registrations found for ' + registeredAs :
'Can\'t resolve type, no named registrations found for ' + registeredAs.replace('name:', '')));
}
if (!collection && registrations.length > 1) {
// todo: custom error
throw new Error(
(registeredAs.startsWith('name:') ?
'Can\'t resolve, there are more than one registrations for ' + registeredAs :
'Can\'t resolve type, there are more than one named registrations for ' + registeredAs.replace('name:', '')));
}
var resolved = registrations.select((symbol: cmp.Symbol) => {
var symbolType = symbol.getType();
var constructorParameters = (<cmp.Signature[]>symbolType.getConstructSignatures()).first().getParameters();
if (constructorParameters.any()) {
// TODO: Constructor injection
throw new Error('TODO: Constructor injection');
}
// TODO: lifetime manager
return symbolType.createInstance();
});
return collection ? resolved : resolved.first();
}
/**
* Resolves the needed named instance from the container.
* @param {String} name The registered registgration name where we want the instance.
**/
resolveByName(name: string, collection: boolean = false): any {
return this.resolve('name:' + name, collection);
}
/**
* Constructor.
*/
constructor() {
var configContent = JSON.parse(fs.readFileSync('log4js.config.json', 'utf8').trim());
log4js.configure(configContent);
// init the logging
this.logger = log4js.getLogger();
this.logger.info('Logger was initialized, confiuration: ' + JSON.stringify(configContent));
}
/**
* Checks the registrations and their constructor injections if the container can resolve it correctly.
*/
checkRegistrations(): void {
var self = this;
for (var registerAs in this._registrations) {
if (typeof this._registrations[registerAs] !== 'array') {
continue;
}
if (!registerAs.startsWith('name:')) {
this.logger.info('Check registration(s) for as:' + registerAs);
this.checkRegistration(registerAs);
this.logger.info('...done, Check registration(s) for as:' + registerAs);
} else {
this.logger.info('Check registration(s) for name:' + registerAs);
this.checkRegistration(registerAs);
this.logger.info('...done, Check registration(s) for name:' + registerAs);
}
}
}
/**
* Checks the registration regarding to the given interface.
**/
checkRegistration(registerAs: string) {
var self = this;
var symbols = this._registrations[registerAs];
if (symbols === undefined) {
throw new Error('A type registration for ' + registerAs + ' was not found');
}
// Check constructors
for (var i = 0; i < symbols.length; i++) {
var sym = symbols[i];
var registeredType = sym.getType();
var constructorSignatures = registeredType.getConstructSignatures();
if (constructorSignatures.any()) {
constructorSignatures.forEach((sig: cmp.Signature) => {
sig.getParameters().forEach(param => {
var paramType = param.getType();
if (!self.isRegistered(paramType.getFullName())) {
throw new Error('The type ' + sym.getFullName() + ', registered as ' + registerAs + ' cant get constructed. The parameter named ' + param.getName() + ' needs the type ' + paramType.getFullName() + ' which was not registered on the container');
}
});
});
}
}
}
/**
* Checks if the given interface type is registered for resolving.
**/
isRegistered(registeredAs: string): boolean {
var registrations = this._registrations[registeredAs];
var expr1 = Object.prototype.getClassName.apply(registrations) == 'Array';
var expr2 = Array.prototype.any.apply(registrations);
return expr1 && expr2;
}
/**
* Checks if the given name was registered for resolving.
* @returns {Boolean} True, if you can use the given name with resolveByName, otherwise false.
**/
isNameRegistered(name: string): boolean {
return this.isRegistered('name:' + name);
}
/**
* Registers the type by the given name.
*/
registerNamed(...args: any[]): void {
if (args.length != 2) {
throw new Error('Argument count wrong.');
}
var arg1 = args[0];
var arg2 = args[1];
var registerAs = 'name:' + arg1;
this.registerType(registerAs, arg2);
}
/**
* Registers the type.
*/
registerType(...args: any[]): void {
// symbol: Symbol
if (args.length == 1) {
// Symbol was pushed to here
var symbol: cmp.Symbol = args[0];
// check the @registerType annotations
if (!symbol.hasAnnotation(this.registerTypeAnnotation)) {
throw new Error('Symbol ' + symbol.getFullName() + ' is not annotated with @' + this.registerTypeAnnotation);
}
var annotations = symbol.getAnnotations(this.registerTypeAnnotation);
// Register them now.
annotations.forEach((annotation => {
// register by value is
if (annotation.value.as) {
var named = annotation.value.as;
var namedType = typeof named;
if (namedType) {
this.registerType(named, symbol);
} else if (namedType == 'array') {
(<any[]>named).forEach(registerAs => this.registerType(named, symbol));
} else throw new Error('@registerType on "' + symbol.getFullName() + '" does specify the "as" value as ' + namedType + ' which is not supported, only String and String[] are allowed.');
} else if (annotation.value.name) {
var named = annotation.value.name;
var namedType = typeof named;
if (namedType == 'string') {
this.registerNamed(named, symbol);
} else if (typeof annotation.value.as == 'array') {
(<any[]>named).forEach(registerAs => this.registerNamed(annotation.value.as, symbol));
} else throw new Error('@registerType on "' + symbol.getFullName() + '" does specify the "named" value as ' + namedType + ' which is not supported, only String and String[] are allowed.');
} else {
this.logger.info('@registerAs directive on "' + symbol.getFullName() + '" does not specify the "as" or "name" value, try to registerAs with all interfaces which are implemented');
var implementedInterfaces = symbol.getDeclaredType().getInterfaces();
if (!implementedInterfaces.any()) {
// todo: custom error
throw new Error('Type "' + symbol.getFullName() + '" does not implement any interface and @registerType is without "as" value, however can\' register it by not implementing any interface.');
}
implementedInterfaces.forEach(interf => {
var registerAs = interf.getFullName();
this.registerType(registerAs, symbol)
});
}
}).bind(this));
} else if (args.length == 2) {
var arg1 = args[0];
var arg2 = args[1];
// as: string, className: string
if (typeof arg1 == 'string' && typeof arg2 == 'string') {
var symbol = Container._reflector.getClassByName(arg1);
if (symbol == null) {
throw new Error('Class name "' + arg2 + '" is not a defined type. Can\'t register this.');
}
this.registerType(arg1, symbol);
} else
// as or name: string, symbol: Symbol
if (typeof args[0] == 'string' && typeof args[1] == 'object') {
// presets
this._registrations = (this._registrations || {});
var registerAs = args[0],
isNamed = registerAs.startsWith('name:'),
symbol: cmp.Symbol = args[1];
if (!isNamed) {
var diag = this.diagCanGetRegistered(registerAs, symbol);
if (diag.length > 0) {
// todo: throw custom error
throw new Error('Type "' + symbol.getFullName() + '" cant get registered as "' + registerAs + '": \r\n ' + diag.select(d => d.messageText).join('\r\n '));
}
}
// add registration
if (this._registrations[registerAs] == undefined) {
this._registrations[registerAs] = [];
}
this._registrations[registerAs].push(symbol);
// logging
if (!isNamed) {
this.logger.info(' ...done, registered ' + symbol.getFullName() + ' as ' + arg1);
} else {
this.logger.info(' ...done, registered ' + symbol.getFullName() + ' with name ' + arg1.replace('name:', ''));
}
}
} else {
throw new Error('registerType() was called with wrong arguments');
}
}
private diagCanGetRegistered(registerAs: string, implementation: cmp.Symbol): cmp.Diagnostic[] {
var resultDiag: cmp.Diagnostic[] = [];
// check registration target (as) is an valid interface
var interfaces = implementation
.getDeclaredType()
.getInterfaces()
.select(sym => sym.getFullName());
if (!interfaces.any(registerAs)) {
resultDiag.push(<any>{
messageText: 'Class ' + implementation.getFullName() + ' cant get registered as ' + registerAs + ' because it does not implement the interface defined',
filename: implementation
});
}
return resultDiag;
}
/**
* Initialized the container.
* @param {IContainerConfig} config? Initialized the container with this configuration.
*/
init(config?: IContainerConfig): void {
this.logger.info('Initializing the container...');
var stdConfig = new StandardContainerConfig();
var _config = config || {};
config = extend(true, {}, extend(true, stdConfig, _config));
if (!Container._compiler) {
this.logger.info('Static compiler was not initialized, do that now.')
// re-compile
if (config.compileOnStart === true) {
this.logger.debug('Recompiling sources...');
this.logger.debug('... with configuration: ' + JSON.stringify(config.compilerConfig));
Container._compiler = new compiler.ioc.Compiler();
Container._compiler.compile(config.compilerConfig);
this.logger.info('...done Recompiling sources');
}
}
if (!Container._reflector) {
this.logger.info('Static reflector was not initialized, do that now.')
// reflect compiled stuff
this.logger.info('Initializing reflector...');
Container._reflector = new reflector.ioc.Reflector();
var cfg = config.reflectorConfig || stdConfig.reflectorConfig;
cfg.sourceFiles = cfg.sourceFiles || [];
if (cfg.sourceFiles.length == 0) {
cfg.sourceFiles = helper.getReflectionFileInfos(config.compilerConfig.sourceFiles);
}
this.logger.debug('... with configuration: ' + JSON.stringify(cfg));
Container._reflector.initSymbols(cfg);
this.logger.info('...done Initializing reflector');
// Auto registering
if (config.autoRegister === true) {
this.logger.info('Auto registering components...');
var classes = Container._reflector.classes();
var classesToRegister = classes.where(function (cls) {
return cls.hasAnnotation('registerType');
});
this.logger.debug('...' + classesToRegister.length + ' found for registering, start register now...');
this.logger.info('...done, Auto registering components');
var self = this;
classesToRegister.forEach(function (classInfo: cmp.Symbol) {
var typeInfo = classInfo.getAnnotations(this.registerTypeAnnotation);
this.registerType(classInfo);
}.bind(this));
// last but not least check all registrations if they are okay.
this.checkRegistrations();
}
}
// Set the min root container.
if (!Container.rootContainer) {
Container.rootContainer = this;
}
}
}
}