diff --git a/CHANGELOG.md b/CHANGELOG.md index 21240b1..2694a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +## [0.3.0] + +### Added + +- 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. +- refactor: add region tags for overloads in Register.ts ## [0.2.0] @@ -76,4 +91,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [unreleased]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...HEAD [0.0.14]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.13...v0.0.14 -[0.2.00]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...v0.2.0 \ No newline at end of file +[0.2.00]: https://github.com/PxaMMaxP/TSinjex/compare/0.0.14...v0.2.0 +[0.3.00]: https://github.com/PxaMMaxP/TSinjex/compare/0.2.0...v0.3.0 \ No newline at end of file diff --git a/package.json b/package.json index 767bff6..54899b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-injex", - "version": "0.2.0", + "version": "0.3.0", "description": "Simple boilerplate code free dependency injection system for TypeScript.", "type": "module", "main": "./dist/index.js", 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'); diff --git a/src/decorators/Register.ts b/src/decorators/Register.ts index fa65076..69ea5f3 100644 --- a/src/decorators/Register.ts +++ b/src/decorators/Register.ts @@ -1,6 +1,134 @@ +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. + * @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 { + * // ... + * } + * ``` + * @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; + +//#endregion Overloads + +// 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. @@ -17,7 +145,7 @@ import { Identifier } from '../types/Identifier'; * } * ``` */ -export function Register< +function _register< TargetType extends new (...args: unknown[]) => InstanceType, >(identifier: Identifier, deprecated?: boolean) { return function (constructor: TargetType, ...args: unknown[]): void { @@ -28,3 +156,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); }