UNPKG

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
/// <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; } } } }