First check-in of the code from the Obsidian Prj project.
This commit is contained in:
96
src/DIContainer.ts
Normal file
96
src/DIContainer.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ImplementsStatic } from './helper/ImplementsStatic';
|
||||
import { ITSInjex, ITSInjex_ } from './interfaces/IDIContainer';
|
||||
|
||||
/**
|
||||
* Dependency Entry Interface
|
||||
*/
|
||||
interface IDependency {
|
||||
/**
|
||||
* The dependency itself
|
||||
*/
|
||||
dependency: unknown;
|
||||
/**
|
||||
* If true, the dependency is deprecated => a warning
|
||||
* is logged when the dependency is resolved
|
||||
*/
|
||||
deprecated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency Injection Container
|
||||
*/
|
||||
@ImplementsStatic<ITSInjex_>()
|
||||
export class DIContainer implements ITSInjex {
|
||||
private static _instance: DIContainer;
|
||||
private readonly _dependencies = new Map<string, IDependency>();
|
||||
|
||||
/**
|
||||
* Private constructor to prevent direct instantiation.
|
||||
*/
|
||||
private constructor() {}
|
||||
|
||||
//#region IDIContainer_
|
||||
|
||||
/**
|
||||
* Retrieves the singleton instance of DependencyRegistry.
|
||||
* @returns The singleton instance.
|
||||
*/
|
||||
public static getInstance(): ITSInjex {
|
||||
if (this._instance == null) {
|
||||
this._instance = new DIContainer();
|
||||
}
|
||||
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region IDIContainer
|
||||
|
||||
/**
|
||||
* Register a dependency
|
||||
* @param identifier The identifier of the dependency
|
||||
* @param dependency The dependency to register
|
||||
* @param deprecated If true, the dependency is deprecated => a warning
|
||||
* is logged when the dependency is resolved
|
||||
*/
|
||||
public register<T>(
|
||||
identifier: string,
|
||||
dependency: T,
|
||||
deprecated = false,
|
||||
): void {
|
||||
this._dependencies.set(identifier, {
|
||||
dependency: dependency,
|
||||
deprecated: deprecated,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a dependency
|
||||
* @param identifier The identifier of the dependency
|
||||
* @param necessary If true, throws an error if the dependency is not found
|
||||
* @returns The resolved dependency or undefined if the dependency is not found (if necessary is false)
|
||||
* @throws Error if the dependency is not found (if necessary is true)
|
||||
*/
|
||||
public resolve<T>(identifier: string, necessary = true): T | undefined {
|
||||
const dependency = this._dependencies.get(identifier);
|
||||
|
||||
if (necessary && !dependency) {
|
||||
throw new Error(`Dependency ${identifier} not found`);
|
||||
} else if (!dependency) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (dependency.deprecated) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Dependency ${identifier} is deprecated`);
|
||||
|
||||
// Remove the deprecation warning; it should only be logged once.
|
||||
dependency.deprecated = false;
|
||||
}
|
||||
|
||||
return dependency.dependency as T;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
4
src/__tests__/DIContainer.test.ts
Normal file
4
src/__tests__/DIContainer.test.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { test_IDIContainer } from './IDIContainer.spec';
|
||||
import { DIContainer } from '../DIContainer';
|
||||
|
||||
test_IDIContainer(DIContainer);
|
35
src/__tests__/IDIContainer.spec.ts
Normal file
35
src/__tests__/IDIContainer.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ITSInjex_, ITSInjex } from '../interfaces/IDIContainer';
|
||||
|
||||
/**
|
||||
* Test the implementation of a DIContainer
|
||||
* @param Container The DIContainer implementation to test.
|
||||
* Must implement {@link ITSInjex}, {@link ITSInjex_}
|
||||
*/
|
||||
export function test_IDIContainer(Container: ITSInjex_): void {
|
||||
describe('IDIContainer Implementation Tests', () => {
|
||||
let container: ITSInjex;
|
||||
|
||||
beforeEach(() => {
|
||||
container = Container.getInstance();
|
||||
});
|
||||
|
||||
it('should register and resolve a dependency', () => {
|
||||
const identifier = 'myDependency';
|
||||
const dependency = { value: 42 };
|
||||
|
||||
container.register(identifier, dependency);
|
||||
|
||||
const resolvedDependency =
|
||||
container.resolve<typeof dependency>(identifier);
|
||||
expect(resolvedDependency).toBe(dependency);
|
||||
});
|
||||
|
||||
it('should throw an error when resolving a non-registered dependency', () => {
|
||||
const identifier = 'nonExistentDependency';
|
||||
|
||||
expect(() => container.resolve<unknown>(identifier)).toThrow();
|
||||
});
|
||||
|
||||
// Add more tests as necessary
|
||||
});
|
||||
}
|
76
src/decorators/Inject.ts
Normal file
76
src/decorators/Inject.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { DIContainer } from '../DIContainer';
|
||||
import { InitDelegate } from '../types/InitDelegate';
|
||||
|
||||
/**
|
||||
* A decorator to inject a dependency from a DI (Dependency Injection) container.
|
||||
* The dependency is lazily evaluated when the property is accessed for the first time.
|
||||
* This can help avoid issues like circular dependencies and not-found dependencies.
|
||||
* @template ClassType The type of the property to be injected.
|
||||
* @param identifier The identifier used to resolve the dependency from the DI container.
|
||||
* @param init An optional initializer function to transform the dependency before injection.
|
||||
* @param necessary Indicates if the dependency is necessary.
|
||||
* - If `true`, an error will be thrown if the dependency cannot be resolved.
|
||||
* - If `false`, `undefined` will be returned if the dependency cannot be resolved.
|
||||
* @returns A decorator function to be applied on the class property.
|
||||
* @see {@link DIContainer}
|
||||
* @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: string,
|
||||
init?: InitDelegate<T, U>,
|
||||
necessary = true,
|
||||
) {
|
||||
return function (target: unknown, propertyKey: string | symbol): void {
|
||||
// Unique symbol to store the private property
|
||||
const privatePropertyKey: unique symbol = Symbol();
|
||||
// Get the DI container instance
|
||||
const diContainer = DIContainer.getInstance();
|
||||
|
||||
// Function to evaluate the dependency lazily
|
||||
// to avoid circular dependencies, not found dependencies, etc.
|
||||
const evaluate = (): T | undefined => {
|
||||
return diContainer.resolve<T>(identifier, necessary);
|
||||
};
|
||||
|
||||
// Define the property
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get() {
|
||||
// If the property is not defined, evaluate the dependency
|
||||
if (!this.hasOwnProperty(privatePropertyKey)) {
|
||||
if (init) {
|
||||
try {
|
||||
this[privatePropertyKey] = init(evaluate() as T);
|
||||
} catch (error) {
|
||||
if (necessary) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this[privatePropertyKey] = evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
return this[privatePropertyKey];
|
||||
},
|
||||
// Not necessary to set the property
|
||||
// set(value: PropertieType) {
|
||||
// this[privatePropertyKey] = value;
|
||||
// },
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
};
|
||||
}
|
28
src/decorators/Register.ts
Normal file
28
src/decorators/Register.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { DIContainer } from '../DIContainer';
|
||||
|
||||
/**
|
||||
* A decorator to register a class in the 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.
|
||||
* @param deprecated If true, the dependency is deprecated => a warning
|
||||
* is logged when the dependency is resolved.
|
||||
* @returns A function that is applied as a decorator to the class.
|
||||
* @example
|
||||
* ```ts
|
||||
* \@Register('MyClassIdentifier')
|
||||
* class MyClass {
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Register<
|
||||
TargetType extends new (...args: unknown[]) => InstanceType<TargetType>,
|
||||
>(identifier: string, deprecated?: boolean) {
|
||||
return function (constructor: TargetType, ...args: unknown[]): void {
|
||||
// Get the instance of the DI container
|
||||
const diContainer = DIContainer.getInstance();
|
||||
|
||||
// Register the class in the DI container
|
||||
diContainer.register(identifier, constructor, deprecated);
|
||||
};
|
||||
}
|
70
src/decorators/RegisterInstance.ts
Normal file
70
src/decorators/RegisterInstance.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { DIContainer } from '../DIContainer';
|
||||
import { InitDelegate } from '../types/InitDelegate';
|
||||
|
||||
/**
|
||||
* A decorator to register an instance of a class in the DI (Dependency Injection) container.
|
||||
* The instance is created only when it is first needed (Lazy Initialization).
|
||||
* @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.
|
||||
* @param init An optional initializer function which get the constructor of the class
|
||||
* as input and returns an instance of the class.
|
||||
* @returns A function that is applied as a decorator to the class.
|
||||
* @example
|
||||
* ```ts
|
||||
* \@RegisterInstance('MyClassInstanceIdentifier', arg1, arg2)
|
||||
* class MyClass {
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function RegisterInstance<
|
||||
TargetType extends new (..._args: unknown[]) => InstanceType<TargetType>,
|
||||
>(
|
||||
identifier: string,
|
||||
init?: InitDelegate<
|
||||
TargetType & { new (..._args: unknown[]): InstanceType<TargetType> },
|
||||
InstanceType<TargetType>
|
||||
>,
|
||||
) {
|
||||
return function (constructor: TargetType, ...args: unknown[]): void {
|
||||
// Get the instance of the DI container
|
||||
const diContainer = DIContainer.getInstance();
|
||||
|
||||
// Create a proxy to instantiate the class when needed (Lazy Initialization)
|
||||
let lazyProxy: unknown = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop, receiver) {
|
||||
let instance: InstanceType<TargetType>;
|
||||
|
||||
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<TargetType>];
|
||||
},
|
||||
set(target, prop, value, receiver) {
|
||||
let instance: InstanceType<TargetType>;
|
||||
|
||||
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<TargetType>] =
|
||||
value);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Register the lazy proxy in the DI container
|
||||
diContainer.register(identifier, lazyProxy);
|
||||
};
|
||||
}
|
35
src/functions/Register.ts
Normal file
35
src/functions/Register.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { DIContainer } from 'src/DIContainer';
|
||||
|
||||
/**
|
||||
* Register a dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param dependency The dependency to register.
|
||||
*/
|
||||
export function Register<T>(identifier: string, dependency: T): void;
|
||||
|
||||
/**
|
||||
* Register a dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param dependency The dependency to register.
|
||||
* @param deprecated A warning is logged when the dependency is resolved.
|
||||
*/
|
||||
export function Register<T>(
|
||||
identifier: string,
|
||||
dependency: T,
|
||||
deprecated?: true,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Register a dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param dependency The dependency to register.
|
||||
* @param deprecated If true, the dependency is deprecated => a warning
|
||||
* is logged when the dependency is resolved.
|
||||
*/
|
||||
export function Register<T>(
|
||||
identifier: string,
|
||||
dependency: T,
|
||||
deprecated?: boolean,
|
||||
): void {
|
||||
DIContainer.getInstance().register(identifier, dependency, deprecated);
|
||||
}
|
33
src/functions/Resolve.ts
Normal file
33
src/functions/Resolve.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DIContainer } from '../DIContainer';
|
||||
import { DependencyResolutionError } from '../interfaces/Exceptions';
|
||||
|
||||
/**
|
||||
* Resolve a dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @returns The resolved dependency.
|
||||
* @throws A {@link DependencyResolutionError} if the dependency is not found.
|
||||
*/
|
||||
export function Resolve<T>(identifier: string): T;
|
||||
|
||||
/**
|
||||
* Resolve a dependency
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param necessary The dependency is **not** necessary.
|
||||
* @returns The resolved dependency or undefined if the dependency is not found.
|
||||
*/
|
||||
export function Resolve<T>(identifier: string, necessary: false): T | undefined;
|
||||
|
||||
/**
|
||||
* Resolve a dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param necessary 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 A {@link DependencyResolutionError} if the dependency is not found and necessary.
|
||||
*/
|
||||
export function Resolve<T>(
|
||||
identifier: string,
|
||||
necessary?: boolean,
|
||||
): T | undefined {
|
||||
return DIContainer.getInstance().resolve<T>(identifier, necessary);
|
||||
}
|
7
src/helper/ImplementsStatic.ts
Normal file
7
src/helper/ImplementsStatic.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Decorator to enforce static implementation of an interface
|
||||
* @returns A decorator function
|
||||
*/
|
||||
export function ImplementsStatic<I>() {
|
||||
return <T extends I>(constructor: T, ...args: unknown[]) => {};
|
||||
}
|
0
src/index.ts
Normal file
0
src/index.ts
Normal file
30
src/interfaces/Exceptions.ts
Normal file
30
src/interfaces/Exceptions.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ITSInjex } from './IDIContainer';
|
||||
|
||||
/**
|
||||
* General error class for {@link ITSInjex} interface.
|
||||
*/
|
||||
export class TSInjexError extends Error {
|
||||
/**
|
||||
* Creates a new instance of {@link TSInjexError}
|
||||
* @param message **The error message**
|
||||
*/
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'TSInjex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error class for dependency resolution errors in {@link ITSInjex}.
|
||||
* @see {@link ITSInjex.resolve}
|
||||
*/
|
||||
export class DependencyResolutionError extends TSInjexError {
|
||||
/**
|
||||
* Creates a new instance of {@link DependencyResolutionError}
|
||||
* @param identifier **The identifier of the dependency**
|
||||
*/
|
||||
constructor(identifier: string) {
|
||||
super(`Dependency ${identifier} not found.`);
|
||||
this.name = 'TSInjexResolutionError';
|
||||
}
|
||||
}
|
60
src/interfaces/IDIContainer.ts
Normal file
60
src/interfaces/IDIContainer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Static Dependency Injection Container Interface
|
||||
*/
|
||||
export interface ITSInjex_ {
|
||||
/**
|
||||
* Get the **singleton** Dependency Injection Container
|
||||
*/
|
||||
getInstance(): ITSInjex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency Injection Container Interface
|
||||
*/
|
||||
export interface ITSInjex {
|
||||
/**
|
||||
* Register a dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param dependency The dependency to register.
|
||||
* @param deprecated If true, the dependency is deprecated => a warning
|
||||
* is logged when the dependency is resolved.
|
||||
*/
|
||||
register<T>(identifier: string, dependency: T, deprecated?: boolean): void;
|
||||
/**
|
||||
* Register a deprecated dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param dependency The dependency to register.
|
||||
* @param deprecated A warning is logged when the dependency is resolved.
|
||||
*/
|
||||
register<T>(identifier: string, dependency: T, deprecated?: true): void;
|
||||
/**
|
||||
* Register a dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param dependency The dependency to register.
|
||||
* @param deprecated No warning is logged when the dependency is resolved.
|
||||
*/
|
||||
register<T>(identifier: string, dependency: T, deprecated?: false): void;
|
||||
|
||||
/**
|
||||
* Resolve a dependency
|
||||
* @param identifier The identifier of the dependency
|
||||
* @param necessary If true, throws an error if the dependency is not found
|
||||
* @returns The resolved dependency or undefined if the dependency is not found
|
||||
*/
|
||||
resolve<T>(identifier: string, necessary?: boolean): T | undefined;
|
||||
/**
|
||||
* Resolve a necessary dependency.
|
||||
* @param identifier The identifier of the dependency.
|
||||
* @param necessary If true, throws an error if the dependency is not found.
|
||||
* @returns The resolved dependency.
|
||||
* @throws Error if the dependency is not found.
|
||||
*/
|
||||
resolve<T>(identifier: string, necessary?: true): T;
|
||||
/**
|
||||
* Resolve a non necessary dependency
|
||||
* @param identifier The identifier of the dependency
|
||||
* @param necessary Not necessary, does not throw an error if the dependency is not found.
|
||||
* @returns The resolved dependency or undefined if the dependency is not found
|
||||
*/
|
||||
resolve<T>(identifier: string, necessary?: false): T | undefined;
|
||||
}
|
12
src/types/GenericContructor.ts
Normal file
12
src/types/GenericContructor.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generic constructor type.
|
||||
*/
|
||||
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 be a constructor.
|
||||
*/
|
||||
export type ForceConstructor<T> = new (...args: unknown[]) => T;
|
9
src/types/InitDelegate.ts
Normal file
9
src/types/InitDelegate.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* A function type representing an initializer that transforms an input of type `T`
|
||||
* into an output of type `U`.
|
||||
* @template T - The type of the input parameter.
|
||||
* @template U - The type of the output parameter.
|
||||
* @param x - The input parameter of type `T`.
|
||||
* @returns The transformed output of type `U`.
|
||||
*/
|
||||
export type InitDelegate<T, U> = (x: T) => U;
|
Reference in New Issue
Block a user