diff --git a/README.md b/README.md index 45cda66..9985cdb 100644 --- a/README.md +++ b/README.md @@ -104,35 +104,66 @@ class Service { ``` ### Life Time control. -> By default, containers resolve singletons when registering with **useClass**. Change it by setting **lifeTime** attribute to **LifeTime.PerRequest**. +> By default, containers resolve singletons when using **useClass** and **useFactory**. +Default life time for all items in a container can be set by passing an option object to it's contructor with **defailtLifeTime** attribute. Possible values: **LifeTime.PerRequest** (resolves instances) and **LifeTime.Persistent** (resolves singletons); ```typescript import { LifeTime } from 'container-ioc'; +const container = new Container({ + defaultLifeTime: LifeTime.PerRequest +}); +``` +> You can also specify life time individually for each item in a container by specifying **lifeTime** attribute. + +```typescript container.register([ - { token: TService, useClass: Service, lifeTime: LifeTime.PerRequest } + { + token: TService, + useClass: Service, + lifeTime: LifeTime.PerRequest + } ]); ``` - -### Hierarchical containers. -> If container can't find value, it will look it up in ascendant containers. ```typescript +container.register([ + { + token: TService, + useFactory: () => { + return { + serve(): void {} + } + }, + lifeTime: LifeTime.Persistent + } +]); +``` -let parentContainer = new Container(); -let childContainer = parentContainer.createChild(); - -parentContainer.register({ token: TApplication, useClass: Application }); +### Hierarchical containers. +> If a container can't find a value within itself, it will look it up in ascendant containers. There a 3 ways to set a parent for a container. -childContainer.resolve(TApplication); +###### 1. Container.createChild() method. +```typescript +const parentContainer = new Container(); +const childContainer = parentContainer.createChild(); ``` -> You can also assign parent container to any other container + +###### 2. Container.setParent() method. ```typescript -let parent = new Container(); -let child = new Container(); +const parent = new Container(); +const child = new Container(); child.setParent(parent); ``` +###### 3. Via Container's constructor with options. +```typescript +const parent = new Container(); +const child = new Container({ + parent: parent +}); +``` + ### Using Factories ```typescript /* Without injections */ diff --git a/src/lib/container.interface.ts b/src/lib/container.interface.ts index 68230b1..3b97815 100644 --- a/src/lib/container.interface.ts +++ b/src/lib/container.interface.ts @@ -1,4 +1,9 @@ -import { IInjectionInstance, ProviderToken, RegistrationProvider } from './interfaces'; +import { IInjectionInstance, ProviderToken, RegistrationProvider, LifeTime } from './interfaces'; + +export interface IContainerOptions { + parent?: IContainer; + defaultLifeTime?: LifeTime; +} export interface IContainer { register(provider: RegistrationProvider|RegistrationProvider[]): void; diff --git a/src/lib/container.ts b/src/lib/container.ts index b88f373..c94414d 100644 --- a/src/lib/container.ts +++ b/src/lib/container.ts @@ -1,7 +1,7 @@ import { IConstructor, IInjectionInstance, IInjectionMd, IProvider, LifeTime, ProviderToken, RegistrationProvider } from './interfaces'; -import { IRegistryData, RegistryData } from './registry-data'; -import { IContainer } from './container.interface'; -import { ClassNotInjectableError, InvalidProviderProvidedError, NoProviderError } from './exceptions'; +import { FactoryFunction, IFactory, IRegistryData, RegistryData } from './registry-data'; +import { IContainer, IContainerOptions } from './container.interface'; +import { InvalidProviderProvidedError, NoProviderError } from './exceptions'; import { INJECTABLE_MD_KEY, INJECTIONS_MD_KEY } from './metadata/keys'; import { IMetadataAnnotator } from './metadata/metadata-annotator.interface'; import { AnnotatorProvider } from './metadata/index'; @@ -9,9 +9,17 @@ import { AnnotatorProvider } from './metadata/index'; const MetadataAnnotator: IMetadataAnnotator = AnnotatorProvider.get(); export class Container implements IContainer { + private static DEFAULT_LIFE_TIME = LifeTime.Persistent; private registry: Map = new Map(); + private parent: IContainer; + private defaultLifeTime: LifeTime = Container.DEFAULT_LIFE_TIME; - constructor(private parent?: IContainer) {} + constructor(options?: IContainerOptions) { + if (options) { + this.parent = options.parent; + this.defaultLifeTime = options.defaultLifeTime || this.defaultLifeTime; + } + } public register(provider: RegistrationProvider|RegistrationProvider[]): void { provider = this.nornalizeProvider(provider); @@ -29,15 +37,15 @@ export class Container implements IContainer { } public createScope(): IContainer { - return new Container(this); + return new Container({ parent: this }); } - public setParent(parent: IContainer): void { - this.parent = parent; + public createChild(): IContainer { + return this.createScope(); } - public createChild(): IContainer { - return new Container(this); + public setParent(parent: IContainer): void { + this.parent = parent; } private resolveInternal(token: ProviderToken, traceMessage?: string): IInjectionInstance { @@ -56,24 +64,7 @@ export class Container implements IContainer { return registryData.instance; } - if (registryData.factory) { - let injections: ProviderToken[] = []; - - if (registryData.injections) { - injections = registryData.injections.map(i => this.resolveInternal(i, traceMessage)); - } - - return registryData.factory(...injections); - } - - const constructor: IConstructor = registryData.cls; - - const isInjectable: boolean = this.isInjectable(constructor); - if (!isInjectable) { - throw new ClassNotInjectableError(constructor.name); - } - - const instance: IInjectionInstance = this.createInstance(constructor, traceMessage); + const instance: IInjectionInstance = this.instantiateWithFactory(registryData.factory, traceMessage); if (registryData.lifeTime === LifeTime.Persistent) { registryData.instance = instance; @@ -92,28 +83,57 @@ export class Container implements IContainer { if (provider.useValue) { registryData.instance = provider.useValue; - } else if (provider.useClass) { - registryData.cls = provider.useClass; - } else if (provider.useFactory) { - registryData.factory = provider.useFactory; - registryData.injections = provider.inject; - } + } else { + const factoryValue = provider.useFactory || provider.useClass; + const isClass: boolean = this.isInjectable(factoryValue); + + registryData.factory = { + value: factoryValue, + isClass + }; + + if (isClass) { + registryData.factory.inject = this.retrieveInjectionsFromClass( registryData.factory.value); + } else { + registryData.factory.inject = this.convertTokensToInjectionMd( provider.inject); + } - registryData.lifeTime = provider.lifeTime || LifeTime.Persistent; + registryData.lifeTime = provider.lifeTime || this.defaultLifeTime; + } this.registry.set(provider.token, registryData); } - private createInstance(cls: IConstructor, message: string): IInjectionInstance { - const injectionsMd: IInjectionMd[] = this.getInjections(cls); - const resolvedInjections: any[] = injectionsMd.map(injectionMd => this.resolveInternal(injectionMd.token, message)); + private convertTokensToInjectionMd(tokens: ProviderToken[]): IInjectionMd[] { + let injections: IInjectionMd[] = []; + + if (tokens) { + injections = tokens.map((token: ProviderToken, index: number) => { + return { + token, + parameterIndex: index + }; + }); + } + + return injections; + } + + private instantiateWithFactory(factory: IFactory, traceMessage: string): IInjectionInstance { + const injections = factory.inject; + + const resolvedInjections: any[] = injections.map(injection => this.resolveInternal(injection.token, traceMessage)); const args: any[] = []; - injectionsMd.forEach((injection: IInjectionMd, index) => { + injections.forEach((injection: IInjectionMd, index: number) => { args[injection.parameterIndex] = resolvedInjections[index]; }); - return new cls(...args); + if (factory.isClass) { + return new ( factory.value)(...args); + } else { + return ( factory.value)(...args); + } } private nornalizeProvider(provider: RegistrationProvider|RegistrationProvider[]): IProvider { @@ -167,7 +187,8 @@ export class Container implements IContainer { return !!(MetadataAnnotator.getMetadata(INJECTABLE_MD_KEY, cls)); } - private getInjections(cls: any): IInjectionMd[] { - return MetadataAnnotator.getMetadata(INJECTIONS_MD_KEY, cls) || []; + private retrieveInjectionsFromClass(cls: IConstructor): IInjectionMd[] { + const injections: IInjectionMd[] = MetadataAnnotator.getMetadata(INJECTIONS_MD_KEY, cls) || []; + return this.convertTokensToInjectionMd(injections); } } \ No newline at end of file diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index 0db8040..6d06300 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -11,14 +11,7 @@ export function Injectable(injections?: ProviderToken[]) { if (injections && Array.isArray(injections)) { const injectionMd: IInjectionMd[] = MetadataAnnotator.getMetadata(INJECTIONS_MD_KEY, target) || []; - - injections.forEach((injectionToken, injectionIndex) => { - injectionMd.push({ - token: injectionToken, - parameterIndex: injectionIndex - }); - }); - + injections.forEach(token => injectionMd.push(token)); MetadataAnnotator.defineMetadata(INJECTIONS_MD_KEY, injectionMd, target); } }; @@ -27,12 +20,7 @@ export function Injectable(injections?: ProviderToken[]) { export function Inject(token: any) { return (target: object, propertyKey: string | symbol, parameterIndex: number) => { const injections: IInjectionMd[] = MetadataAnnotator.getMetadata(INJECTIONS_MD_KEY, target) || []; - - injections.push({ - token, - parameterIndex - }); - + injections.push(token); MetadataAnnotator.defineMetadata(INJECTIONS_MD_KEY, injections, target); }; } \ No newline at end of file diff --git a/src/lib/registry-data.ts b/src/lib/registry-data.ts index 869ef4a..cca7e59 100644 --- a/src/lib/registry-data.ts +++ b/src/lib/registry-data.ts @@ -1,17 +1,21 @@ -import { IConstructor, IInjectionInstance, LifeTime, ProviderToken } from './interfaces'; +import { IConstructor, IInjectionInstance, IInjectionMd, LifeTime } from './interfaces'; + +export type FactoryFunction = (...args: any[]) => any; + +export interface IFactory { + value: IConstructor | FactoryFunction; + isClass: boolean; + inject?: IInjectionMd[]; +} export interface IRegistryData { instance: IInjectionInstance; - cls: IConstructor; - factory: (...args: any[]) => any; - injections: ProviderToken[]; + factory: IFactory; lifeTime: LifeTime; } export class RegistryData { public instance: IInjectionInstance; - public cls: IConstructor; - public factory: (...args: any[]) => any; - public injections: ProviderToken[]; + public factory: IFactory; public lifeTime: LifeTime; } \ No newline at end of file diff --git a/src/tests/container.spec.ts b/src/tests/container.spec.ts index 9054a65..324097a 100644 --- a/src/tests/container.spec.ts +++ b/src/tests/container.spec.ts @@ -91,7 +91,7 @@ describe('Container', () => { expect(instance instanceof TestClass).to.be.true; }); - it('should resolve ф value when registered with "useFactory"', () => { + it('should resolve value when registered with "useFactory"', () => { container.register({ token: 'V', useFactory: () => { @@ -144,7 +144,7 @@ describe('Container', () => { }); describe('LifeTime', () => { - it('should resolve a singleton instance if LifeTime was not specified', () => { + it('should resolve a singleton by default if LifeTime was not specified with useClass', () => { @Injectable() class A {} @@ -156,7 +156,40 @@ describe('Container', () => { expect(instance1).to.be.equal(instance2); }); - it('should resolve a different instances if LifeTime was set to LifeTime.PerRequest', () => { + it('should resolve a singleton by default if LifeTime was not specified with useFactory', () => { + class A {} + + container.register([ + { + token: A, + useFactory: () => new A() + } + ]); + + const instance1 = container.resolve(A); + const instance2 = container.resolve(A); + + expect(instance1).to.be.equal(instance2); + }); + + it('should resolve an instance with useFactory if LifeTime was set to LifeTime.PerRequest', () => { + class A {} + + container.register([ + { + token: A, + useFactory: () => new A(), + lifeTime: LifeTime.PerRequest + } + ]); + + const instance1 = container.resolve(A); + const instance2 = container.resolve(A); + + expect(instance1).not.to.be.equal(instance2); + }); + + it('should resolve an instances if LifeTime was set to LifeTime.PerRequest', () => { @Injectable() class A {} @@ -168,7 +201,7 @@ describe('Container', () => { expect(instance1).not.to.be.equal(instance2); }); - it('should resolve a different instances if LifeTime was set to LifeTime.PerRequest in case of nested dependencies', () => { + it('should resolve different instances if LifeTime was set to LifeTime.PerRequest in case of nested dependencies', () => { @Injectable() class A { constructor(@Inject('IB') private b: any) {} @@ -188,6 +221,27 @@ describe('Container', () => { expect(instance1).not.to.be.equal(instance2); expect(instance1.b).to.be.equal(instance2.b); }); + + it('should set default lifeTime via options', () => { + const cont = new Container({ + defaultLifeTime: LifeTime.PerRequest + }); + + @Injectable() + class A {} + + cont.register({ token: A, useClass: A }); + let instance1 = cont.resolve(A); + let instance2 = cont.resolve(A); + + expect(instance1).not.to.be.equal(instance2); + + cont.register({ token: 'IB', useFactory: () => ({}) }); + instance1 = cont.resolve('IB'); + instance2 = cont.resolve('IB'); + + expect(instance1).not.to.be.equal(instance2); + }); }); describe('Errors', () => { @@ -306,11 +360,38 @@ describe('Container', () => { }); - describe('createScope()', () => { - it('should create child scope', () => { - const childContainer: any = container.createScope(); + describe('Constructor', () => { + it('should set defaultLife through option object', () => { + const cont = new Container({ defaultLifeTime: LifeTime.PerRequest }); + + cont.register({ token: 'A', useFactory: () => ({})}); + + const instance1 = cont.resolve('A'); + const instance2 = cont.resolve('A'); + + expect(instance1).not.to.be.equal(instance2); + }); + }); + + describe('createChild()', () => { + it('should create child container', () => { + const childContainer: any = container.createChild(); expect(childContainer).to.be.ok; expect(childContainer.parent).to.equal(container); }); }); + + describe('setParent()', () => { + it('should set parent for a container', () => { + const parentContainer = new Container(); + const childContainer = new Container(); + childContainer.setParent(parentContainer); + + parentContainer.register({ token: 'A', useValue: 'string' }); + + const value = childContainer.resolve('A'); + + expect(value).to.be.equal('string'); + }); + }); }); \ No newline at end of file