refactor(di): modularize and improve dependency injection for deno
- Consolidate import paths into scoped modules for better structure. - Refactor decorators (`Inject`, `Register`) for improved type safety. - Add `clear` method to the DI container for easier test cleanup. - Introduce lazy initialization for registered instances. - Add comprehensive unit tests for decorators and DI container. - Standardize error handling and naming conventions for exceptions. Signed-off-by: Max P. <Mail@MPassarello.de>
This commit is contained in:
@@ -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
|
* # TSinjex
|
||||||
* The main class for the Dependency Injection Container **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 register} for registering a dependency (class or instance) as a function.
|
||||||
* @see {@link resolve} for resolving a dependency 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<ITSinjex_>()
|
@ImplementsStatic<ITSinjex_>()
|
||||||
export class TSinjex implements ITSinjex {
|
export class TSinjex implements ITSinjex {
|
||||||
/**
|
/**
|
||||||
@@ -123,5 +117,9 @@ export class TSinjex implements ITSinjex {
|
|||||||
return dependency.dependency as T;
|
return dependency.dependency as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._dependencies.clear();
|
||||||
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
1
src/classes/mod.ts
Normal file
1
src/classes/mod.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { TSinjex } from "./TSinjex.ts";
|
@@ -1,151 +1,54 @@
|
|||||||
import { TSinjex } from '../classes/TSinjex.js';
|
import { TSinjex } from "../classes/mod.ts";
|
||||||
import {
|
import { InitializationError } from "../interfaces/mod.ts";
|
||||||
DependencyResolutionError,
|
import { Identifier, InitDelegate } from "../types/mod.ts";
|
||||||
InitializationError,
|
|
||||||
InjectorError,
|
|
||||||
NoInstantiationMethodError,
|
|
||||||
} from '../interfaces/Exceptions.js';
|
|
||||||
import { Identifier } from '../types/Identifier.js';
|
|
||||||
import { InitDelegate } from '../types/InitDelegate.js';
|
|
||||||
|
|
||||||
/**
|
export function Inject<InstanzType, DependencyType, FieldType extends object>(
|
||||||
* 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<MyDependency>('MyDependencyIdentifier')
|
|
||||||
* private myDependency!: MyDependency;
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* class MyClass {
|
|
||||||
* \@Inject('ILogger_', (x: ILogger_) => x.getLogger('Tags'), false)
|
|
||||||
* private _logger?: ILogger;
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function Inject<T, U>(
|
|
||||||
identifier: Identifier,
|
identifier: Identifier,
|
||||||
init?: InitDelegate<T, U> | true,
|
init?: InitDelegate<DependencyType, FieldType>,
|
||||||
isNecessary = true,
|
isNecessary = true,
|
||||||
) {
|
): (
|
||||||
return function (target: unknown, propertyKey: string | symbol): void {
|
target: undefined,
|
||||||
/**
|
context: ClassFieldDecoratorContext<InstanzType, FieldType>,
|
||||||
* Function to evaluate the dependency lazily
|
) => (initialValue: FieldType) => FieldType {
|
||||||
* to avoid circular dependencies, not found dependencies, etc.
|
return function (
|
||||||
* @returns The resolved dependency or undefined if the dependency is not found.
|
_target: undefined,
|
||||||
*/
|
context: ClassFieldDecoratorContext<InstanzType, FieldType>,
|
||||||
const resolve = (): T | undefined => {
|
): (initialValue: FieldType) => FieldType {
|
||||||
return TSinjex.getInstance().resolve<T>(identifier, isNecessary);
|
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<DependencyType>(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, {
|
return function (_initialValue: FieldType): FieldType {
|
||||||
get() {
|
return initializer();
|
||||||
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,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<ReturnType, ErrorType>(
|
|
||||||
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<T>(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'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@@ -1,266 +1,88 @@
|
|||||||
import { InitDelegate } from 'src/types/InitDelegate.js';
|
import { TSinjex } from "../classes/mod.ts";
|
||||||
import { TSinjex } from '../classes/TSinjex.js';
|
import { ClassConstructor, Identifier, InitDelegate } from "../types/mod.ts";
|
||||||
import { Identifier } from '../types/Identifier.js';
|
|
||||||
|
|
||||||
//#region Overloads
|
export function Register<ClassType extends ClassConstructor>(
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<TargetType>,
|
|
||||||
>(
|
|
||||||
identifier: Identifier,
|
identifier: Identifier,
|
||||||
|
init?: InitDelegate<ClassType, InstanceType<ClassType>>,
|
||||||
deprecated?: boolean,
|
deprecated?: boolean,
|
||||||
): (constructor: TargetType, ...args: unknown[]) => void;
|
): (target: ClassType, context: ClassDecoratorContext<ClassType>) => void {
|
||||||
|
return function (
|
||||||
/**
|
target: ClassType,
|
||||||
* A decorator to register an instance of a class in the DI (Dependency Injection) container.
|
context: ClassDecoratorContext<ClassType>,
|
||||||
* @template TargetType The type of the class whose instance is to be registered.
|
): void {
|
||||||
* @param identifier The identifier used to register the instance in the DI container.
|
if (context.kind !== "class") {
|
||||||
* @see {@link Identifier} for more information on identifiers.
|
throw new Error("Register decorator can only be used on classes.");
|
||||||
* @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<TargetType>,
|
|
||||||
>(
|
|
||||||
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<TargetType>,
|
|
||||||
>(
|
|
||||||
identifier: Identifier,
|
|
||||||
init?: InitDelegate<
|
|
||||||
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
|
|
||||||
InstanceType<TargetType>
|
|
||||||
>,
|
|
||||||
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<TargetType>,
|
|
||||||
>(
|
|
||||||
identifier: Identifier,
|
|
||||||
arg1?:
|
|
||||||
| undefined
|
|
||||||
| boolean
|
|
||||||
| InitDelegate<TargetType, InstanceType<TargetType>>
|
|
||||||
| '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<TargetType>,
|
|
||||||
>(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<TargetType>,
|
|
||||||
>(
|
|
||||||
identifier: Identifier,
|
|
||||||
init?: InitDelegate<
|
|
||||||
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
|
|
||||||
InstanceType<TargetType>
|
|
||||||
>,
|
|
||||||
deprecated?: boolean,
|
|
||||||
) {
|
|
||||||
return function (constructor: TargetType, ...args: unknown[]): void {
|
|
||||||
// Get the instance of the DI container
|
|
||||||
const diContainer = TSinjex.getInstance();
|
|
||||||
let instance: InstanceType<TargetType>;
|
|
||||||
|
|
||||||
// Create a proxy to instantiate the class when needed (Lazy Initialization)
|
|
||||||
let lazyProxy: unknown = new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
get(target, prop, receiver) {
|
|
||||||
({ instance, lazyProxy } = initializeInstance<TargetType>(
|
|
||||||
instance,
|
|
||||||
init,
|
|
||||||
constructor,
|
|
||||||
args,
|
|
||||||
lazyProxy,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Return the requested property of the instance
|
|
||||||
return instance[prop as keyof InstanceType<TargetType>];
|
|
||||||
},
|
|
||||||
set(target, prop, value, receiver) {
|
|
||||||
({ instance, lazyProxy } = initializeInstance<TargetType>(
|
|
||||||
instance,
|
|
||||||
init,
|
|
||||||
constructor,
|
|
||||||
args,
|
|
||||||
lazyProxy,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Set the requested property of the instance
|
|
||||||
return (instance[prop as keyof InstanceType<TargetType>] =
|
|
||||||
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<TargetType>,
|
|
||||||
>(
|
|
||||||
instance: InstanceType<TargetType>,
|
|
||||||
init:
|
|
||||||
| InitDelegate<
|
|
||||||
TargetType &
|
|
||||||
(new (..._args: unknown[]) => InstanceType<TargetType>),
|
|
||||||
InstanceType<TargetType>
|
|
||||||
>
|
|
||||||
| undefined,
|
|
||||||
constructor: TargetType,
|
|
||||||
args: unknown[],
|
|
||||||
lazyProxy: unknown,
|
|
||||||
): { instance: InstanceType<TargetType>; lazyProxy: unknown } {
|
|
||||||
if (instance == null) {
|
|
||||||
if (init) {
|
|
||||||
instance = init(constructor);
|
|
||||||
} else {
|
|
||||||
instance = new constructor(...args);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
lazyProxy = instance;
|
|
||||||
|
|
||||||
return { instance, lazyProxy };
|
const diContainer = TSinjex.getInstance();
|
||||||
|
let _instance: InstanceType<ClassType> | undefined;
|
||||||
|
|
||||||
|
if (init == undefined) {
|
||||||
|
diContainer.register(identifier, target, deprecated);
|
||||||
|
} else {
|
||||||
|
diContainer.register(
|
||||||
|
identifier,
|
||||||
|
createLazyProxy(
|
||||||
|
_instance,
|
||||||
|
init,
|
||||||
|
target,
|
||||||
|
),
|
||||||
|
deprecated,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLazyProxy<ClassType extends ClassConstructor>(
|
||||||
|
instance: InstanceType<ClassType> | undefined,
|
||||||
|
init: InitDelegate<ClassType, InstanceType<ClassType>>,
|
||||||
|
constructor: ClassType,
|
||||||
|
): InstanceType<ClassType> {
|
||||||
|
const initializeInstance = (
|
||||||
|
instance: InstanceType<ClassType> | undefined,
|
||||||
|
init: InitDelegate<ClassType, InstanceType<ClassType>>,
|
||||||
|
constructor: ClassType,
|
||||||
|
) => {
|
||||||
|
if (instance == undefined) {
|
||||||
|
if (init != undefined) {
|
||||||
|
instance = init(constructor);
|
||||||
|
} else {
|
||||||
|
instance = new constructor() as InstanceType<ClassType>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { instance: instance, lazyProxy: instance };
|
||||||
|
};
|
||||||
|
|
||||||
|
let lazyProxy = new Proxy(
|
||||||
|
{} as InstanceType<ClassType>,
|
||||||
|
{
|
||||||
|
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<ClassType>];
|
||||||
|
},
|
||||||
|
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<ClassType>] = value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return lazyProxy;
|
||||||
}
|
}
|
||||||
|
2
src/decorators/mod.ts
Normal file
2
src/decorators/mod.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { Register } from "./Register.ts";
|
||||||
|
export { Inject } from "./Inject.ts";
|
0
src/export.ts
Normal file
0
src/export.ts
Normal file
0
src/functions/mod.ts
Normal file
0
src/functions/mod.ts
Normal file
@@ -6,5 +6,5 @@
|
|||||||
* @returns A decorator function
|
* @returns A decorator function
|
||||||
*/
|
*/
|
||||||
export function ImplementsStatic<I>() {
|
export function ImplementsStatic<I>() {
|
||||||
return <T extends I>(constructor: T, ...args: unknown[]) => {};
|
return <T extends I>(_constructor: T, ..._args: unknown[]) => {};
|
||||||
}
|
}
|
||||||
|
1
src/helper/mod.ts
Normal file
1
src/helper/mod.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ImplementsStatic } from "./ImplementsStatic.ts";
|
@@ -1,5 +1,4 @@
|
|||||||
import { Identifier } from 'src/types/Identifier.js';
|
import { Identifier } from "../types/mod.ts";
|
||||||
import { ITSinjex } from './ITSinjex.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General error class for {@link ITSinjex} interface.
|
* General error class for {@link ITSinjex} interface.
|
||||||
@@ -11,7 +10,7 @@ export class TSinjexError extends Error {
|
|||||||
*/
|
*/
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'TSinjex';
|
this.name = "TSinjex";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ export class DependencyResolutionError extends TSinjexError {
|
|||||||
*/
|
*/
|
||||||
constructor(identifier: Identifier) {
|
constructor(identifier: Identifier) {
|
||||||
super(`Dependency ${identifier.toString()} could not be resolved.`);
|
super(`Dependency ${identifier.toString()} could not be resolved.`);
|
||||||
this.name = 'TSinjexResolutionError';
|
this.name = "TSinjexResolutionError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ export class InjectorError extends TSinjexError {
|
|||||||
super(
|
super(
|
||||||
`Error injecting dependency ${identifier.toString()} with error: "${originalError}"`,
|
`Error injecting dependency ${identifier.toString()} with error: "${originalError}"`,
|
||||||
);
|
);
|
||||||
this.name = 'TSinjexInjectorError';
|
this.name = "TSinjexInjectorError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ export class NoInstantiationMethodError extends TSinjexError {
|
|||||||
super(
|
super(
|
||||||
`No instantiation method found for dependency ${identifier.toString()}.`,
|
`No instantiation method found for dependency ${identifier.toString()}.`,
|
||||||
);
|
);
|
||||||
this.name = 'TSinjexNoInstantiationMethodError';
|
this.name = "TSinjexNoInstantiationMethodError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +78,6 @@ export class InitializationError extends TSinjexError {
|
|||||||
super(
|
super(
|
||||||
`Error initializing dependency ${identifier.toString()} with error: "${originalError}"`,
|
`Error initializing dependency ${identifier.toString()} with error: "${originalError}"`,
|
||||||
);
|
);
|
||||||
this.name = 'TSinjexInitializationError';
|
this.name = "TSinjexInitializationError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { DependencyResolutionError } from './Exceptions.js';
|
import { Identifier } from "../types/mod.ts";
|
||||||
import { Identifier } from '../types/Identifier.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static TSInjex Interface
|
* Static TSInjex Interface
|
||||||
|
9
src/interfaces/mod.ts
Normal file
9
src/interfaces/mod.ts
Normal file
@@ -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";
|
0
src/mod.ts
Normal file
0
src/mod.ts
Normal file
18
src/types/Constructor.ts
Normal file
18
src/types/Constructor.ts
Normal file
@@ -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<T>,
|
||||||
|
> = new (...args: ConstructorParameters<T>) => T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force generic constructor type.
|
||||||
|
* This type is used to force a class to has a constructor.
|
||||||
|
*/
|
||||||
|
export type ForceConstructor<T> = new (...args: unknown[]) => T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents any concrete (non-abstract) class constructor.
|
||||||
|
*/
|
||||||
|
export type ClassConstructor = new (...args: unknown[]) => object;
|
5
src/types/mod.ts
Normal file
5
src/types/mod.ts
Normal file
@@ -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";
|
229
tests/Decorators.ts
Normal file
229
tests/Decorators.ts
Normal file
@@ -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<TestClass, any, any>("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<TestClass>("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<TestClass>("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<TestClass>("InstanceIdentifier");
|
||||||
|
if (instance1 == null) {
|
||||||
|
throw new Error("Instance1 is null");
|
||||||
|
}
|
||||||
|
instance1.test = "test2";
|
||||||
|
|
||||||
|
const instance2 = container.resolve<TestClass>("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<TestClass>("InstanceIdentifier");
|
||||||
|
if (instance1 == null) {
|
||||||
|
throw new Error("Instance1 is null");
|
||||||
|
}
|
||||||
|
instance1.test = "test2";
|
||||||
|
|
||||||
|
const instance2 = container.resolve<TestClass>("InstanceIdentifier");
|
||||||
|
if (instance2 == null) {
|
||||||
|
throw new Error("Instance2 is null");
|
||||||
|
}
|
||||||
|
assertEquals(instance2.test, "test2");
|
||||||
|
});
|
78
tests/TSInjex.ts
Normal file
78
tests/TSInjex.ts
Normal file
@@ -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<typeof dependency>(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<typeof dependency>(identifier);
|
||||||
|
|
||||||
|
assertStrictEquals(resolved, dependency);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("should throw error when resolving non-registered dependency (static)", () => {
|
||||||
|
container.clear();
|
||||||
|
|
||||||
|
const identifier = "nonExistentDependency";
|
||||||
|
assertThrows(() => {
|
||||||
|
TSinjex.resolve<unknown>(identifier);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("should return undefined when resolving non-necessary dependency", () => {
|
||||||
|
container.clear();
|
||||||
|
|
||||||
|
const result = TSinjex.resolve<unknown>("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<typeof dependency>(identifier);
|
||||||
|
assertStrictEquals(resolved, dependency);
|
||||||
|
if (!wasCalled) {
|
||||||
|
throw new Error("console.warn was not called");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
console.warn = originalWarn; // Restore
|
||||||
|
}
|
||||||
|
});
|
Reference in New Issue
Block a user