From 7622be88afdb6d6ee2738a7209734d0d3efe94f8 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Oct 2017 15:10:37 +0300 Subject: [PATCH 1/5] #84_LifeTime for Factories --- README.md | 10 ++++- src/lib/container.ts | 89 +++++++++++++++++++++---------------- src/lib/decorators.ts | 16 +------ src/lib/registry-data.ts | 18 +++++--- src/tests/container.spec.ts | 43 +++++++++++++++--- 5 files changed, 111 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 45cda66..361c7f6 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ 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 **useClass** and **useFactory**. Change it by setting **lifeTime** attribute to **LifeTime.PerRequest**. ```typescript import { LifeTime } from 'container-ioc'; @@ -113,6 +113,14 @@ container.register([ { token: TService, useClass: Service, lifeTime: LifeTime.PerRequest } ]); ``` +```typescript +container.register([ + { + token: TService, + useFactory: () => { serve(): void {} }, + lifeTime: LifeTime.PerRequest } +]); +``` ### Hierarchical containers. > If container can't find value, it will look it up in ascendant containers. diff --git a/src/lib/container.ts b/src/lib/container.ts index b88f373..15959aa 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 { FactoryFunction, IFactory, IRegistryData, RegistryData } from './registry-data'; import { IContainer } from './container.interface'; -import { ClassNotInjectableError, InvalidProviderProvidedError, NoProviderError } from './exceptions'; +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'; @@ -32,14 +32,14 @@ export class Container implements IContainer { return new Container(this); } - public setParent(parent: IContainer): void { - this.parent = parent; - } - public createChild(): IContainer { return new Container(this); } + public setParent(parent: IContainer): void { + this.parent = parent; + } + private resolveInternal(token: ProviderToken, traceMessage?: string): IInjectionInstance { traceMessage = this.buildTraceMessage(token, traceMessage); @@ -56,24 +56,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 +75,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 || LifeTime.Persistent; // TODO issue #88 + } 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 +179,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..086397f 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) {} @@ -308,7 +341,7 @@ describe('Container', () => { describe('createScope()', () => { it('should create child scope', () => { - const childContainer: any = container.createScope(); + const childContainer: any = container.createChild(); expect(childContainer).to.be.ok; expect(childContainer.parent).to.equal(container); }); From 97083476fe4bc6b52e678c2030aa5d8d3846c3f7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Oct 2017 15:12:03 +0300 Subject: [PATCH 2/5] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 361c7f6..287282e 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ class Service { ``` ### Life Time control. -> By default, containers resolve singletons when **useClass** and **useFactory**. Change it by setting **lifeTime** attribute to **LifeTime.PerRequest**. +> By default, containers resolve singletons when using **useClass** and **useFactory**. Change it by setting **lifeTime** attribute to **LifeTime.PerRequest**. ```typescript import { LifeTime } from 'container-ioc'; From a01e64072fc3e4ee0273c0717d368e3a9dfae2b3 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Oct 2017 16:17:32 +0300 Subject: [PATCH 3/5] #88 Default Life time for container. --- README.md | 48 ++++++++++++++++++++++--------- src/lib/container.interface.ts | 7 ++++- src/lib/container.ts | 18 ++++++++---- src/tests/container.spec.ts | 52 ++++++++++++++++++++++++++++++++-- 4 files changed, 103 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 287282e..a108e7f 100644 --- a/README.md +++ b/README.md @@ -104,43 +104,63 @@ class Service { ``` ### Life Time control. -> By default, containers resolve singletons when using **useClass** and **useFactory**. Change it by setting **lifeTime** attribute to **LifeTime.PerRequest**. +> By default, containers resolve singletons when using **useClass** and **useFactory**. You can specify life time individually for each item in a container by setting **lifeTime** attribute to **LifeTime.PerRequest**. ```typescript import { LifeTime } from 'container-ioc'; container.register([ - { token: TService, useClass: Service, lifeTime: LifeTime.PerRequest } + { + token: TService, + useClass: Service, + lifeTime: LifeTime.PerRequest + } ]); ``` ```typescript container.register([ { token: TService, - useFactory: () => { serve(): void {} }, + useFactory: () => { + return { + serve(): void {} + } + }, lifeTime: LifeTime.PerRequest } ]); ``` - -### Hierarchical containers. -> If container can't find value, it will look it up in ascendant containers. +> Or you can set default life time for all items in a container by passing an option object. ```typescript +const container = new Container({ + defaultLifeTime: LifeTime.PerRequest +}); +``` -let parentContainer = new Container(); -let childContainer = parentContainer.createChild(); - -parentContainer.register({ token: TApplication, useClass: Application }); +### Hierarchical containers. +> If 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 15959aa..c94414d 100644 --- a/src/lib/container.ts +++ b/src/lib/container.ts @@ -1,6 +1,6 @@ import { IConstructor, IInjectionInstance, IInjectionMd, IProvider, LifeTime, ProviderToken, RegistrationProvider } from './interfaces'; import { FactoryFunction, IFactory, IRegistryData, RegistryData } from './registry-data'; -import { IContainer } from './container.interface'; +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'; @@ -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,11 +37,11 @@ export class Container implements IContainer { } public createScope(): IContainer { - return new Container(this); + return new Container({ parent: this }); } public createChild(): IContainer { - return new Container(this); + return this.createScope(); } public setParent(parent: IContainer): void { @@ -90,7 +98,7 @@ export class Container implements IContainer { registryData.factory.inject = this.convertTokensToInjectionMd( provider.inject); } - registryData.lifeTime = provider.lifeTime || LifeTime.Persistent; // TODO issue #88 + registryData.lifeTime = provider.lifeTime || this.defaultLifeTime; } this.registry.set(provider.token, registryData); diff --git a/src/tests/container.spec.ts b/src/tests/container.spec.ts index 086397f..324097a 100644 --- a/src/tests/container.spec.ts +++ b/src/tests/container.spec.ts @@ -221,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', () => { @@ -339,11 +360,38 @@ describe('Container', () => { }); - describe('createScope()', () => { - it('should create child scope', () => { + 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 From 463a74190f3b790e3c548dfb16ba619146c63d11 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Oct 2017 16:18:43 +0300 Subject: [PATCH 4/5] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a108e7f..033fdc9 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ const container = new Container({ ``` ### Hierarchical containers. -> If 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. +> 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. ###### 1. Container.createChild() method. ```typescript From 0ce9e500f2734a3f5667730c7a45acc8b651ee9a Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Oct 2017 16:31:12 +0300 Subject: [PATCH 5/5] updated readme --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 033fdc9..9985cdb 100644 --- a/README.md +++ b/README.md @@ -104,11 +104,19 @@ class Service { ``` ### Life Time control. -> By default, containers resolve singletons when using **useClass** and **useFactory**. You can specify life time individually for each item in a container 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, @@ -126,15 +134,10 @@ container.register([ serve(): void {} } }, - lifeTime: LifeTime.PerRequest } + lifeTime: LifeTime.Persistent + } ]); ``` -> Or you can set default life time for all items in a container by passing an option object. -```typescript -const container = new Container({ - defaultLifeTime: LifeTime.PerRequest -}); -``` ### 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.