From 070b9e185ce9ce2e95844bafe6bdb51b3f245306 Mon Sep 17 00:00:00 2001 From: Eugene Daragan Date: Sat, 13 Apr 2024 00:24:43 +0200 Subject: [PATCH] resolves #5: add beforInject and beforeConstruct hooks --- src/container.ts | 30 +++-- src/scopes.ts | 20 ++-- src/types.ts | 11 +- test/dioma.test.ts | 268 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 307 insertions(+), 22 deletions(-) diff --git a/src/container.ts b/src/container.ts index 6c19837..5b16447 100644 --- a/src/container.ts +++ b/src/container.ts @@ -44,19 +44,31 @@ export class Container { return new Container(this, name); }; - public $getInstance(cls: any, args: any[] = []) { + public $getInstance( + descriptor: TokenClassDescriptor, + args: any[] = [], + cache = true + ) { let instance = null; let container: Container | null = this; + const cls = descriptor.class; + while (!instance && container) { instance = container.instances.get(cls); container = container.parentContainer; } if (!instance) { + if (descriptor.beforeConstruct) { + descriptor.beforeConstruct(this, descriptor, args); + } + instance = new cls(...args); - this.instances.set(cls, instance); + if (cache) { + this.instances.set(cls, instance); + } } return instance; @@ -94,7 +106,7 @@ export class Container { let container: Container = this; - const descriptor = this.getTokenDescriptor(clsOrToken); + let descriptor = this.getTokenDescriptor(clsOrToken); this.resolutionSet.add(clsOrToken); @@ -104,16 +116,17 @@ export class Container { } cls = clsOrToken; - scope = cls.scope || Scopes.Transient(); - container = this; + descriptor = { class: cls, container: this }; } else { + if (descriptor.beforeInject) { + descriptor.beforeInject(container, descriptor, args); + } + if ("class" in descriptor) { cls = descriptor.class as ScopedClass; - scope = descriptor.scope || cls.scope || Scopes.Transient(); - container = descriptor.container; } else if ("value" in descriptor) { return descriptor.value; @@ -125,10 +138,9 @@ export class Container { } } - return scope(cls, args, container, this.resolutionContainer); + return scope(descriptor, args, container, this.resolutionContainer); } finally { this.resolutionSet.delete(clsOrToken); - this.resolutionContainer = resolutionContainer; if (!resolutionContainer) { diff --git a/src/scopes.ts b/src/scopes.ts index 5388eb9..22e347c 100644 --- a/src/scopes.ts +++ b/src/scopes.ts @@ -4,34 +4,34 @@ import type { ScopeHandler } from "./types"; export class Scopes { public static Singleton(): ScopeHandler { - return function SingletonScope(cls, args) { + return function SingletonScope(descriptor, args) { if (args.length > 0) { - throw new ArgumentsError(SingletonScope.name, cls.name); + throw new ArgumentsError(SingletonScope.name, descriptor.class.name); } - return globalContainer.$getInstance(cls); + return globalContainer.$getInstance(descriptor); }; } public static Transient(): ScopeHandler { - return function TransientScope(cls, args) { - return new cls(...args); + return function TransientScope(descriptor, args, container) { + return container.$getInstance(descriptor, args, false); }; } public static Container(): ScopeHandler { - return function ContainerScope(cls, args, container) { + return function ContainerScope(descriptor, args, container) { if (args.length > 0) { - throw new ArgumentsError(ContainerScope.name, cls.name); + throw new ArgumentsError(ContainerScope.name, descriptor.class.name); } - return container.$getInstance(cls); + return container.$getInstance(descriptor); }; } public static Resolution(): ScopeHandler { - return function ResolutionScope(cls, args, _, resolutionContainer) { - return resolutionContainer.$getInstance(cls, args); + return function ResolutionScope(descriptor, args, _, resolutionContainer) { + return resolutionContainer.$getInstance(descriptor, args); }; } diff --git a/src/types.ts b/src/types.ts index 61b79ce..5c71f5a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,17 +27,22 @@ type OptionalArgs> = R extends readonly [...infer Rest, infer type TokenType = T extends Token ? U : R; -export type TokenValueDescriptor> = { +type BaseDescriptor = { + beforeInject?: (container: Container, descriptor: AnyDescriptor, args: any[]) => any; + beforeConstruct?: (container: Container, descriptor: AnyDescriptor, args: any[]) => any; +}; + +export type TokenValueDescriptor> = BaseDescriptor & { token: T; value: TokenType; }; -export type TokenFactoryDescriptor> = { +export type TokenFactoryDescriptor> = BaseDescriptor & { token: T; factory: (container: Container, ...args: any[]) => TokenType; }; -export type TokenClassDescriptor> = { +export type TokenClassDescriptor> = BaseDescriptor & { token?: T; class: Newable>; scope?: ScopeHandler; diff --git a/test/dioma.test.ts b/test/dioma.test.ts index 724d464..e303afe 100644 --- a/test/dioma.test.ts +++ b/test/dioma.test.ts @@ -12,6 +12,8 @@ import { inject, injectAsync, } from "../src"; +import { AnyDescriptor } from "../src/types"; +import { c } from "vite/dist/node/types.d-aGj9QkWt"; describe("Dioma", () => { beforeEach(() => { @@ -1535,6 +1537,272 @@ describe("Dioma", () => { ); }); }); + + describe("beforeInject", () => { + it("should be able to use beforeInject for class injection with token", () => { + class TokenClass { + constructor(public value: string) {} + + static scope = Scopes.Transient(); + } + + const token = new Token(); + + let called = false; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + token, + class: TokenClass, + beforeInject: (container, descriptor, args) => { + called = true; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const instance = container.inject(token, "test"); + + expect(called).toBe(true); + expect(containerArg).toBe(container); + // @ts-expect-error + expect(descriptorArg.class).toBe(TokenClass); + expect(argsArg).toEqual(["test"]); + }); + + it("should be able to use beforeInject for class injection with class", () => { + class TokenClass { + constructor(public value: string) {} + + static scope = Scopes.Transient(); + } + + let called = false; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + class: TokenClass, + beforeInject: (container, descriptor, args) => { + called = true; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const instance = container.inject(TokenClass, "test"); + + expect(called).toBe(true); + expect(containerArg).toBe(container); + // @ts-expect-error + expect(descriptorArg.class).toBe(TokenClass); + expect(argsArg).toEqual(["test"]); + }); + + it("should be able to use beforeInject for value injection", () => { + const token = new Token(); + + let called = false; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + token, + value: "test", + beforeInject: (container, descriptor, args) => { + called = true; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const value = container.inject(token); + + expect(called).toBe(true); + expect(containerArg).toBe(container); + // @ts-expect-error + expect(descriptorArg.value).toBe("test"); + expect(argsArg).toEqual([]); + }); + + it("should be able to use beforeInject for factory injection", () => { + const token = new Token(); + + let called = false; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + token, + factory: (container, a: string) => a + "!", + beforeInject: (container, descriptor, args) => { + called = true; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const value = container.inject(token, "test"); + + expect(called).toBe(true); + expect(containerArg).toBe(container); + // @ts-expect-error + expect(descriptorArg.factory).toBeDefined(); + expect(argsArg).toEqual(["test"]); + }); + }); + + describe("beforeConstruct", () => { + it("should be able to use beforeConstruct for class injection (transient scope)", () => { + class TokenClass { + constructor(public value: string) {} + + static scope = Scopes.Transient(); + } + + let called = false; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + class: TokenClass, + beforeConstruct: (container, descriptor, args) => { + called = true; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const instance = container.inject(TokenClass, "test"); + + expect(called).toBe(true); + expect(containerArg).toBe(container); + // @ts-expect-error + expect(descriptorArg.class).toBe(TokenClass); + expect(argsArg).toEqual(["test"]); + + const instance2 = container.inject(TokenClass, "test2"); + expect(instance2).not.toBe(instance); + expect(argsArg).toEqual(["test2"]); + }); + + it("should be able to use beforeConstruct for class injection (singleton scope)", () => { + class TokenClass { + constructor(public value: string) {} + + static scope = Scopes.Singleton(); + } + + let called = 0; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + class: TokenClass, + beforeConstruct: (container, descriptor, args) => { + called += 1; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const instance = container.inject(TokenClass); + + expect(called).toBe(1); + expect(containerArg).toBe(globalContainer); + // @ts-expect-error + expect(descriptorArg.class).toBe(TokenClass); + expect(argsArg).toEqual([]); + + const instance2 = container.inject(TokenClass); + expect(instance2).toBe(instance); + expect(called).toBe(1); + }); + + it("should be able to use beforeConstruct for class injection (container scope)", () => { + class TokenClass { + constructor(public value: string) {} + + static scope = Scopes.Container(); + } + + let called = 0; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + class: TokenClass, + beforeConstruct: (container, descriptor, args) => { + called += 1; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const instance = container.inject(TokenClass); + + expect(called).toBe(1); + expect(containerArg).toBe(container); + // @ts-expect-error + expect(descriptorArg.class).toBe(TokenClass); + expect(argsArg).toEqual([]); + + const instance2 = container.inject(TokenClass); + expect(instance2).toBe(instance); + expect(called).toBe(1); + }); + + it("should be able to use beforeConstruct for class injection (resolution scope)", () => { + class TokenClass { + constructor(public value: string) {} + + static scope = Scopes.Resolution(); + } + + let called = 0; + let containerArg: Container | null = null; + let descriptorArg: AnyDescriptor | null = null; + let argsArg: any[] | null = null; + + container.register({ + class: TokenClass, + beforeConstruct: (container, descriptor, args) => { + called += 1; + containerArg = container; + descriptorArg = descriptor; + argsArg = args; + }, + }); + + const instance = container.inject(TokenClass, "test"); + + expect(called).toBe(1); + expect(containerArg).toBeInstanceOf(Container); // child resolution container is expected + expect(containerArg).not.toBe(container); + // @ts-expect-error + expect(descriptorArg.class).toBe(TokenClass); + expect(argsArg).toEqual(["test"]); + + const instance2 = container.inject(TokenClass, "test2"); + expect(instance2).not.toBe(instance); + expect(argsArg).toEqual(["test2"]); + }); + }); }); }); });