First check-in of the code from the Obsidian Prj project.

This commit is contained in:
2024-08-14 19:40:52 +02:00
parent 1341427590
commit 6c4db19926
14 changed files with 495 additions and 0 deletions

96
src/DIContainer.ts Normal file
View 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
}

View File

@@ -0,0 +1,4 @@
import { test_IDIContainer } from './IDIContainer.spec';
import { DIContainer } from '../DIContainer';
test_IDIContainer(DIContainer);

View 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
View 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,
});
};
}

View 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);
};
}

View 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
View 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
View 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);
}

View 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
View File

View 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';
}
}

View 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;
}

View 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;

View 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;