Skip to content

Commit

Permalink
feat: middleware (#3)
Browse files Browse the repository at this point in the history
* feat: middleware

* chore: add example jsdoc

* test: middleware

* refactor: reorder parameters

* refactor: rename function

* refactor: use overload signature for jsdoc

* refactor: rename parameter

* test: use mock.calls
  • Loading branch information
exuanbo authored Oct 27, 2024
1 parent 33f2d3e commit 9d68446
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 0 deletions.
81 changes: 81 additions & 0 deletions src/__tests__/middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {beforeEach, describe, expect, it, vi} from "vitest";

import {applyMiddleware, type Container, createContainer, inject, injectAll, type Middleware, type Token} from "..";

describe("Middleware", () => {
let container: Container;

beforeEach(() => {
container = createContainer();
});

it("should apply middleware", () => {
const log = vi.fn();

function getLogger(loggerName: string): Middleware {
return (composer) => {
composer
.use("resolve", (next) => <T>(token: Token<T>) => {
log(`[${loggerName}] pre resolve ${token.name}`);
const result = next(token);
log(`[${loggerName}] post resolve ${String(result)}`);
return result;
})
.use("resolveAll", (next) => <T>(token: Token<T>) => {
log(`[${loggerName}] pre resolveAll ${token.name}`);
const result = next(token);
log(`[${loggerName}] post resolveAll [${String(result)}]`);
return result;
});
};
}

applyMiddleware(
container,
[getLogger("A"), getLogger("B")],
);

class Decoration {
toString() {
return "Decoration {}";
}
}

class Wand {
decorations = injectAll(Decoration);

toString() {
return `Wand {decorations: [${String(this.decorations)}]}`;
}
}

class Wizard {
wand = inject(Wand);

toString() {
return `Wizard {wand: ${String(this.wand)}}`;
}
}

const wizard = container.resolve(Wizard);

expect(wizard).toBeInstanceOf(Wizard);
expect(wizard.wand).toBeInstanceOf(Wand);
expect(wizard.wand.decorations).toEqual([new Decoration()]);

expect(log.mock.calls).toEqual([
["[B] pre resolve Wizard"],
["[A] pre resolve Wizard"],
["[B] pre resolve Wand"],
["[A] pre resolve Wand"],
["[B] pre resolveAll Decoration"],
["[A] pre resolveAll Decoration"],
["[A] post resolveAll [Decoration {}]"],
["[B] post resolveAll [Decoration {}]"],
["[A] post resolve Wand {decorations: [Decoration {}]}"],
["[B] post resolve Wand {decorations: [Decoration {}]}"],
["[A] post resolve Wizard {wand: Wand {decorations: [Decoration {}]}}"],
["[B] post resolve Wizard {wand: Wand {decorations: [Decoration {}]}}"],
]);
});
});
1 change: 1 addition & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export interface Container {
* Creates a new container.
*/
export function createContainer(options?: ContainerOptions): Container;

export function createContainer({
autoRegister = false,
defaultScope = Scope.Inherited,
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type {ClassDecorator, ClassFieldDecorator, ClassFieldInitializer} from ".
export {AutoRegister, Inject, Injectable, InjectAll, Scoped} from "./decorators";
export {inject, injectAll, injectBy, Injector} from "./inject";
export type {InstanceRef} from "./instance";
export type {Middleware, MiddlewareComposer} from "./middleware";
export {applyMiddleware} from "./middleware";
export type {ClassProvider, FactoryProvider, Provider, ValueProvider} from "./provider";
export type {Registration, RegistrationMap, RegistrationOptions, Registry} from "./registry";
export {Build, Value} from "./registry";
Expand Down
76 changes: 76 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type {Container} from "./container";

/**
* Middleware function that can be used to extend the container.
*
* @example
* ```ts
* const logger: Middleware = (composer, _api) => {
* composer
* .use("resolve", (next) => (...args) => {
* console.log("resolve", args);
* return next(...args);
* })
* .use("resolveAll", (next) => (...args) => {
* console.log("resolveAll", args);
* return next(...args);
* });
* };
* ```
*/
export interface Middleware {
(composer: MiddlewareComposer, api: Readonly<Container>): void;
}

/**
* Composer API for middleware functions.
*/
export interface MiddlewareComposer {
/**
* Add a middleware function to the composer.
*
* @template Key - The key of the container method to wrap.
*/
use<Key extends keyof Container>(
key: Key,
wrap: Container[Key] extends Function
? (next: Container[Key]) => Container[Key]
: never
): MiddlewareComposer;
}

/**
* Apply middleware functions to a container.
*
* Middlewares are applied in array order, but execute in reverse order.
*
* @example
* ```ts
* const container = applyMiddleware(
* createContainer(),
* [A, B],
* );
* ```
*
* The execution order will be:
*
* 1. B before
* 2. A before
* 3. original function
* 4. A after
* 5. B after
*
* This allows outer middlewares to wrap and control the behavior of inner middlewares.
*/
export function applyMiddleware(container: Container, middlewares: Middleware[]): Container;

export function applyMiddleware(container: Container, middlewares: Middleware[]) {
const composer: MiddlewareComposer = {
use(key, wrap) {
container[key] = wrap(container[key]);
return composer;
},
};
middlewares.forEach((middleware) => middleware(composer, {...container}));
return container;
}

0 comments on commit 9d68446

Please sign in to comment.