3 Commits

Author SHA1 Message Date
Robin Chappatte
c60af59bec implements typed identifiers 2024-06-12 18:13:15 +02:00
Robin Chappatte
b654a3b26f provider is key 2024-06-10 16:27:21 +02:00
Robin Chappatte
9bb3e28b34 document the case of dependency class with private constructor 2024-06-01 16:06:50 +02:00
6 changed files with 182 additions and 163 deletions

7
deno.json Normal file
View File

@@ -0,0 +1,7 @@
{
"fmt": {
"semiColons": true,
"singleQuote": true,
"useTabs": true
}
}

38
deno.lock generated Normal file
View File

@@ -0,0 +1,38 @@
{
"version": "3",
"remote": {
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e"
}
}

View File

@@ -1,77 +1,77 @@
import {
assertEquals,
assertRejects,
assertThrows,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { DependencyManager } from "./dependency-manager.ts";
assertEquals,
assertThrows,
} from 'https://deno.land/std@0.224.0/assert/mod.ts';
// Test case for registering a module
Deno.test("DependencyManager: register a module", () => {
const manager = new DependencyManager();
const provider = () => "dependency";
manager.register("identifier", provider);
assertEquals(
manager.resolve("identifier"),
Promise.resolve("dependency"),
);
import { Identifier, Manager, Provider } from './dependency-manager.ts';
Deno.test('provide: should throw error when an identifier is reused', () => {
const value = 'test';
const identifier = Symbol() as Identifier<typeof value>;
const manager = new Manager();
manager.register(identifier, () => value);
assertThrows(
() => manager.register(identifier, () => value),
Error,
);
});
// Test case for resolving a module
Deno.test("DependencyManager: resolve a module", async () => {
const manager = new DependencyManager();
const provider = () => "dependency";
manager.register("identifier", provider);
const result = await manager.resolve("identifier");
assertEquals(result, "dependency");
Deno.test('provide: should throw error when resolving a non-registered dependency', () => {
const identifier = Symbol();
const manager = new Manager();
assertThrows(
() => manager.inject(identifier),
Error,
);
});
// Test case for resolving an async module
Deno.test("DependencyManager: resolve an async module", async () => {
const manager = new DependencyManager();
const provider = () => {
return new Promise((resolve) =>
setTimeout(() => resolve("dependency"), 100)
);
};
manager.register("identifier", provider);
const result = await manager.resolve("identifier");
assertEquals(result, "dependency");
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');
});
// Test case for handling circular dependencies
Deno.test("DependencyManager: circular dependency detection", async () => {
const manager = new DependencyManager();
const providerA = async () => await manager.resolve("B");
const providerB = async () => await manager.resolve("A");
Deno.test('inject: should return the expected value (injectable with dependencies)', () => {
const providerA = () => 'a';
const identifierA = Symbol() as Identifier<ReturnType<typeof providerA>>;
manager.register("A", providerA);
manager.register("B", providerB);
const providerB = (m: Manager) => {
const a = manager.inject(identifierA);
await assertRejects(
() => manager.resolve("A"),
Error,
"Circular dependency detected for module A.",
);
return `-${a}-`;
};
const identifierB = Symbol() as Identifier<ReturnType<typeof providerB>>;
const manager = new Manager();
manager.register(identifierA, providerA);
manager.register(identifierB, providerB);
const b = manager.inject(identifierB);
assertEquals(b, '-a-');
});
// Test case for handling duplicate module registration
Deno.test("DependencyManager: duplicate module registration", () => {
const manager = new DependencyManager();
const provider = () => "dependency";
manager.register("identifier", provider);
assertThrows(
() => manager.register("identifier", provider),
Error,
"Module identifier is already registered.",
);
});
Deno.test('inject: should throw error when detecting a circular dependency', () => {
const identifierA = Symbol();
const identifierB = Symbol();
// Test case for handling unresolved modules
Deno.test("DependencyManager: resolve an unregistered module", async () => {
const manager = new DependencyManager();
await assertRejects(
() => manager.resolve("unregistered-identifier"),
Error,
"Module unregistered-identifier is not registered.",
);
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.inject(identifierA),
Error,
);
});

View File

@@ -1,75 +1,38 @@
type Provider<T> = (manager: DependencyManager) => Promise<T> | T;
export type Provider<T = unknown> = (manager: Manager) => T;
export interface Identifier<T = unknown> extends Symbol {}
// deno-lint-ignore no-explicit-any
type HasConstructor<T> = T extends new (...args: any[]) => unknown ? T : never;
// deno-lint-ignore no-explicit-any
type ConstructorInstance<T> = T extends new (...args: any[]) => infer I ? I
: never;
export class Manager {
private providers = new Map<Identifier, Provider>();
private instances = new Map<Identifier, unknown>();
private resolving = new Set<Identifier>();
// deno-lint-ignore no-explicit-any
type ModuleIdentifier = any;
register<T>(identifier: Identifier<T>, provider: Provider<T>) {
if (this.providers.has(identifier)) {
throw new Error('This identifier is already used');
}
type Modules = Map<ModuleIdentifier, {
// deno-lint-ignore no-explicit-any
provider: Provider<any>;
// deno-lint-ignore no-explicit-any
instance?: any;
}>;
this.providers.set(identifier, provider);
}
export class DependencyManager {
private modules: Modules = new Map();
private resolving: Set<ModuleIdentifier> = new Set();
inject<T>(identifier: Identifier<T>): T {
if (!this.providers.has(identifier)) {
throw new Error('This identifier has not been registred');
}
/**
* Set the given provider to be used to resolve the dependency identified by the given identifier.
*
* Throw an error if the identifier is already used.
*
* @param identifier the identifier that will be used to ask the manager for the resolved dependency.
* @param provider a function that receive the dependency manager as param and return the dependency. Can be asynchronous.
*/
register<T>(identifier: ModuleIdentifier, provider: Provider<T>): void {
if (this.modules.has(identifier)) {
throw new Error(`Module ${identifier} is already registered.`);
}
this.modules.set(identifier, { provider });
}
if (this.instances.has(identifier)) {
return this.instances.get(identifier) as T;
}
/**
* Return the dependency matching the given identifier.
* If the dependency has not been resolved yet, resolve it first.
*
* Throw an error if:
* - no provider has been registred with that identifier
* - a circular dependency is detected
*
* @param identifier the identifier used to register the provider
*/
async resolve<T>(
identifier: HasConstructor<T>,
): Promise<ConstructorInstance<T>>;
async resolve<T>(identifier: ModuleIdentifier): Promise<T>;
async resolve(identifier: ModuleIdentifier) {
const module = this.modules.get(identifier);
if (!module) {
throw new Error(`Module ${identifier} is not registered.`);
}
if (this.resolving.has(identifier)) {
throw new Error('Circular dependency detected');
}
if (module.instance) {
return module.instance;
}
this.resolving.add(identifier);
const declaration = this.providers.get(identifier) as Provider<T>;
const instance = declaration(this);
this.instances.set(identifier, instance);
this.resolving.delete(identifier);
if (this.resolving.has(identifier)) {
throw new Error(`Circular dependency detected for module ${identifier}.`);
}
this.resolving.add(identifier);
try {
module.instance = await module.provider(this);
return module.instance;
} finally {
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

@@ -5,64 +5,71 @@ This lib provides a simplistic dependency manager.
## Features
- Resolves dependencies only when they are needed
- Detect circular dependency
- Accept any valid `Map` key as dependency identifier
- Infer the dependency type from the dependency identifier if the later is a class
- Detects circular dependency
- Strong type support
## Usage
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.
1. Create a manager:
## Basic usage
```typescript
import { DependencyManager } from 'dependency-manager/mod.ts'
const manager = new DependencyManager()
// 1. Create a manager:
const manager = new DependencyManager();
// 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(identifier, provider);
// 4. Anywhere in your code, inject the resolved dependency value:
const value = await manager.inject(identifier);
```
2. Register dependencies by giving the manager an identifier and a provider:
## Identifier
```typescript
manager.register('dependency-identifier', () => 'value');
```
An identifier is a uniq value used by the dependency manager to store and retrieve the dependency.
3. Get the dependency by giving the manager its identifier (always asynchrone):
```typescript
const value = await manager.resolve('dependency-identifier');
```
The `DependencyIdentifier` type allow the return value of the `inject` method to be fully typed,
## Providers
Providers are functions that are called when resolving the dependency for the first time.
They receive the dependency manager as parameter, and should return the dependency value, or a promise that resolves to it.
A dependency is rarely just a simple value like in the basic usage code and often rely on other dependencies.
Example of the registration of a dependency which provider uses another dependency:
That's why the `register` method takes a function (that we call the dependency `provider`) who's role is to build the dependency.
That function receive a uniq parameter: The dependency manager.
That way it is possible to inject dependencies inside it:
```typescript
async function provider(manager: DependencyManager) {
const value = await manager.resolve('dependencyIdentifier');
return `The value is: ${valueA}`;
function providerA() {
return 'foo'
}
manager.register('other-identifier', provider);
```
const identifierA = Symbol() as DependencyIdentifier<ReturnType<typeof providerA>>;
## Identifiers and typing
function providerB(manager: DependencyManager) {
const a = manager.inject(identifierA);
return `The value is: ${a}`;
}
const identifierB = Symbol() as DependencyIdentifier<ReturnType<typeof providerB>>;
Any valid `Map` key can be used as identifier, but using a class allow the return value of the `resolve` method to be automatically typed:
manager.register(identifierA, providerA);
manager.register(identifierB, providerB);
```typescript
class MyDependency {}
// ...
const a = await manager.resolve(MyDependecy); //< `a` is of type `MyDependency`
const b = await manager.resolve('dependency-identifier'); //< `b` is of type `unknown`
const c = await manager.resolve<number>('dependency-identifier'); //< `c` is of type `number`
const b = manager.inject(identifierB); // "The value is: foo"
```
## Errors
The `register` method throw an error if:
- the name is already used.
- the dependency is already registred.
The `resolve` method throw an error if:
- the name doesn't exist (if no module has been registred with the given name).
The `inject` method throw an error if:
- the dependency is not registred.
- a circular dependency is detected