From 6c4db199265cc57940e6551fc7162ca3cc80ef0b Mon Sep 17 00:00:00 2001 From: Max P Date: Wed, 14 Aug 2024 19:40:52 +0200 Subject: [PATCH] First check-in of the code from the Obsidian Prj project. --- src/DIContainer.ts | 96 ++++++++++++++++++++++++++++++ src/__tests__/DIContainer.test.ts | 4 ++ src/__tests__/IDIContainer.spec.ts | 35 +++++++++++ src/decorators/Inject.ts | 76 +++++++++++++++++++++++ src/decorators/Register.ts | 28 +++++++++ src/decorators/RegisterInstance.ts | 70 ++++++++++++++++++++++ src/functions/Register.ts | 35 +++++++++++ src/functions/Resolve.ts | 33 ++++++++++ src/helper/ImplementsStatic.ts | 7 +++ src/index.ts | 0 src/interfaces/Exceptions.ts | 30 ++++++++++ src/interfaces/IDIContainer.ts | 60 +++++++++++++++++++ src/types/GenericContructor.ts | 12 ++++ src/types/InitDelegate.ts | 9 +++ 14 files changed, 495 insertions(+) create mode 100644 src/DIContainer.ts create mode 100644 src/__tests__/DIContainer.test.ts create mode 100644 src/__tests__/IDIContainer.spec.ts create mode 100644 src/decorators/Inject.ts create mode 100644 src/decorators/Register.ts create mode 100644 src/decorators/RegisterInstance.ts create mode 100644 src/functions/Register.ts create mode 100644 src/functions/Resolve.ts create mode 100644 src/helper/ImplementsStatic.ts create mode 100644 src/index.ts create mode 100644 src/interfaces/Exceptions.ts create mode 100644 src/interfaces/IDIContainer.ts create mode 100644 src/types/GenericContructor.ts create mode 100644 src/types/InitDelegate.ts diff --git a/src/DIContainer.ts b/src/DIContainer.ts new file mode 100644 index 0000000..f1b8125 --- /dev/null +++ b/src/DIContainer.ts @@ -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() +export class DIContainer implements ITSInjex { + private static _instance: DIContainer; + private readonly _dependencies = new Map(); + + /** + * 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( + 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(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 +} diff --git a/src/__tests__/DIContainer.test.ts b/src/__tests__/DIContainer.test.ts new file mode 100644 index 0000000..efcc40b --- /dev/null +++ b/src/__tests__/DIContainer.test.ts @@ -0,0 +1,4 @@ +import { test_IDIContainer } from './IDIContainer.spec'; +import { DIContainer } from '../DIContainer'; + +test_IDIContainer(DIContainer); diff --git a/src/__tests__/IDIContainer.spec.ts b/src/__tests__/IDIContainer.spec.ts new file mode 100644 index 0000000..988e386 --- /dev/null +++ b/src/__tests__/IDIContainer.spec.ts @@ -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(identifier); + expect(resolvedDependency).toBe(dependency); + }); + + it('should throw an error when resolving a non-registered dependency', () => { + const identifier = 'nonExistentDependency'; + + expect(() => container.resolve(identifier)).toThrow(); + }); + + // Add more tests as necessary + }); +} diff --git a/src/decorators/Inject.ts b/src/decorators/Inject.ts new file mode 100644 index 0000000..44382cf --- /dev/null +++ b/src/decorators/Inject.ts @@ -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('MyDependencyIdentifier') + * private myDependency!: MyDependency; + * } + * ``` + * @example + * ```ts + * class MyClass { + * \@Inject('ILogger_', (x: ILogger_) => x.getLogger('Tags'), false) + * private _logger?: ILogger; + * } + * ``` + */ +export function Inject( + identifier: string, + init?: InitDelegate, + 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(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, + }); + }; +} diff --git a/src/decorators/Register.ts b/src/decorators/Register.ts new file mode 100644 index 0000000..c8b93fc --- /dev/null +++ b/src/decorators/Register.ts @@ -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, +>(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); + }; +} diff --git a/src/decorators/RegisterInstance.ts b/src/decorators/RegisterInstance.ts new file mode 100644 index 0000000..b75f2dc --- /dev/null +++ b/src/decorators/RegisterInstance.ts @@ -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, +>( + identifier: string, + init?: InitDelegate< + TargetType & { new (..._args: unknown[]): InstanceType }, + InstanceType + >, +) { + 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; + + if (init) { + instance = init(constructor); + } else { + instance = new constructor(...args); + } + lazyProxy = instance; + + // Return the requested property of the instance + return instance[prop as keyof InstanceType]; + }, + set(target, prop, value, receiver) { + let instance: InstanceType; + + if (init) { + instance = init(constructor); + } else { + instance = new constructor(...args); + } + lazyProxy = instance; + + // Set the requested property of the instance + return (instance[prop as keyof InstanceType] = + value); + }, + }, + ); + + // Register the lazy proxy in the DI container + diContainer.register(identifier, lazyProxy); + }; +} diff --git a/src/functions/Register.ts b/src/functions/Register.ts new file mode 100644 index 0000000..dc08225 --- /dev/null +++ b/src/functions/Register.ts @@ -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(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( + 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( + identifier: string, + dependency: T, + deprecated?: boolean, +): void { + DIContainer.getInstance().register(identifier, dependency, deprecated); +} diff --git a/src/functions/Resolve.ts b/src/functions/Resolve.ts new file mode 100644 index 0000000..482f87f --- /dev/null +++ b/src/functions/Resolve.ts @@ -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(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(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( + identifier: string, + necessary?: boolean, +): T | undefined { + return DIContainer.getInstance().resolve(identifier, necessary); +} diff --git a/src/helper/ImplementsStatic.ts b/src/helper/ImplementsStatic.ts new file mode 100644 index 0000000..00d3bf2 --- /dev/null +++ b/src/helper/ImplementsStatic.ts @@ -0,0 +1,7 @@ +/** + * Decorator to enforce static implementation of an interface + * @returns A decorator function + */ +export function ImplementsStatic() { + return (constructor: T, ...args: unknown[]) => {}; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/interfaces/Exceptions.ts b/src/interfaces/Exceptions.ts new file mode 100644 index 0000000..0b11c47 --- /dev/null +++ b/src/interfaces/Exceptions.ts @@ -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'; + } +} diff --git a/src/interfaces/IDIContainer.ts b/src/interfaces/IDIContainer.ts new file mode 100644 index 0000000..69032b5 --- /dev/null +++ b/src/interfaces/IDIContainer.ts @@ -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(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(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(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(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(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(identifier: string, necessary?: false): T | undefined; +} diff --git a/src/types/GenericContructor.ts b/src/types/GenericContructor.ts new file mode 100644 index 0000000..0b8ffe0 --- /dev/null +++ b/src/types/GenericContructor.ts @@ -0,0 +1,12 @@ +/** + * Generic constructor type. + */ +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 be a constructor. + */ +export type ForceConstructor = new (...args: unknown[]) => T; diff --git a/src/types/InitDelegate.ts b/src/types/InitDelegate.ts new file mode 100644 index 0000000..343cfed --- /dev/null +++ b/src/types/InitDelegate.ts @@ -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 = (x: T) => U;