From c23ac1de981555689aa28f65a9f6ee57370ecae5 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Tue, 11 Mar 2025 17:43:32 +0100 Subject: [PATCH 1/5] refactor: consolidate registration decorators - Introduced `Register` decorator to handle class and instance registration in the DI container. - Deprecated `RegisterInstance` in favor of `Register`, which now internally handles instance registration. - Added support for marking dependencies as deprecated with a warning logged upon first resolution. - Updated documentation with examples and notes on deprecation. --- src/decorators/Register.ts | 232 +++++++++++++++++++++++++++++ src/decorators/RegisterInstance.ts | 52 ++----- 2 files changed, 241 insertions(+), 43 deletions(-) diff --git a/src/decorators/Register.ts b/src/decorators/Register.ts index fa65076..b733e89 100644 --- a/src/decorators/Register.ts +++ b/src/decorators/Register.ts @@ -1,3 +1,4 @@ +import { InitDelegate } from 'src/types/InitDelegate'; import { TSinjex } from '../classes/TSinjex'; import { Identifier } from '../types/Identifier'; @@ -16,9 +17,132 @@ import { Identifier } from '../types/Identifier'; * // ... * } * ``` + * @example + * ```ts + * \@Register('MyClassIdentifier', true) + * class MyClass { + * // ... + * } + * ``` */ export function Register< TargetType extends new (...args: unknown[]) => InstanceType, +>( + identifier: Identifier, + deprecated?: boolean, +): (constructor: TargetType, ...args: unknown[]) => void; + +/** + * A decorator to register an instance of a class in the DI (Dependency Injection) container. + * @template TargetType The type of the class whose instance is to be registered. + * @param identifier The identifier used to register the instance in the DI container. + * @see {@link Identifier} for more information on identifiers. + * @param shouldRegister Set to 'instance' to register the instance in the DI container + * with an empty constructor. + * @param deprecated If true, the dependency is deprecated and a warning + * is logged only once upon the first resolution of the dependency. + * @returns The decorator function to be applied on the class. + * @example + * ```ts + * \@RegisterInstance('MyClassInstanceIdentifier', 'instance') + * class MyClass { + * // ... + * } + * ``` + * @example + * ```ts + * \@RegisterInstance('MyClassInstanceIdentifier', 'instance', true) + * class MyClass { + * // ... + * } + * ``` + */ +export function Register< + TargetType extends new (..._args: unknown[]) => InstanceType, +>( + identifier: Identifier, + shouldRegister: 'instance', + deprecated?: boolean, +): (constructor: TargetType, ...args: unknown[]) => void; + +/** + * A decorator to register an instance of a class in the DI (Dependency Injection) container. + * @template TargetType The type of the class whose instance is to be registered. + * @param identifier The identifier used to register the instance in the DI container. + * @see {@link Identifier} for more information on identifiers. + * @param init An optional initializer function which get the constructor of the class + * as input and returns an instance of the class. + * @param deprecated If true, the dependency is deprecated and a warning + * is logged only once upon the first resolution of the dependency. + * @see {@link InitDelegate} for more information on initializer functions. + * @returns The decorator function to be applied on the class. + * @example + * ```ts + * \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor()) + * class MyClass { + * // ... + * } + * ``` + * @example + * ```ts + * \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor(), true) + * class MyClass { + * // ... + * } + * ``` + */ +export function Register< + TargetType extends new (..._args: unknown[]) => InstanceType, +>( + identifier: Identifier, + init?: InitDelegate< + TargetType & { new (..._args: unknown[]): InstanceType }, + InstanceType + >, + deprecated?: boolean, +): (constructor: TargetType, ...args: unknown[]) => void; + +// eslint-disable-next-line jsdoc/require-jsdoc +export function Register< + TargetType extends new (...args: unknown[]) => InstanceType, +>( + identifier: Identifier, + arg1?: + | undefined + | boolean + | InitDelegate> + | 'instance', + arg2?: boolean, +): (constructor: TargetType, ...args: unknown[]) => void { + const deprecated = typeof arg1 === 'boolean' ? arg1 : arg2; + const init = typeof arg1 === 'function' ? arg1 : undefined; + const shouldRegisterInstance = arg1 === 'instance'; + + if (init == undefined && shouldRegisterInstance !== true) { + return _register(identifier, deprecated); + } else { + return _registerInstance(identifier, init, deprecated); + } +} + +/** + * A decorator to register a class in the **TSinjex** DI (Dependency Injection) container. + * @template TargetType The type of the class to be registered. + * @param identifier The identifier used to register the class in the DI container. + * @see {@link Identifier} for more information on identifiers. + * @param deprecated If true, the dependency is deprecated and a warning + * is logged only once upon the first resolution of the dependency. + * @returns The decorator function to be applied on the class. + * @example + * ```ts + * \@Register('MyClassIdentifier') + * class MyClass { + * // ... + * } + * ``` + */ +function _register< + TargetType extends new (...args: unknown[]) => InstanceType, >(identifier: Identifier, deprecated?: boolean) { return function (constructor: TargetType, ...args: unknown[]): void { // Get the instance of the DI container @@ -28,3 +152,111 @@ export function Register< diContainer.register(identifier, constructor, deprecated); }; } + +/** + * A decorator to register an instance of a class in the DI (Dependency Injection) container. + * @template TargetType The type of the class whose instance is to be registered. + * @param identifier The identifier used to register the instance in the DI container. + * @see {@link Identifier} for more information on identifiers. + * @param init An optional initializer function which get the constructor of the class + * as input and returns an instance of the class. + * @param deprecated If true, the dependency is deprecated and a warning + * is logged only once upon the first resolution of the dependency. + * @see {@link InitDelegate} for more information on initializer functions. + * @returns The decorator function to be applied on the class. + * @example + * ```ts + * \@RegisterInstance('MyClassInstanceIdentifier', (constructor) => new constructor()) + * class MyClass { + * // ... + * } + * ``` + */ +function _registerInstance< + TargetType extends new (..._args: unknown[]) => InstanceType, +>( + identifier: Identifier, + init?: InitDelegate< + TargetType & { new (..._args: unknown[]): InstanceType }, + InstanceType + >, + deprecated?: boolean, +) { + return function (constructor: TargetType, ...args: unknown[]): void { + // Get the instance of the DI container + const diContainer = TSinjex.getInstance(); + let instance: InstanceType; + + // Create a proxy to instantiate the class when needed (Lazy Initialization) + let lazyProxy: unknown = new Proxy( + {}, + { + get(target, prop, receiver) { + ({ instance, lazyProxy } = initializeInstance( + instance, + init, + constructor, + args, + lazyProxy, + )); + + // Return the requested property of the instance + return instance[prop as keyof InstanceType]; + }, + set(target, prop, value, receiver) { + ({ instance, lazyProxy } = initializeInstance( + instance, + init, + constructor, + args, + lazyProxy, + )); + + // Set the requested property of the instance + return (instance[prop as keyof InstanceType] = + value); + }, + }, + ); + + // Register the lazy proxy in the DI container + diContainer.register(identifier, lazyProxy, deprecated); + }; +} + +/** + * Initializes the instance of the class. + * @template TargetType The type of the class whose instance is to be initialized. + * @param instance The instance of the class to be initialized. + * @param init The optional initializer function to initialize the instance. + * @param constructor The constructor of the class. + * @param args The arguments to be passed to the constructor of the class. + * @param lazyProxy The lazy proxy to instantiate the class when needed. + * @returns The initialized instance and the lazy proxy. + */ +function initializeInstance< + TargetType extends new (..._args: unknown[]) => InstanceType, +>( + instance: InstanceType, + init: + | InitDelegate< + TargetType & + (new (..._args: unknown[]) => InstanceType), + InstanceType + > + | undefined, + constructor: TargetType, + args: unknown[], + lazyProxy: unknown, +): { instance: InstanceType; lazyProxy: unknown } { + if (instance == null) { + if (init) { + instance = init(constructor); + } else { + instance = new constructor(...args); + } + } + lazyProxy = instance; + + return { instance, lazyProxy }; +} diff --git a/src/decorators/RegisterInstance.ts b/src/decorators/RegisterInstance.ts index d638f6e..2fa4545 100644 --- a/src/decorators/RegisterInstance.ts +++ b/src/decorators/RegisterInstance.ts @@ -1,4 +1,4 @@ -import { TSinjex } from '../classes/TSinjex'; +import { Register } from './Register'; import { Identifier } from '../types/Identifier'; import { InitDelegate } from '../types/InitDelegate'; @@ -9,6 +9,8 @@ import { InitDelegate } from '../types/InitDelegate'; * @see {@link Identifier} for more information on identifiers. * @param init An optional initializer function which get the constructor of the class * as input and returns an instance of the class. + * @param deprecated If true, the dependency is deprecated and a warning + * is logged only once upon the first resolution of the dependency. * @see {@link InitDelegate} for more information on initializer functions. * @returns The decorator function to be applied on the class. * @example @@ -18,6 +20,7 @@ import { InitDelegate } from '../types/InitDelegate'; * // ... * } * ``` + * @deprecated Use {@link Register} instead. This decorator already uses the {@link Register} decorator internally. */ export function RegisterInstance< TargetType extends new (..._args: unknown[]) => InstanceType, @@ -27,47 +30,10 @@ export function RegisterInstance< TargetType & { new (..._args: unknown[]): InstanceType }, InstanceType >, -) { - return function (constructor: TargetType, ...args: unknown[]): void { - // Get the instance of the DI container - const diContainer = TSinjex.getInstance(); - let instance: InstanceType; + deprecated?: boolean, +): (constructor: TargetType, ...args: unknown[]) => void { + const initDelegate = typeof init === 'function' ? init : undefined; - // Create a proxy to instantiate the class when needed (Lazy Initialization) - let lazyProxy: unknown = new Proxy( - {}, - { - get(target, prop, receiver) { - if (instance == null) { - if (init) { - instance = init(constructor); - } else { - instance = new constructor(...args); - } - } - lazyProxy = instance; - - // Return the requested property of the instance - return instance[prop as keyof InstanceType]; - }, - set(target, prop, value, receiver) { - if (instance == null) { - if (init) { - instance = init(constructor); - } else { - instance = new constructor(...args); - } - } - lazyProxy = instance; - - // Set the requested property of the instance - return (instance[prop as keyof InstanceType] = - value); - }, - }, - ); - - // Register the lazy proxy in the DI container - diContainer.register(identifier, lazyProxy); - }; + if (initDelegate) return Register(identifier, initDelegate, deprecated); + else return Register(identifier, 'instance', deprecated); } From 3563f9c590d2d79dd05f5a60f76b30724eb91633 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Tue, 11 Mar 2025 17:44:03 +0100 Subject: [PATCH 2/5] tests: add mode parameter to RegisterInstanceDecorator - Introduced a `mode` parameter to the `test_RegisterInstanceDecorator` function allowing 'instance' or 'standalone' modes. - Updated test cases to utilize the new `mode` parameter when registering an instance. - Disabled specific ESLint rule in `Decorators.test.ts` for deprecation warnings. - Added an additional test call to `test_RegisterInstanceDecorator` with 'instance' mode. --- src/__tests__/Decorators.spec.ts | 11 +++++++++-- src/__tests__/Decorators.test.ts | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/__tests__/Decorators.spec.ts b/src/__tests__/Decorators.spec.ts index b3c6cdc..df90bf3 100644 --- a/src/__tests__/Decorators.spec.ts +++ b/src/__tests__/Decorators.spec.ts @@ -282,6 +282,7 @@ export function test_RegisterInstanceDecorator( Container: ITSinjex_, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type registerInstance: Function, + mode: 'instance' | 'standalone' = 'standalone', ): void { describe('RegisterInstance Decorator Tests', () => { let container: ITSinjex; @@ -295,7 +296,10 @@ export function test_RegisterInstanceDecorator( }); it('should register an instance of a dependency', () => { - @registerInstance('InstanceIdentifier') + @registerInstance( + 'InstanceIdentifier', + mode === 'instance' ? 'instance' : undefined, + ) class TestClass { private readonly _dependency!: any; @@ -337,7 +341,10 @@ export function test_RegisterInstanceDecorator( }); it('should register an instance of a dependency and get it on set', () => { - @registerInstance('InstanceIdentifier') + @registerInstance( + 'InstanceIdentifier', + mode === 'instance' ? 'instance' : undefined, + ) class TestClass { private readonly _dependency!: any; diff --git a/src/__tests__/Decorators.test.ts b/src/__tests__/Decorators.test.ts index efba3e6..28d2d54 100644 --- a/src/__tests__/Decorators.test.ts +++ b/src/__tests__/Decorators.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ import { TSinjex } from 'src/classes/TSinjex'; import { Inject } from 'src/decorators/Inject'; import { Register } from 'src/decorators/Register'; @@ -13,3 +14,5 @@ test_InjectDecorator(TSinjex, Inject); test_RegisterDecorator(TSinjex, Register); test_RegisterInstanceDecorator(TSinjex, RegisterInstance); + +test_RegisterInstanceDecorator(TSinjex, Register, 'instance'); From 405b7ff99ad417e78f66c513c599aa0b44386e85 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Tue, 11 Mar 2025 17:46:00 +0100 Subject: [PATCH 3/5] docs: Reflect changes to changelog --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ddba9e..c2cfebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add pre release building to release workflow on dev/* branches an version changes. +- refactor: consolidate registration decorators + Introduced Register decorator to handle class and instance registration in the DI container. + Deprecated RegisterInstance in favor of Register, which now internally handles instance registration. + Added support for marking dependencies as deprecated with a warning logged upon first resolution. + Updated documentation with examples and notes on deprecation. +- tests: add mode parameter to RegisterInstanceDecorator + Introduced a mode parameter to the test_RegisterInstanceDecorator function allowing 'instance' or 'standalone' modes. + Updated test cases to utilize the new mode parameter when registering an instance. + Disabled specific ESLint rule in Decorators.test.ts for deprecation warnings. + Added an additional test call to test_RegisterInstanceDecorator with 'instance' mode. ### Deprecated From 816a6d128aaa3e3b511969cafa1bf29fc4221df9 Mon Sep 17 00:00:00 2001 From: "Max P." Date: Wed, 12 Mar 2025 09:34:53 +0100 Subject: [PATCH 4/5] refactor: add region tags for overloads in Register.ts --- src/decorators/Register.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/decorators/Register.ts b/src/decorators/Register.ts index b733e89..69ea5f3 100644 --- a/src/decorators/Register.ts +++ b/src/decorators/Register.ts @@ -2,6 +2,8 @@ import { InitDelegate } from 'src/types/InitDelegate'; import { TSinjex } from '../classes/TSinjex'; import { Identifier } from '../types/Identifier'; +//#region Overloads + /** * A decorator to register a class in the **TSinjex** DI (Dependency Injection) container. * @template TargetType The type of the class to be registered. @@ -102,6 +104,8 @@ export function Register< deprecated?: boolean, ): (constructor: TargetType, ...args: unknown[]) => void; +//#endregion Overloads + // eslint-disable-next-line jsdoc/require-jsdoc export function Register< TargetType extends new (...args: unknown[]) => InstanceType, From 97bf2be7451ececd0fc64e4d58db0bab16b4db7b Mon Sep 17 00:00:00 2001 From: "Max P." Date: Wed, 12 Mar 2025 09:35:43 +0100 Subject: [PATCH 5/5] docs: Reflect changes to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2cfebc..c8bd771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Updated test cases to utilize the new mode parameter when registering an instance. Disabled specific ESLint rule in Decorators.test.ts for deprecation warnings. Added an additional test call to test_RegisterInstanceDecorator with 'instance' mode. +- refactor: add region tags for overloads in Register.ts ### Deprecated