diff --git a/src/classes/TSinjex.ts b/src/classes/TSinjex.ts index 531a1a4..32e2949 100644 --- a/src/classes/TSinjex.ts +++ b/src/classes/TSinjex.ts @@ -1,14 +1,3 @@ -import type { Inject } from '../decorators/Inject.js'; -import type { Register } from '../decorators/Register.js'; -import type { RegisterInstance } from '../decorators/RegisterInstance.js'; -import type { register } from '../functions/register.js'; -import type { resolve } from '../functions/resolve.js'; -import { ImplementsStatic } from '../helper/ImplementsStatic.js'; -import { DependencyResolutionError } from '../interfaces/Exceptions.js'; -import { IDependency } from '../interfaces/IDependency.js'; -import { ITSinjex, ITSinjex_ } from '../interfaces/ITSinjex.js'; -import { Identifier } from '../types/Identifier.js'; - /** * # TSinjex * The main class for the Dependency Injection Container **TSinjex**. @@ -21,6 +10,11 @@ import { Identifier } from '../types/Identifier.js'; * @see {@link register} for registering a dependency (class or instance) as a function. * @see {@link resolve} for resolving a dependency as a function. */ + +import { ImplementsStatic } from "../helper/mod.ts"; +import { DependencyResolutionError, IDependency, ITSinjex, ITSinjex_ } from "../interfaces/mod.ts"; +import { Identifier } from "../types/mod.ts"; + @ImplementsStatic() export class TSinjex implements ITSinjex { /** @@ -123,5 +117,9 @@ export class TSinjex implements ITSinjex { return dependency.dependency as T; } + public clear(): void { + this._dependencies.clear(); + } + //#endregion } diff --git a/src/classes/mod.ts b/src/classes/mod.ts new file mode 100644 index 0000000..baf711c --- /dev/null +++ b/src/classes/mod.ts @@ -0,0 +1 @@ +export { TSinjex } from "./TSinjex.ts"; diff --git a/src/decorators/Inject.ts b/src/decorators/Inject.ts index cbfb341..4563965 100644 --- a/src/decorators/Inject.ts +++ b/src/decorators/Inject.ts @@ -1,151 +1,54 @@ -import { TSinjex } from '../classes/TSinjex.js'; -import { - DependencyResolutionError, - InitializationError, - InjectorError, - NoInstantiationMethodError, -} from '../interfaces/Exceptions.js'; -import { Identifier } from '../types/Identifier.js'; -import { InitDelegate } from '../types/InitDelegate.js'; +import { TSinjex } from "../classes/mod.ts"; +import { InitializationError } from "../interfaces/mod.ts"; +import { Identifier, InitDelegate } from "../types/mod.ts"; -/** - * A decorator to inject a dependency from a DI (Dependency Injection) container into a class property. - * @template T The type of the dependency to be injected. - * @template U The type of the property to be injected. - * @param identifier The identifier used to resolve the class in the DI container. - * @see {@link Identifier} for more information on identifiers. - * @param init Optional an initializer function to transform the dependency before injection - * or true to instantiate the dependency if it has a constructor. - * @see {@link InitDelegate} for more information on initializer functions. - * @param isNecessary If true, throws an error if the dependency is not found. - * @returns The resolved dependency or undefined if the dependency is not necessary - * and not found, or throws an error if the dependency is necessary and not found. - * @throws **Only throws errors if the dependency is necessary.** - * @throws A {@link DependencyResolutionError} if the dependency is not found. - * @throws A {@link InjectorError} if an error occurs during the injection process. - * @throws A {@link NoInstantiationMethodError} if the dependency does not have a constructor. - * @throws An {@link InitializationError} if an error occurs during the initialization process. - * @example - * ```ts - * class MyClass { - * \@Inject('MyDependencyIdentifier') - * private myDependency!: MyDependency; - * } - * ``` - * @example - * ```ts - * class MyClass { - * \@Inject('ILogger_', (x: ILogger_) => x.getLogger('Tags'), false) - * private _logger?: ILogger; - * } - * ``` - */ -export function Inject( +export function Inject( identifier: Identifier, - init?: InitDelegate | true, + init?: InitDelegate, isNecessary = true, -) { - return function (target: unknown, propertyKey: string | symbol): void { - /** - * Function to evaluate the dependency lazily - * to avoid circular dependencies, not found dependencies, etc. - * @returns The resolved dependency or undefined if the dependency is not found. - */ - const resolve = (): T | undefined => { - return TSinjex.getInstance().resolve(identifier, isNecessary); +): ( + target: undefined, + context: ClassFieldDecoratorContext, +) => (initialValue: FieldType) => FieldType { + return function ( + _target: undefined, + context: ClassFieldDecoratorContext, + ): (initialValue: FieldType) => FieldType { + if (context.kind !== "field") { + throw new Error("Inject decorator can only be used on fields."); + } + + const initializer = () => { + let instance: DependencyType | FieldType | undefined; + + const dependency: DependencyType | undefined = TSinjex.getInstance() + .resolve(identifier, isNecessary); + + if (init == null || dependency == null) { + instance = dependency; + } else { + try { + instance = init(dependency); + } catch (error) { + if (isNecessary) { + throw new InitializationError( + identifier, + error instanceof Error ? error : new Error(String(error)), + ); + } else { + console.warn( + `Error initializing not necessary dependency ${identifier.toString()}: ${error}`, + ); + instance = undefined; + } + } + } + + return instance as FieldType; }; - Object.defineProperty(target, propertyKey, { - get() { - let instance: T | U | undefined; - - const dependency: T | undefined = tryAndCatch( - () => resolve(), - isNecessary, - identifier, - DependencyResolutionError, - ); - - if (dependency != null) { - const initFunction: (() => U) | undefined = - typeof init === 'function' && dependency != null - ? (): U => init(dependency) - : init === true && hasConstructor(dependency) - ? (): U => new dependency() as U - : undefined; - - if (init == null) instance = dependency; - else if (initFunction != null) - instance = tryAndCatch( - initFunction, - isNecessary, - identifier, - InitializationError, - ); - else if (isNecessary) - throw new NoInstantiationMethodError(identifier); - } else if (isNecessary) - throw new DependencyResolutionError(identifier); - - /** - * Replace itself with the resolved dependency - * for performance reasons. - */ - Object.defineProperty(this, propertyKey, { - value: instance, - writable: false, - enumerable: false, - configurable: false, - }); - - return instance; - }, - /** - * Make the property configurable to allow replacing it - */ - configurable: true, - }); + return function (_initialValue: FieldType): FieldType { + return initializer(); + }; }; } - -/** - * Tries to execute a function and catches any errors that occur. - * If the function is necessary and an error occurs, it throws the error - * with the specified error class and identifier. - * @param fn The function to execute. - * @param necessary If true, throws an error if an error occurs. - * @param identifier The identifier of the dependency. - * @param errorClass The error class to throw if an error occurs. - * @returns The result of the function or undefined if an error occurs and the function is not necessary. - */ -function tryAndCatch( - fn: () => ReturnType, - necessary: boolean, - identifier?: Identifier, - errorClass?: ErrorType, -): ReturnType | undefined { - try { - return fn(); - } catch (error) { - if (necessary) - throw new (errorClass != null ? errorClass : error)( - identifier ?? 'not specified', - error, - ); - else return undefined; - } -} - -/** - * Checks if an object has a constructor. - * @param obj The object to check. - * @returns True if the object has a constructor, false otherwise. - */ -function hasConstructor(obj: T): obj is T & { new (): unknown } { - const _obj = obj as unknown as { prototype?: { constructor?: unknown } }; - - return ( - _obj?.prototype != null && - typeof _obj.prototype.constructor === 'function' - ); -} diff --git a/src/decorators/Register.ts b/src/decorators/Register.ts index 9b675fe..caff42b 100644 --- a/src/decorators/Register.ts +++ b/src/decorators/Register.ts @@ -1,266 +1,88 @@ -import { InitDelegate } from 'src/types/InitDelegate.js'; -import { TSinjex } from '../classes/TSinjex.js'; -import { Identifier } from '../types/Identifier.js'; +import { TSinjex } from "../classes/mod.ts"; +import { ClassConstructor, Identifier, InitDelegate } from "../types/mod.ts"; -//#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, ->( +export function Register( identifier: Identifier, + init?: InitDelegate>, 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. - * @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 - const diContainer = TSinjex.getInstance(); - - // Register the class in the DI container - 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); +): (target: ClassType, context: ClassDecoratorContext) => void { + return function ( + target: ClassType, + context: ClassDecoratorContext, + ): void { + if (context.kind !== "class") { + throw new Error("Register decorator can only be used on classes."); } - } - lazyProxy = instance; - return { instance, lazyProxy }; + const diContainer = TSinjex.getInstance(); + let _instance: InstanceType | undefined; + + if (init == undefined) { + diContainer.register(identifier, target, deprecated); + } else { + diContainer.register( + identifier, + createLazyProxy( + _instance, + init, + target, + ), + deprecated, + ); + } + }; +} + +function createLazyProxy( + instance: InstanceType | undefined, + init: InitDelegate>, + constructor: ClassType, +): InstanceType { + const initializeInstance = ( + instance: InstanceType | undefined, + init: InitDelegate>, + constructor: ClassType, + ) => { + if (instance == undefined) { + if (init != undefined) { + instance = init(constructor); + } else { + instance = new constructor() as InstanceType; + } + } + + return { instance: instance, lazyProxy: instance }; + }; + + let lazyProxy = new Proxy( + {} as InstanceType, + { + get(_target, prop, _receiver) { + ({ instance, lazyProxy } = initializeInstance( + instance, + init, + constructor, + )); + + if (!instance) throw new Error("Instance is not defined"); + + // Return the requested property of the instance + return instance[prop as keyof InstanceType]; + }, + set(_target, prop, value, _receiver) { + ({ instance, lazyProxy } = initializeInstance( + instance, + init, + constructor, + )); + + if (!instance) throw new Error("Instance is not defined"); + + // Set the requested property of the instance + return (instance[prop as keyof InstanceType] = value); + }, + }, + ); + + return lazyProxy; } diff --git a/src/decorators/mod.ts b/src/decorators/mod.ts new file mode 100644 index 0000000..ccc7d88 --- /dev/null +++ b/src/decorators/mod.ts @@ -0,0 +1,2 @@ +export { Register } from "./Register.ts"; +export { Inject } from "./Inject.ts"; diff --git a/src/export.ts b/src/export.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/functions/mod.ts b/src/functions/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/helper/ImplementsStatic.ts b/src/helper/ImplementsStatic.ts index 8375afb..47c2e5d 100644 --- a/src/helper/ImplementsStatic.ts +++ b/src/helper/ImplementsStatic.ts @@ -6,5 +6,5 @@ * @returns A decorator function */ export function ImplementsStatic() { - return (constructor: T, ...args: unknown[]) => {}; + return (_constructor: T, ..._args: unknown[]) => {}; } diff --git a/src/helper/mod.ts b/src/helper/mod.ts new file mode 100644 index 0000000..4c6a674 --- /dev/null +++ b/src/helper/mod.ts @@ -0,0 +1 @@ +export { ImplementsStatic } from "./ImplementsStatic.ts"; diff --git a/src/interfaces/Exceptions.ts b/src/interfaces/Exceptions.ts index dc8df22..49773af 100644 --- a/src/interfaces/Exceptions.ts +++ b/src/interfaces/Exceptions.ts @@ -1,5 +1,4 @@ -import { Identifier } from 'src/types/Identifier.js'; -import { ITSinjex } from './ITSinjex.js'; +import { Identifier } from "../types/mod.ts"; /** * General error class for {@link ITSinjex} interface. @@ -11,7 +10,7 @@ export class TSinjexError extends Error { */ constructor(message: string) { super(message); - this.name = 'TSinjex'; + this.name = "TSinjex"; } } @@ -26,7 +25,7 @@ export class DependencyResolutionError extends TSinjexError { */ constructor(identifier: Identifier) { super(`Dependency ${identifier.toString()} could not be resolved.`); - this.name = 'TSinjexResolutionError'; + this.name = "TSinjexResolutionError"; } } @@ -44,7 +43,7 @@ export class InjectorError extends TSinjexError { super( `Error injecting dependency ${identifier.toString()} with error: "${originalError}"`, ); - this.name = 'TSinjexInjectorError'; + this.name = "TSinjexInjectorError"; } } @@ -61,7 +60,7 @@ export class NoInstantiationMethodError extends TSinjexError { super( `No instantiation method found for dependency ${identifier.toString()}.`, ); - this.name = 'TSinjexNoInstantiationMethodError'; + this.name = "TSinjexNoInstantiationMethodError"; } } @@ -79,6 +78,6 @@ export class InitializationError extends TSinjexError { super( `Error initializing dependency ${identifier.toString()} with error: "${originalError}"`, ); - this.name = 'TSinjexInitializationError'; + this.name = "TSinjexInitializationError"; } } diff --git a/src/interfaces/ITSinjex.ts b/src/interfaces/ITSinjex.ts index 2078c5c..7f47c57 100644 --- a/src/interfaces/ITSinjex.ts +++ b/src/interfaces/ITSinjex.ts @@ -1,5 +1,4 @@ -import { DependencyResolutionError } from './Exceptions.js'; -import { Identifier } from '../types/Identifier.js'; +import { Identifier } from "../types/mod.ts"; /** * Static TSInjex Interface diff --git a/src/interfaces/mod.ts b/src/interfaces/mod.ts new file mode 100644 index 0000000..4ad63ba --- /dev/null +++ b/src/interfaces/mod.ts @@ -0,0 +1,9 @@ +export type { ITSinjex, ITSinjex_ } from "./ITSinjex.ts"; +export type { IDependency } from "./IDependency.ts"; +export { + DependencyResolutionError, + InitializationError, + InjectorError, + NoInstantiationMethodError, + TSinjexError, +} from "./Exceptions.ts"; diff --git a/src/mod.ts b/src/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/types/Constructor.ts b/src/types/Constructor.ts new file mode 100644 index 0000000..d0dc7b0 --- /dev/null +++ b/src/types/Constructor.ts @@ -0,0 +1,18 @@ +/** + * Generic constructor type. + * This type is used to define a constructor of a class. + */ +export type GenericConstructor< + T extends abstract new (...args: unknown[]) => InstanceType, +> = new (...args: ConstructorParameters) => T; + +/** + * Force generic constructor type. + * This type is used to force a class to has a constructor. + */ +export type ForceConstructor = new (...args: unknown[]) => T; + +/** + * Represents any concrete (non-abstract) class constructor. + */ +export type ClassConstructor = new (...args: unknown[]) => object; diff --git a/src/types/mod.ts b/src/types/mod.ts new file mode 100644 index 0000000..bf81495 --- /dev/null +++ b/src/types/mod.ts @@ -0,0 +1,5 @@ +// Export all type aliases for external use + +export type { Identifier } from "./Identifier.ts"; +export type { InitDelegate } from "./InitDelegate.ts"; +export type { ClassConstructor, ForceConstructor, GenericConstructor } from "./Constructor.ts"; diff --git a/tests/Decorators.ts b/tests/Decorators.ts new file mode 100644 index 0000000..5f7613e --- /dev/null +++ b/tests/Decorators.ts @@ -0,0 +1,229 @@ +// deno-coverage-ignore-file +// deno-lint-ignore-file no-explicit-any +import { + assertEquals, + assertInstanceOf, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { TSinjex } from "../src/classes/mod.ts"; +import { Inject, Register } from "../src/decorators/mod.ts"; +import { DependencyResolutionError } from "../src/interfaces/mod.ts"; + +const container = TSinjex.getInstance() as TSinjex; + +Deno.test("should inject dependency when necessary is true", () => { + container.clear(); + container.register("MockDependencyIdentifier", { value: "test-value" }); + + class TestClass { + @Inject("MockDependencyIdentifier") + private _dependency!: any; + + public getDependency() { + return this._dependency; + } + } + + const instance = new TestClass(); + assertEquals(instance.getDependency().value, "test-value"); +}); + +Deno.test("should inject dependency and run initializer", () => { + container.clear(); + container.register("MockDependencyIdentifier", { value: "test-value" }); + + class TestClass { + @Inject("MockDependencyIdentifier", (x: any) => { + x.value = "test-value-init"; + return x; + }) + dependency!: any; + public getDependency() { + return this.dependency; + } + } + + const instance = new TestClass(); + assertEquals(instance.getDependency().value, "test-value-init"); +}); + +Deno.test("should throw error if initializer fails and dependency is necessary", () => { + container.clear(); + container.register("InitThrowDependencie", { value: "test-value" }); + + class TestClass { + @Inject("InitThrowDependencie", () => { + throw new Error("Initializer error"); + }, true) + dependency!: any; + + public getDependency() { + return this.dependency; + } + } + + assertThrows( + () => { + const instance = new TestClass(); + instance.getDependency()(); + }, + Error, + "Initializer error", + ); +}); + +Deno.test("should throw DependencyResolutionError if dependency not found", () => { + container.clear(); + + class TestClass { + @Inject("NonExistentDependencyIdentifier") + private _dependency!: any; + public getDependency() { + return this._dependency; + } + } + + assertThrows(() => { + const instance = new TestClass(); + instance.getDependency()(); + }, DependencyResolutionError); +}); + +Deno.test("should replace the property with the resolved dependency", () => { + container.clear(); + container.register("MockDependencyIdentifier", { value: "test-value" }); + + class TestClass { + @Inject("MockDependencyIdentifier") + private _dependency!: any; + public getDependency() { + return this._dependency; + } + public isDependencyTypeofFunction() { + return typeof this._dependency === "function"; + } + } + + const instance = new TestClass(); + assertEquals(instance.getDependency().value, "test-value"); + assertEquals(instance.isDependencyTypeofFunction(), false); + assertEquals(instance.getDependency().value, "test-value"); +}); + +Deno.test("Register Decorator: should register a dependency", () => { + container.clear(); + + @Register("MockDependencyIdentifier") + class TestClass { + private readonly _dependency!: any; + public getDependency() { + return this._dependency; + } + } + + assertStrictEquals( + container.resolve("MockDependencyIdentifier"), + TestClass, + ); +}); + +Deno.test("RegisterInstance: should register an instance of a dependency", () => { + container.clear(); + + @Register("InstanceIdentifier", (x) => new x()) + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = "instance"; + } + + const resolved = container.resolve("InstanceIdentifier"); + assertEquals(resolved?.mark, "instance"); +}); + +Deno.test("RegisterInstance: should run init function during registration", () => { + container.clear(); + + @Register("InstanceIdentifier", (x: new () => TestClass) => { + const instance = new x(); + instance.mark = "init"; + return instance; + }) + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = "instance"; + } + + const resolved = container.resolve("InstanceIdentifier"); + assertEquals(resolved?.mark, "init"); +}); + +Deno.test("RegisterInstance: instance should persist modifications", () => { + container.clear(); + + @Register("InstanceIdentifier", (x) => new x()) + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = "instance"; + public test: string = "test"; + } + + const instance1 = container.resolve("InstanceIdentifier"); + if (instance1 == null) { + throw new Error("Instance1 is null"); + } + instance1.test = "test2"; + + const instance2 = container.resolve("InstanceIdentifier"); + if (instance2 == null) { + throw new Error("Instance2 is null"); + } + assertEquals(instance2.test, "test2"); +}); + +Deno.test("RegisterInstance: init function should persist modifications", () => { + container.clear(); + + @Register("InstanceIdentifier", (x: new () => TestClass) => { + const instance = new x(); + instance.mark = "init"; + return instance; + }) + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = "instance"; + public test: string = "test"; + } + + const instance1 = container.resolve("InstanceIdentifier"); + if (instance1 == null) { + throw new Error("Instance1 is null"); + } + instance1.test = "test2"; + + const instance2 = container.resolve("InstanceIdentifier"); + if (instance2 == null) { + throw new Error("Instance2 is null"); + } + assertEquals(instance2.test, "test2"); +}); diff --git a/tests/TSInjex.ts b/tests/TSInjex.ts new file mode 100644 index 0000000..5eb7213 --- /dev/null +++ b/tests/TSInjex.ts @@ -0,0 +1,78 @@ +// deno-coverage-ignore-file +// deno-lint-ignore-file +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { TSinjex } from "../src/classes/mod.ts"; + +const container = TSinjex.getInstance() as TSinjex; + +Deno.test("should register and resolve a dependency (instance)", () => { + container.clear(); + + const identifier = "myDependency"; + const dependency = { value: 42 }; + + container.register(identifier, dependency); + + const resolved = container.resolve(identifier); + assertStrictEquals(resolved, dependency); +}); + +Deno.test("should register and resolve a dependency (static)", () => { + container.clear(); + + const identifier = "myDependency"; + const dependency = { value: 42 }; + + TSinjex.register(identifier, dependency); + const resolved = TSinjex.resolve(identifier); + + assertStrictEquals(resolved, dependency); +}); + +Deno.test("should throw error when resolving non-registered dependency (static)", () => { + container.clear(); + + const identifier = "nonExistentDependency"; + assertThrows(() => { + TSinjex.resolve(identifier); + }); +}); + +Deno.test("should return undefined when resolving non-necessary dependency", () => { + container.clear(); + + const result = TSinjex.resolve("nonExistentDependency", false); + assertEquals(result, undefined); +}); + +Deno.test("should warn when resolving a deprecated dependency", () => { + container.clear(); + + const identifier = "deprecatedDependency"; + const dependency = { value: 42 }; + + // Mock console.warn + const originalWarn = console.warn; + let wasCalled = false; + let lastMessage = ""; + + console.warn = (msg: string) => { + wasCalled = true; + lastMessage = msg; + }; + + try { + TSinjex.register(identifier, dependency, true); + const resolved = TSinjex.resolve(identifier); + assertStrictEquals(resolved, dependency); + if (!wasCalled) { + throw new Error("console.warn was not called"); + } + } finally { + console.warn = originalWarn; // Restore + } +});