From c60af59becb2ed39ef50f664833c3a5858bed011 Mon Sep 17 00:00:00 2001 From: Robin Chappatte Date: Wed, 12 Jun 2024 18:13:15 +0200 Subject: [PATCH] implements typed identifiers --- dependency-manager.test.ts | 122 ++++++++++++++++--------------------- dependency-manager.ts | 60 ++++++------------ mod.ts | 6 +- readme.md | 79 ++++++++++-------------- 4 files changed, 112 insertions(+), 155 deletions(-) diff --git a/dependency-manager.test.ts b/dependency-manager.test.ts index 430b2be..32d3ce9 100644 --- a/dependency-manager.test.ts +++ b/dependency-manager.test.ts @@ -3,89 +3,75 @@ import { assertThrows, } from 'https://deno.land/std@0.224.0/assert/mod.ts'; -import { DependencyManager, Provider } from './dependency-manager.ts'; +import { Identifier, Manager, Provider } from './dependency-manager.ts'; -Deno.test('should register and resolve a simple dependency', () => { - const manager = new DependencyManager(); +Deno.test('provide: should throw error when an identifier is reused', () => { + const value = 'test'; + const identifier = Symbol() as Identifier; - const dependency = () => 'test'; - manager.register(dependency); + const manager = new Manager(); + manager.register(identifier, () => value); - const resolved = manager.resolve(dependency); - assertEquals(resolved, 'test'); + assertThrows( + () => manager.register(identifier, () => value), + Error, + ); }); -Deno.test('should register and resolve a module with multiple dependencies', () => { - const manager = new DependencyManager(); +Deno.test('provide: should throw error when resolving a non-registered dependency', () => { + const identifier = Symbol(); + const manager = new Manager(); - const module = { - a: () => 'test1', - b: () => 42, + assertThrows( + () => manager.inject(identifier), + Error, + ); +}); + +Deno.test('inject: should return the provided value (stand-alone injectable)', () => { + const value = 'foo'; + const identifier = Symbol() as Identifier; + + const manager = new Manager(); + manager.register(identifier, () => value); + + const resolved = manager.inject(identifier); + assertEquals(resolved, 'foo'); +}); + +Deno.test('inject: should return the expected value (injectable with dependencies)', () => { + const providerA = () => 'a'; + const identifierA = Symbol() as Identifier>; + + const providerB = (m: Manager) => { + const a = manager.inject(identifierA); + + return `-${a}-`; }; + const identifierB = Symbol() as Identifier>; - manager.register(module); + const manager = new Manager(); + manager.register(identifierA, providerA); + manager.register(identifierB, providerB); - const a = manager.resolve(module.a); - const b = manager.resolve(module.b); + const b = manager.inject(identifierB); - assertEquals(a, 'test1'); - assertEquals(b, 42); + assertEquals(b, '-a-'); }); -Deno.test('should throw error when registering the same dependency twice', () => { - const manager = new DependencyManager(); +Deno.test('inject: should throw error when detecting a circular dependency', () => { + const identifierA = Symbol(); + const identifierB = Symbol(); - const dependency = () => 'test'; - manager.register(dependency); + const providerA = (manager: Manager) => manager.inject(identifierB); + const providerB = (manager: Manager) => manager.inject(identifierA); + + const manager = new Manager(); + manager.register(identifierA, providerA); + manager.register(identifierB, providerB); assertThrows( - () => manager.register(dependency), + () => manager.inject(identifierA), Error, - 'This dependency or module has already been registered', - ); -}); - -Deno.test('should throw error when resolving a non-registered dependency', () => { - const manager = new DependencyManager(); - - const dependency = () => 'test'; - - assertThrows( - () => manager.resolve(dependency), - Error, - 'This key has not (yet ?) been used to register something', - ); -}); - -Deno.test('should resolve dependency recursively (when not circular)', () => { - const manager = new DependencyManager(); - - const dependencyA = () => 'A'; - const dependencyB = (manager: DependencyManager) => - manager.resolve(dependencyA); - - manager.register(dependencyA); - manager.register(dependencyB); - - const b = manager.resolve(dependencyB); - - assertEquals(b, 'A'); -}); - -Deno.test('should throw error when detecting a circular dependency', () => { - const manager = new DependencyManager(); - - const dependencyA: Provider = (manager) => - manager.resolve(dependencyB); - const dependencyB: Provider = (manager) => - manager.resolve(dependencyA); - - manager.register(dependencyA); - manager.register(dependencyB); - - assertThrows( - () => manager.resolve(dependencyA), - Error, - 'Circular dependency detected', ); }); diff --git a/dependency-manager.ts b/dependency-manager.ts index e777050..be36b88 100644 --- a/dependency-manager.ts +++ b/dependency-manager.ts @@ -1,57 +1,37 @@ -export type Provider = (manager: DependencyManager) => T; +export type Provider = (manager: Manager) => T; +export interface Identifier extends Symbol {} -export type ProviderGroup = { - [key: string]: Provider | ProviderGroup; -}; +export class Manager { + private providers = new Map(); + private instances = new Map(); + private resolving = new Set(); -export class DependencyManager { - private declarations = new Map(); - private instances = new Map(); - private resolving = new Set(); - - register(dependency: Provider): void; - register(module: ProviderGroup): void; - register(dependencyOrModule: Provider | ProviderGroup): void; - register(dependencyOrModule: Provider | ProviderGroup) { - if (this.declarations.has(dependencyOrModule)) { - throw new Error('This dependency has already been registred'); + register(identifier: Identifier, provider: Provider) { + if (this.providers.has(identifier)) { + throw new Error('This identifier is already used'); } - if (typeof dependencyOrModule === 'function') { - this.declarations.set(dependencyOrModule, dependencyOrModule); - return; - } - - const module = dependencyOrModule; - for (const key in module) { - this.register(module[key]); - } + this.providers.set(identifier, provider); } - resolve(dependency: Provider): T { - if (!this.declarations.has(dependency)) { - throw new Error('This dependency has not been registred'); + inject(identifier: Identifier): T { + if (!this.providers.has(identifier)) { + throw new Error('This identifier has not been registred'); } - if (this.instances.has(dependency)) { - return this.instances.get(dependency) as T; + if (this.instances.has(identifier)) { + return this.instances.get(identifier) as T; } - if (this.resolving.has(dependency)) { + if (this.resolving.has(identifier)) { throw new Error('Circular dependency detected'); } - this.resolving.add(dependency); - const instance = this.instanciate(dependency); - this.resolving.delete(dependency); - - return instance; - } - - private instanciate(dependency: Provider): T { - const declaration = this.declarations.get(dependency) as Provider; + this.resolving.add(identifier); + const declaration = this.providers.get(identifier) as Provider; const instance = declaration(this); - this.instances.set(dependency, instance); + this.instances.set(identifier, instance); + this.resolving.delete(identifier); return instance; } diff --git a/mod.ts b/mod.ts index 9e243a9..3914a19 100644 --- a/mod.ts +++ b/mod.ts @@ -1 +1,5 @@ -export * from './dependency-manager.ts'; +export { + type Identifier as DependencyIdentifier, + Manager as DependencyManager, + type Provider as DependencyProvider, +} from './dependency-manager.ts'; diff --git a/readme.md b/readme.md index 1b35fdc..824b7a0 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,9 @@ This lib provides a simplistic dependency manager. - Resolves dependencies only when they are needed - Detects circular dependency -- Allows "module" registration (grouped dependencies) +- Strong type support + +The [previous major release](https://git.chpt.dev/Chappatte/Typescript-Dependency-Manager/src/tag/v1.1.1) used to allow asynchronous dependencies and the use of class as identifier, but those features were replaced by the `Symbol` identifier to keep the lib dead simple. ## Basic usage @@ -16,66 +18,51 @@ import { DependencyManager } from 'dependency-manager/mod.ts' // 1. Create a manager: const manager = new DependencyManager(); -// 2. Create a dependency: -const dependency = () => 'value'; +// 2. Create a dependency (a provider and an identifier): +const value = 'foo'; +const provider = () => value; +const identifier = Symbol() as DependencyIdentifier; // 3. Register the dependency: -manager.register(dependency); +manager.register(identifier, provider); -// 4. Anywhere in your code, get the resolved dependency value: -const value = await manager.resolve(dependency); +// 4. Anywhere in your code, inject the resolved dependency value: +const value = await manager.inject(identifier); ``` +## Identifier + +An identifier is a uniq value used by the dependency manager to store and retrieve the dependency. + +The `DependencyIdentifier` type allow the return value of the `inject` method to be fully typed, + ## Providers -To register a dependency, we pass a function to the dependency manager. -This function is called a provider. +A dependency is rarely just a simple value like in the basic usage code and often rely on other dependencies. -Providers are called when resolving the dependency for the first time. -They receive the dependency manager as parameter, and should return the dependency value. +That's why the `register` method takes a function (that we call the dependency `provider`) who's role is to build the dependency. -Example of the registration of a dependency which provider uses another dependency: +That function receive a uniq parameter: The dependency manager. +That way it is possible to inject dependencies inside it: ```typescript -function dependency(manager: DependencyManager) { - const value = manager.resolve(otherDependency); - return `The value is: ${valueA}`; +function providerA() { + return 'foo' } -manager.register(dependency); -manager.register(otherDependency); -``` +const identifierA = Symbol() as DependencyIdentifier>; -## Typing +function providerB(manager: DependencyManager) { + const a = manager.inject(identifierA); + return `The value is: ${a}`; +} +const identifierB = Symbol() as DependencyIdentifier>; -The reason we pass the provider to resolve a dependency is that it allow the `resolve` method to correctly type the returned value: +manager.register(identifierA, providerA); +manager.register(identifierB, providerB); -```typescript -const A = () => 'foo'; -const B = () => 42; -const C = () => ({}); +// ... -const a = manager.resolve(A); //< `a` is of type `string` -const b = manager.resolve(B); //< `b` is of type `number` -const c = manager.resolve(C); //< `c` is of type `object` -``` - -## Modules - -It is possible to register many dependency at once by using "modules". - -A module is an object whose values are dependencies or modules. - -```typescript -const moduleA = { - dependencyB: () => 'b', - moduleC: { - dependencyD: () => 'd', - }, -}; - -manager.register(moduleA); - -const d = manager.resolve(moduleA.moduleC.dependencyD); +const b = manager.inject(identifierB); // "The value is: foo" ``` ## Errors @@ -83,6 +70,6 @@ const d = manager.resolve(moduleA.moduleC.dependencyD); The `register` method throw an error if: - the dependency is already registred. -The `resolve` method throw an error if: +The `inject` method throw an error if: - the dependency is not registred. - a circular dependency is detected \ No newline at end of file