From aaf0f8c28777c14cffc3e3ea32d8c45c43d41846 Mon Sep 17 00:00:00 2001 From: Danylo Hrudzynskyi Date: Tue, 9 Feb 2021 18:12:14 +0200 Subject: [PATCH] allow parametrized resolution of services through getParametrized method --- src/container/container-binding-config.ts | 18 +++- src/decorators.ts | 4 +- src/model.ts | 4 + src/typescript-ioc.ts | 28 +++++ test/integration/ioc-container-tests.spec.ts | 108 +++++++++++++++++++ test/unit/decorators.spec.ts | 28 +++++ 6 files changed, 186 insertions(+), 4 deletions(-) diff --git a/src/container/container-binding-config.ts b/src/container/container-binding-config.ts index 12d6ec1..43fd226 100644 --- a/src/container/container-binding-config.ts +++ b/src/container/container-binding-config.ts @@ -27,9 +27,23 @@ export class IoCBindConfig implements Config { this.targetSource = targetSource; if (this.source === targetSource) { this.factory((context) => { - const params = this.getParameters(context); + const requestParameters = context && context.getRequestSpecificParameters(); + if(requestParameters) { + // reset request specific parameters before resolving other parameters + // in order to avoid propagation of request-specific params to dependencies + context.resetRequestSpecificParameters(); + } + + const params = this.getParameters(context) || []; + + if(requestParameters) { + for(let i = 0; i < requestParameters.length; i++) { + params[i] = requestParameters[i]; + } + } + const constructor = this.decoratedConstructor || target; - return (params ? new constructor(...params) : new constructor()); + return (params.length ? new constructor(...params) : new constructor()); }); } else { this.factory((context) => { diff --git a/src/decorators.ts b/src/decorators.ts index df1dab6..f02f50e 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -215,7 +215,7 @@ function InjectParamDecorator(target: Function, propertyKey: string | symbol, pa const config = IoCContainer.bind(target); config.paramTypes = config.paramTypes || []; const paramTypes: Array = Reflect.getMetadata('design:paramtypes', target); - config.paramTypes.unshift(paramTypes[parameterIndex]); + config.paramTypes[parameterIndex] = paramTypes[parameterIndex]; } } @@ -233,6 +233,6 @@ function InjectValueParamDecorator(target: Function, propertyKey: string | symbo if (!propertyKey) { // only intercept constructor parameters const config = IoCContainer.bind(target); config.paramTypes = config.paramTypes || []; - config.paramTypes.unshift(value); + config.paramTypes[_parameterIndex] = value; } } \ No newline at end of file diff --git a/src/model.ts b/src/model.ts index 6ee62f9..8f5d806 100644 --- a/src/model.ts +++ b/src/model.ts @@ -61,6 +61,10 @@ export type ObjectFactory = (context?: BuildContext) => Object; * The context of the current Container resolution. */ export abstract class BuildContext { + public resetRequestSpecificParameters() { + // do nothing. default implementation does not perform any action + } + public getRequestSpecificParameters() { return []; } public abstract resolve(source: Function & { prototype: T }): T; public abstract build(source: Function & { prototype: T }, factory: ObjectFactory): T; } diff --git a/src/typescript-ioc.ts b/src/typescript-ioc.ts index ef939ee..fde0ddc 100644 --- a/src/typescript-ioc.ts +++ b/src/typescript-ioc.ts @@ -56,6 +56,20 @@ export class Container { return IoCContainer.get(source, new ContainerBuildContext()); } + /** + * Retrieve an object from the container. It will resolve all dependencies and apply any type replacement + * before return the object. + * If there is no declared dependency to the given source type, an implicity bind is performed to this type. + * if any number of items is provided in the 'params' argument - these items will be used as constructor arguments + * taking over the potential bindings + * @param source The dependency type to resolve + * @param contextParams list of X parameters, which will be passed as first X constructor arguments + * @return an object resolved for the given source type; + */ + public static getParametrized(source: Function & { prototype: T }, ...contextParams: Array): T { + return IoCContainer.get(source, new ParametrizedBuildContext(contextParams)); + } + /** * Retrieve a type associated with the type provided from the container * @param source The dependency type to resolve @@ -178,4 +192,18 @@ class ContainerBuildContext extends BuildContext { public resolve(source: Function & { prototype: T }): T { return IoCContainer.get(source, this); } +} + +export class ParametrizedBuildContext extends ContainerBuildContext { + constructor(private contextParams: Array) { + super(); + } + + public getRequestSpecificParameters() { + return this.contextParams; + } + + public resetRequestSpecificParameters() { + this.contextParams = []; + } } \ No newline at end of file diff --git a/test/integration/ioc-container-tests.spec.ts b/test/integration/ioc-container-tests.spec.ts index cae2afd..3b204ed 100644 --- a/test/integration/ioc-container-tests.spec.ts +++ b/test/integration/ioc-container-tests.spec.ts @@ -671,4 +671,112 @@ describe('The IoC Container Config.withParams()', () => { const instance: WithParamClass = Container.get(WithParamClass); expect(instance.date).toBeDefined(); }); +}); + +describe('shoould handle parametrized requests', () => { + let constructorsMultipleArgs: Array = new Array(); + + beforeEach(() => { + constructorsMultipleArgs = []; + }); + + class SingleDynamicArgConstructor { + public injectedString: string; + constructor(stringArg: string) { + this.injectedString = stringArg; + } + } + + it('should let single argument be defined in runtime', () => { + const instance = Container.getParametrized(SingleDynamicArgConstructor, 'str1'); + expect(instance.injectedString).toEqual('str1'); + }); + + class SingleInjectedArgConstructor { + public injectedDate: Date; + constructor(@Inject date: Date) { + this.injectedDate = date; + } + } + + it('should let single argument be defined in runtime and override injected value if any', () => { + const customDate = new Date(); + const instance = Container.getParametrized(SingleInjectedArgConstructor, customDate); + expect(instance.injectedDate).toEqual(customDate); + }); + + class Aaaa { } + class Bbbb { } + class Cccc { } + + class Dddd { + constructor(@Inject a: Aaaa, @Inject b: Bbbb, @Inject c: Cccc) { + constructorsMultipleArgs.push(a); + constructorsMultipleArgs.push(b); + constructorsMultipleArgs.push(c); + } + } + + it('should let single resolution parameter be passed as a first argument. Must override injected value while resolving multiple', () => { + const aaaaInstance = new Aaaa(); + + const instance = Container.getParametrized(Dddd, aaaaInstance); + expect(instance).toBeDefined(); + expect(constructorsMultipleArgs[0]).toBeDefined(); + expect(constructorsMultipleArgs[1]).toBeDefined(); + expect(constructorsMultipleArgs[2]).toBeDefined(); + expect(constructorsMultipleArgs[0]).toEqual(aaaaInstance); + expect(constructorsMultipleArgs[1]).toBeInstanceOf(Bbbb); + expect(constructorsMultipleArgs[2]).toBeInstanceOf(Cccc); + }); + + + it('should let multiple resolution parameters be passed as a first arguments. Must override injected values while resolving multiple', () => { + const aaaaInstance = new Aaaa(); + const bbbbInstance = new Bbbb(); + + const instance = Container.getParametrized(Dddd, aaaaInstance, bbbbInstance); + expect(instance).toBeDefined(); + expect(constructorsMultipleArgs[0]).toEqual(aaaaInstance); + expect(constructorsMultipleArgs[1]).toEqual(bbbbInstance); + expect(constructorsMultipleArgs[2]).toBeInstanceOf(Cccc); + }); + + class DdddWithExpectedResolutionParam { + constructor(a: Aaaa, @Inject b: Bbbb, @Inject c: Cccc) { + constructorsMultipleArgs.push(a); + constructorsMultipleArgs.push(b); + constructorsMultipleArgs.push(c); + } + } + + it('should let single resolution parameter be passed as a first argument if not injectable', () => { + const aaaaInstance = new Aaaa(); + + const instance = Container.getParametrized(DdddWithExpectedResolutionParam, aaaaInstance); + expect(instance).toBeDefined(); + expect(constructorsMultipleArgs[0]).toEqual(aaaaInstance); + expect(constructorsMultipleArgs[1]).toBeInstanceOf(Bbbb); + expect(constructorsMultipleArgs[2]).toBeInstanceOf(Cccc); + }); + + class ClassWhichIsDependency { + constructor(public paramDep: string){ + } + } + + class ClassWhichHasDependency { + constructor(public paramDep: string, @Inject public classDep: ClassWhichIsDependency){ + } + } + + it('must not pass dynamic resolution parameters to any instances other than the root one', () => { + const dynamicResolutionParameter = 'theOne'; + + const instance = Container.getParametrized(ClassWhichHasDependency, dynamicResolutionParameter); + expect(instance).toBeDefined(); + expect(instance.paramDep).toEqual('theOne'); + expect(instance.classDep).toBeInstanceOf(ClassWhichIsDependency); + expect(instance.classDep.paramDep).toEqual(undefined); + }); }); \ No newline at end of file diff --git a/test/unit/decorators.spec.ts b/test/unit/decorators.spec.ts index 31c9f89..4bec205 100644 --- a/test/unit/decorators.spec.ts +++ b/test/unit/decorators.spec.ts @@ -54,6 +54,20 @@ describe('@Inject decorator', () => { expect(testFunction).toThrow(new TypeError('Invalid @Inject Decorator declaration.')); }); + + it('should inject into proper arguments despite previous arguments are not decorated', () => { + const config: any = {}; + mockBind.mockReturnValue(config); + + class ConstructorInjected { + constructor(public anotherDate: Date, + @Inject public myProp: String) { + } + } + expect(mockBind).toBeCalledWith(ConstructorInjected); + expect(config.paramTypes[0]).toEqual(undefined); + expect(config.paramTypes[1]).toEqual(String); + }); }); const mockInjectValueProperty = IoCContainer.injectValueProperty as jest.Mock; @@ -107,6 +121,20 @@ describe('@InjectValue decorator', () => { expect(testFunction).toThrow(new TypeError('Invalid @InjectValue Decorator declaration.')); }); + + it('should inject into proper arguments despite previous arguments are not decorated', () => { + const config: any = {}; + mockBind.mockReturnValue(config); + + class ConstructorInjected { + constructor(public anotherDate: Date, + @InjectValue('myString') public myProp: String) { + } + } + expect(mockBind).toBeCalledWith(ConstructorInjected); + expect(config.paramTypes[0]).toEqual(undefined); + expect(config.paramTypes[1]).toEqual('myString'); + }); }); const mockTo = jest.fn();