implements typed identifiers
This commit is contained in:
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
6
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';
|
||||
|
||||
79
readme.md
79
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<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
|
||||
Reference in New Issue
Block a user