implements typed identifiers

This commit is contained in:
Robin Chappatte
2024-06-12 18:13:15 +02:00
parent b654a3b26f
commit c60af59bec
4 changed files with 112 additions and 155 deletions

View File

@@ -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<typeof value>;
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<typeof value>;
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<ReturnType<typeof providerA>>;
const providerB = (m: Manager) => {
const a = manager.inject(identifierA);
return `-${a}-`;
};
const identifierB = Symbol() as Identifier<ReturnType<typeof providerB>>;
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<string> = (manager) =>
manager.resolve(dependencyB);
const dependencyB: Provider<string> = (manager) =>
manager.resolve(dependencyA);
manager.register(dependencyA);
manager.register(dependencyB);
assertThrows(
() => manager.resolve(dependencyA),
Error,
'Circular dependency detected',
);
});

View File

@@ -1,57 +1,37 @@
export type Provider<T = unknown> = (manager: DependencyManager) => T;
export type Provider<T = unknown> = (manager: Manager) => T;
export interface Identifier<T = unknown> extends Symbol {}
export type ProviderGroup = {
[key: string]: Provider | ProviderGroup;
};
export class Manager {
private providers = new Map<Identifier, Provider>();
private instances = new Map<Identifier, unknown>();
private resolving = new Set<Identifier>();
export class DependencyManager {
private declarations = new Map<unknown, Provider>();
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<T>(identifier: Identifier<T>, provider: Provider<T>) {
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<T>(dependency: Provider<T>): T {
if (!this.declarations.has(dependency)) {
throw new Error('This dependency has not been registred');
inject<T>(identifier: Identifier<T>): 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<T>(dependency: Provider<T>): T {
const declaration = this.declarations.get(dependency) as Provider<T>;
this.resolving.add(identifier);
const declaration = this.providers.get(identifier) as Provider<T>;
const instance = declaration(this);
this.instances.set(dependency, instance);
this.instances.set(identifier, instance);
this.resolving.delete(identifier);
return instance;
}

6
mod.ts
View File

@@ -1 +1,5 @@
export * from './dependency-manager.ts';
export {
type Identifier as DependencyIdentifier,
Manager as DependencyManager,
type Provider as DependencyProvider,
} from './dependency-manager.ts';

View File

@@ -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<typeof value>;
// 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<ReturnType<typeof providerA>>;
## Typing
function providerB(manager: DependencyManager) {
const a = manager.inject(identifierA);
return `The value is: ${a}`;
}
const identifierB = Symbol() as DependencyIdentifier<ReturnType<typeof providerB>>;
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