From 75ede6e957cbdf63b77a70e8b37c4e8ac474f2c9 Mon Sep 17 00:00:00 2001 From: Alexey Vykhrystyuk Date: Fri, 4 Jun 2021 00:10:10 +0300 Subject: [PATCH 1/2] feat: impl and test container.useValue --- src/impl/container-impl.spec.ts | 83 +++++++++++++++++++++++++++++++++ src/impl/container-impl.ts | 78 ++++++++++++++++++++++++++++--- src/interfaces/constructor.ts | 29 ++++++++++++ src/interfaces/container.ts | 66 +++++++++++++++++++++++++- src/interfaces/providers.ts | 82 +++++++++++++++++++++++++++++--- 5 files changed, 322 insertions(+), 16 deletions(-) diff --git a/src/impl/container-impl.spec.ts b/src/impl/container-impl.spec.ts index 5a7b390..4048458 100644 --- a/src/impl/container-impl.spec.ts +++ b/src/impl/container-impl.spec.ts @@ -7,6 +7,7 @@ import { createModernFactoryRegistry } from './create-modern-factory-registry'; import { Cat, Dog, Fish, Meat, Milk, stringToken, symbolToken } from './container-types.spec'; describe('ContainerImpl', () => { + // TODO: use legacyFactoryRegistry describe('with ModernFactoryRegistry', () => { let container: Container; @@ -18,6 +19,88 @@ describe('ContainerImpl', () => { assert.throws(() => container.resolve(Cat), DependencyInjectionError, 'Error is not being thrown'); }); + describe('useClass', () => { + it('class token', () => { + container + .useClass({ for: Fish, use: Fish }) + .useClass({ for: Milk, use: Milk }) + .useClass({ for: Cat, use: Cat, inject: [Fish, Milk] }); + + assert.ok(container.resolve(Cat).isCat, "Cat wasn't resolved"); + }); + + it('symbol token', () => { + const { fish, milk, cat } = symbolToken; + + container + .useClass({ for: fish, use: Fish }) + .useClass({ for: milk, use: Milk }) + .useClass({ for: cat, use: Cat, inject: [fish, milk] }); + + assert.ok(container.resolve(cat).isCat, "Cat wasn't resolved"); + }); + + it('string token', () => { + const { fish, milk, cat } = stringToken; + + container + .useClass({ for: fish, use: Fish }) + .useClass({ for: milk, use: Milk }) + .useClass({ for: cat, use: Cat, inject: [fish, milk] }); + + assert.ok(container.resolve(cat).isCat, "Cat wasn't resolved"); + }); + + it('regular - not a singleton', () => { + container + .useClass({ for: Fish, use: Fish }) + .useClass({ for: Milk, use: Milk }) + .useClass({ for: Cat, use: Cat, inject: [Fish, Milk] }); + + assert.notStrictEqual(container.resolve(Fish), container.resolve(Fish)); + assert.notStrictEqual(container.resolve(Milk), container.resolve(Milk)); + assert.notStrictEqual(container.resolve(Cat), container.resolve(Cat)); + }); + + it('singleton', () => { + container + .useClass({ for: Fish, use: Fish }) + .useClass({ for: Milk, use: Milk, singleton: true }) + .useClass({ for: Cat, use: Cat, inject: [Fish, Milk], singleton: true }); + + assert.notStrictEqual(container.resolve(Fish), container.resolve(Fish)); + assert.strictEqual(container.resolve(Milk), container.resolve(Milk)); + assert.strictEqual(container.resolve(Cat), container.resolve(Cat)); + assert.strictEqual(container.resolve(Cat).milk, container.resolve(Milk)); + }); + + it('error path - error during resolve', () => { + const thrownError = new Error('Ops!'); + container + .useClass({ + for: Meat, + use: class ThrowableMeat extends Meat { + constructor() { + throw thrownError; + super(); + } + }, + }) + .useClass({ for: Dog, use: Dog, inject: [Meat] }); + + assert.throws( + () => container.resolve(Meat), + (err: DependencyInjectionError) => err.innerError === thrownError, + 'Error is not being thrown' + ); + assert.throws( + () => container.resolve(Dog), + (err: DependencyInjectionError) => (err.innerError as DependencyInjectionError).innerError === thrownError, + 'Error is not being thrown' + ); + }); + }); + describe('useFactory', () => { it('class token', () => { container diff --git a/src/impl/container-impl.ts b/src/impl/container-impl.ts index 2ec808a..8d85d9f 100644 --- a/src/impl/container-impl.ts +++ b/src/impl/container-impl.ts @@ -1,18 +1,27 @@ import { Container, - Resolver, ResolveFactoryFunction, FactoryRegistry, DependencyInjectionError, Token, + Constructor, Constructor1, Constructor2, Constructor3, + Constructor4, + Constructor5, + Constructor6, + Constructor7, ClassProvider1, ClassProvider2, ClassProvider3, + ClassProvider4, + ClassProvider5, + ClassProvider6, + ClassProvider7, ResolveProvider, ValueProvider, + ClassBaseProvider, } from '../interfaces'; import { memoize } from './utils'; @@ -23,9 +32,7 @@ export class ContainerImpl extends Container { } public useFactory(provider: ResolveProvider): Container { - const factory: ResolveFactoryFunction = provider.singleton ? memoize(provider.use) : provider.use; - - this.register(provider.for, factory); + this.register(provider.for, provider.use, provider.singleton); return this; } @@ -41,7 +48,62 @@ export class ContainerImpl extends Container { provider: ClassProvider3 ): Container; - public useClass(provider: any): Container { + public useClass< + T, + TCtor extends Constructor4, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4 + >(provider: ClassProvider4): Container; + + public useClass< + T, + TCtor extends Constructor5, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5 + >(provider: ClassProvider5): Container; + + public useClass< + T, + TCtor extends Constructor6, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5, + TCtorArg6 + >(provider: ClassProvider6): Container; + + public useClass< + T, + TCtor extends Constructor7, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5, + TCtorArg6, + TCtorArg7 + >( + provider: ClassProvider7 + ): Container; + + public useClass(provider: ClassBaseProvider>): Container { + this.register( + provider.for, + () => { + const Ctor = provider.use; + const depTokens = provider.inject ?? []; + const deps = depTokens.map((token) => this.resolve(token)); + return new Ctor(...deps); + }, + provider.singleton + ); + return this; } @@ -49,13 +111,15 @@ export class ContainerImpl extends Container { super(); } - private register(token: Token, factory: ResolveFactoryFunction): this { + private register(token: Token, factory: ResolveFactoryFunction, singleton?: boolean): this { + const finalFactory = singleton ? memoize(factory) : factory; + if (this.factoryRegistry.hasFactory(token)) { const displayToken = this.getTokenDisplayName(token); throw new DependencyInjectionError(`Factory is already registered for '${displayToken}'`); } - this.factoryRegistry.setFactory(token, factory); + this.factoryRegistry.setFactory(token, finalFactory); return this; } diff --git a/src/interfaces/constructor.ts b/src/interfaces/constructor.ts index 2ba721c..9886ba1 100644 --- a/src/interfaces/constructor.ts +++ b/src/interfaces/constructor.ts @@ -8,3 +8,32 @@ export type Constructor1 = new (arg1: TArg1) => T; export type Constructor2 = new (arg1: TArg1, arg2: TArg2) => T; export type Constructor3 = new (arg1: TArg1, arg2: TArg2, arg3: TArg3) => T; + +export type Constructor4 = new (arg1: TArg1, arg2: TArg2, arg3: TArg3, arg4: TArg4) => T; + +export type Constructor5 = new ( + arg1: TArg1, + arg2: TArg2, + arg3: TArg3, + arg4: TArg4, + arg5: TArg5 +) => T; + +export type Constructor6 = new ( + arg1: TArg1, + arg2: TArg2, + arg3: TArg3, + arg4: TArg4, + arg5: TArg5, + arg6: TArg6 +) => T; + +export type Constructor7 = new ( + arg1: TArg1, + arg2: TArg2, + arg3: TArg3, + arg4: TArg4, + arg5: TArg5, + arg6: TArg6, + arg7: TArg7 +) => T; diff --git a/src/interfaces/container.ts b/src/interfaces/container.ts index 0078ef4..29dcccf 100644 --- a/src/interfaces/container.ts +++ b/src/interfaces/container.ts @@ -1,6 +1,24 @@ -import { Constructor1, Constructor2, Constructor3 } from './constructor'; +import { + Constructor1, + Constructor2, + Constructor3, + Constructor4, + Constructor5, + Constructor6, + Constructor7, +} from './constructor'; import { Resolver } from './resolver'; -import { ClassProvider1, ClassProvider2, ClassProvider3, ResolveProvider, ValueProvider } from './providers'; +import { + ClassProvider1, + ClassProvider2, + ClassProvider3, + ClassProvider4, + ClassProvider5, + ClassProvider6, + ClassProvider7, + ResolveProvider, + ValueProvider, +} from './providers'; export abstract class Container extends Resolver { public abstract useFactory(provider: ResolveProvider): Container; @@ -22,6 +40,50 @@ export abstract class Container extends Resolver { TCtorArg3 >(provider: ClassProvider3): Container; + public abstract useClass< + T, + TCtor extends Constructor4, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4 + >(provider: ClassProvider4): Container; + + public abstract useClass< + T, + TCtor extends Constructor5, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5 + >(provider: ClassProvider5): Container; + + public abstract useClass< + T, + TCtor extends Constructor6, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5, + TCtorArg6 + >(provider: ClassProvider6): Container; + + public abstract useClass< + T, + TCtor extends Constructor7, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5, + TCtorArg6, + TCtorArg7 + >( + provider: ClassProvider7 + ): Container; + public abstract tryVerifyAll(): boolean; /** diff --git a/src/interfaces/providers.ts b/src/interfaces/providers.ts index 25f5cc0..757513c 100644 --- a/src/interfaces/providers.ts +++ b/src/interfaces/providers.ts @@ -1,4 +1,13 @@ -import { Constructor1, Constructor2, Constructor3 } from './constructor'; +import { + Constructor, + Constructor1, + Constructor2, + Constructor3, + Constructor4, + Constructor5, + Constructor6, + Constructor7, +} from './constructor'; import { Token } from './tokens'; import { ResolveFactoryFunction } from './resolve-factory-function'; @@ -17,16 +26,18 @@ export interface ValueProvider extends BaseProvider { export interface ResolveProvider extends SingletonBaseProvider { use: ResolveFactoryFunction; } +export interface ClassBaseProvider> extends SingletonBaseProvider { + use: TCtor; + inject?: Token[]; +} export interface ClassProvider1, TCtorArg1> - extends SingletonBaseProvider { - use: TCtor; + extends ClassBaseProvider { inject?: [Token]; } export interface ClassProvider2, TCtorArg1, TCtorArg2> - extends BaseProvider { - use: TCtor; + extends ClassBaseProvider { inject?: [Token, Token]; } @@ -36,7 +47,64 @@ export interface ClassProvider3< TCtorArg1, TCtorArg2, TCtorArg3 -> extends BaseProvider { - use: TCtor; +> extends ClassBaseProvider { inject?: [Token, Token, Token]; } + +export interface ClassProvider4< + T, + TCtor extends Constructor4, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4 +> extends ClassBaseProvider { + inject?: [Token, Token, Token, Token]; +} + +export interface ClassProvider5< + T, + TCtor extends Constructor5, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5 +> extends ClassBaseProvider { + inject?: [Token, Token, Token, Token, Token]; +} + +export interface ClassProvider6< + T, + TCtor extends Constructor6, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5, + TCtorArg6 +> extends ClassBaseProvider { + inject?: [Token, Token, Token, Token, Token, Token]; +} + +export interface ClassProvider7< + T, + TCtor extends Constructor7, + TCtorArg1, + TCtorArg2, + TCtorArg3, + TCtorArg4, + TCtorArg5, + TCtorArg6, + TCtorArg7 +> extends ClassBaseProvider { + inject?: [ + Token, + Token, + Token, + Token, + Token, + Token, + Token + ]; +} From feeee9bb3f06caf92121508a65bf0cee80febd7e Mon Sep 17 00:00:00 2001 From: Alexey Vykhrystyuk Date: Fri, 4 Jun 2021 00:22:52 +0300 Subject: [PATCH 2/2] refactor: memoize factory after guard --- src/impl/container-impl.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/impl/container-impl.ts b/src/impl/container-impl.ts index 8d85d9f..b436961 100644 --- a/src/impl/container-impl.ts +++ b/src/impl/container-impl.ts @@ -112,13 +112,12 @@ export class ContainerImpl extends Container { } private register(token: Token, factory: ResolveFactoryFunction, singleton?: boolean): this { - const finalFactory = singleton ? memoize(factory) : factory; - if (this.factoryRegistry.hasFactory(token)) { const displayToken = this.getTokenDisplayName(token); throw new DependencyInjectionError(`Factory is already registered for '${displayToken}'`); } + const finalFactory = singleton ? memoize(factory) : factory; this.factoryRegistry.setFactory(token, finalFactory); return this; }