diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..503bd6f --- /dev/null +++ b/deno.json @@ -0,0 +1,7 @@ +{ + "fmt": { + "semiColons": true, + "singleQuote": true, + "useTabs": true + } +} \ No newline at end of file diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..83fe0e7 --- /dev/null +++ b/deno.lock @@ -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" + } +} diff --git a/dependency-manager.test.ts b/dependency-manager.test.ts index d628584..430b2be 100644 --- a/dependency-manager.test.ts +++ b/dependency-manager.test.ts @@ -1,77 +1,91 @@ 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 { DependencyManager, Provider } from './dependency-manager.ts'; + +Deno.test('should register and resolve a simple dependency', () => { + const manager = new DependencyManager(); + + const dependency = () => 'test'; + manager.register(dependency); + + const resolved = manager.resolve(dependency); + assertEquals(resolved, 'test'); }); -// 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('should register and resolve a module with multiple dependencies', () => { + const manager = new DependencyManager(); + + const module = { + a: () => 'test1', + b: () => 42, + }; + + manager.register(module); + + const a = manager.resolve(module.a); + const b = manager.resolve(module.b); + + assertEquals(a, 'test1'); + assertEquals(b, 42); }); -// 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('should throw error when registering the same dependency twice', () => { + const manager = new DependencyManager(); + + const dependency = () => 'test'; + manager.register(dependency); + + assertThrows( + () => manager.register(dependency), + Error, + 'This dependency or module has already been registered', + ); }); -// 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('should throw error when resolving a non-registered dependency', () => { + const manager = new DependencyManager(); - manager.register("A", providerA); - manager.register("B", providerB); + const dependency = () => 'test'; - await assertRejects( - () => manager.resolve("A"), - Error, - "Circular dependency detected for module A.", - ); + assertThrows( + () => manager.resolve(dependency), + Error, + 'This key has not (yet ?) been used to register something', + ); }); -// 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('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'); }); -// 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.", - ); +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 b0212c4..e777050 100644 --- a/dependency-manager.ts +++ b/dependency-manager.ts @@ -1,75 +1,58 @@ -type Provider = (manager: DependencyManager) => Promise | T; +export type Provider = (manager: DependencyManager) => T; -// deno-lint-ignore no-explicit-any -type HasConstructor = T extends new (...args: any[]) => unknown ? T : never; -// deno-lint-ignore no-explicit-any -type ConstructorInstance = T extends new (...args: any[]) => infer I ? I - : never; - -// deno-lint-ignore no-explicit-any -type ModuleIdentifier = any; - -type Modules = Map; - // deno-lint-ignore no-explicit-any - instance?: any; -}>; +export type ProviderGroup = { + [key: string]: Provider | ProviderGroup; +}; export class DependencyManager { - private modules: Modules = new Map(); - private resolving: Set = new Set(); + private declarations = new Map(); + private instances = new Map(); + private resolving = new Set(); - /** - * 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(identifier: ModuleIdentifier, provider: Provider): void { - if (this.modules.has(identifier)) { - throw new Error(`Module ${identifier} is already registered.`); - } - this.modules.set(identifier, { provider }); - } + 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'); + } - /** - * 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( - identifier: HasConstructor, - ): Promise>; - async resolve(identifier: ModuleIdentifier): Promise; - async resolve(identifier: ModuleIdentifier) { - const module = this.modules.get(identifier); - if (!module) { - throw new Error(`Module ${identifier} is not registered.`); - } + if (typeof dependencyOrModule === 'function') { + this.declarations.set(dependencyOrModule, dependencyOrModule); + return; + } - if (module.instance) { - return module.instance; - } + const module = dependencyOrModule; + for (const key in module) { + this.register(module[key]); + } + } - if (this.resolving.has(identifier)) { - throw new Error(`Circular dependency detected for module ${identifier}.`); - } + resolve(dependency: Provider): T { + if (!this.declarations.has(dependency)) { + throw new Error('This dependency has not been registred'); + } - this.resolving.add(identifier); + if (this.instances.has(dependency)) { + return this.instances.get(dependency) as T; + } - try { - module.instance = await module.provider(this); - return module.instance; - } finally { - this.resolving.delete(identifier); - } - } + if (this.resolving.has(dependency)) { + 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; + const instance = declaration(this); + this.instances.set(dependency, instance); + + return instance; + } } diff --git a/mod.ts b/mod.ts index 662adcf..9e243a9 100644 --- a/mod.ts +++ b/mod.ts @@ -1 +1 @@ -export * from "./dependency-manager.ts"; +export * from './dependency-manager.ts'; diff --git a/readme.md b/readme.md index be01545..1b35fdc 100644 --- a/readme.md +++ b/readme.md @@ -5,66 +5,84 @@ 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 +- Allows "module" registration (grouped dependencies) -## Usage - -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. Register dependencies by giving the manager an identifier and a provider: +// 2. Create a dependency: +const dependency = () => 'value'; -```typescript -manager.register('dependency-identifier', () => 'value'); -``` +// 3. Register the dependency: +manager.register(dependency); -3. Get the dependency by giving the manager its identifier (always asynchrone): - -```typescript -const value = await manager.resolve('dependency-identifier'); +// 4. Anywhere in your code, get the resolved dependency value: +const value = await manager.resolve(dependency); ``` ## 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. +To register a dependency, we pass a function to the dependency manager. +This function is called a provider. + +Providers are called when resolving the dependency for the first time. +They receive the dependency manager as parameter, and should return the dependency value. Example of the registration of a dependency which provider uses another dependency: ```typescript -async function provider(manager: DependencyManager) { - const value = await manager.resolve('dependencyIdentifier'); +function dependency(manager: DependencyManager) { + const value = manager.resolve(otherDependency); return `The value is: ${valueA}`; } -manager.register('other-identifier', provider); +manager.register(dependency); +manager.register(otherDependency); ``` -## Identifiers and typing +## Typing -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: +The reason we pass the provider to resolve a dependency is that it allow the `resolve` method to correctly type the returned value: ```typescript -class MyDependency {} +const A = () => 'foo'; +const B = () => 42; +const C = () => ({}); -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('dependency-identifier'); //< `c` is of type `number` +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` ``` -Class with private constructor cannot be infered as class and thus need to be explicitly typed when resolved. +## 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); +``` ## 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 dependency is not registred. - a circular dependency is detected \ No newline at end of file