-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: pipe universal middlewares (#19)
* feat: compose universal middlewares * chore: simplify types * chore: export compose * refactor: rename compose to pipe * refactor: rename compose to pipe * refactor: rename compose to pipe
- Loading branch information
Showing
7 changed files
with
241 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -145,4 +145,4 @@ | |
"@universal-middleware/hono": "^0", | ||
"@universal-middleware/webroute": "^0" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import type { Awaitable, UniversalHandler, UniversalMiddleware } from "./types"; | ||
|
||
type Out<T> = T extends UniversalMiddleware<any, infer C> ? C : never; | ||
type In<T> = | ||
T extends UniversalHandler<infer C> | ||
? C | ||
: T extends UniversalMiddleware<infer C, any> | ||
? C | ||
: never; | ||
type First<T extends any[]> = T extends [infer X, ...any[]] ? X : never; | ||
type Last<T extends any[]> = T extends [...any[], infer X] ? X : never; | ||
|
||
type ComposeReturnType<T extends UniversalMiddleware<any, any>[]> = | ||
Last<T> extends UniversalHandler<any> | ||
? UniversalHandler<In<First<T>>> | ||
: UniversalMiddleware<In<First<T>>, In<Last<T>>>; | ||
|
||
type Pipe<F extends UniversalMiddleware<any, any>[]> = F extends [] | ||
? F | ||
: F extends [UniversalMiddleware<any, any>] | ||
? F | ||
: F extends [ | ||
UniversalMiddleware<infer A, infer B>, | ||
UniversalMiddleware<any, infer D>, | ||
] | ||
? [UniversalMiddleware<A, B>, UniversalMiddleware<B, D>] | ||
: F extends [ | ||
...infer X extends UniversalMiddleware<any, any>[], | ||
infer Y extends UniversalMiddleware<any, any>, | ||
UniversalMiddleware<any, infer D1>, | ||
] | ||
? [...Pipe<[...X, Y]>, UniversalMiddleware<Out<Y>, D1>] | ||
: never; | ||
|
||
export function pipe<F extends UniversalMiddleware<any, any>[]>( | ||
...a: Pipe<F> | ||
): ComposeReturnType<F> { | ||
const middlewares = a as UniversalMiddleware[]; | ||
const handler = a.pop() as UniversalHandler; | ||
|
||
return async (request, context, runtime) => { | ||
const pending: ((response: Response) => Awaitable<Response>)[] = []; | ||
|
||
for (const m of middlewares) { | ||
const response = await m(request, context, runtime); | ||
|
||
if (typeof response === "function") { | ||
pending.push(response); | ||
} else if (response !== null && typeof response === "object") { | ||
if (response instanceof Response) { | ||
return response; | ||
} | ||
// Update context | ||
context = response as any; | ||
} | ||
} | ||
|
||
let response = await handler(request, context, runtime); | ||
|
||
for (const m of pending) { | ||
const r = await m(response); | ||
if (r) { | ||
response = r; | ||
} | ||
} | ||
|
||
return response; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { expectTypeOf, test } from "vitest"; | ||
import type { | ||
UniversalHandler, | ||
UniversalMiddleware, | ||
} from "@universal-middleware/core"; | ||
import { pipe } from "../src/pipe"; | ||
|
||
type M1 = UniversalMiddleware<{ a: 1 }, { a: 1; b: 2 }>; | ||
type M2 = UniversalMiddleware<{ a: 1; b: 2 }, { a: 1; b: 2; c: 3 }>; | ||
type M3 = UniversalMiddleware<{ a: 1; b: 2; c: 3 }, { a: 1; b: 2; c: 3 }>; | ||
type H1 = UniversalHandler<{ a: 1; b: 2; c: 3 }>; | ||
|
||
test("pipe", () => { | ||
const m1: M1 = {} as any; | ||
const m2: M2 = {} as any; | ||
const m3: M3 = {} as any; | ||
const h1: H1 = {} as any; | ||
|
||
expectTypeOf(pipe(m3, h1)).toEqualTypeOf< | ||
UniversalHandler<{ a: 1; b: 2; c: 3 }> | ||
>(h1); | ||
expectTypeOf( | ||
pipe( | ||
m1, | ||
// @ts-expect-error | ||
h1, | ||
), | ||
).toEqualTypeOf<UniversalHandler<{ a: 1 }>>(); | ||
expectTypeOf(pipe(m1, m2, m3, h1)).toEqualTypeOf< | ||
UniversalHandler<{ a: 1 }> | ||
>(); | ||
expectTypeOf( | ||
pipe( | ||
m1, | ||
m1, | ||
// @ts-expect-error | ||
h1, | ||
), | ||
).toEqualTypeOf<UniversalHandler<{ a: 1 }>>(); | ||
expectTypeOf( | ||
pipe( | ||
m1, | ||
// @ts-expect-error | ||
m3, | ||
h1, | ||
), | ||
).toEqualTypeOf<UniversalHandler<{ a: 1 }>>(); | ||
|
||
expectTypeOf(pipe(m1, m2, m3)).toEqualTypeOf< | ||
UniversalMiddleware<{ a: 1 }, { a: 1; b: 2; c: 3 }> | ||
>(); | ||
|
||
expectTypeOf( | ||
pipe((_: Request) => new Response(null)), | ||
).toEqualTypeOf<UniversalHandler>(); | ||
expectTypeOf( | ||
pipe( | ||
() => { | ||
return { | ||
a: "1", | ||
}; | ||
}, | ||
(a: Request, c: { a: string }) => new Response(c.a), | ||
), | ||
).toEqualTypeOf<UniversalHandler>(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { describe, expect, test } from "vitest"; | ||
import { pipe } from "../src/pipe"; | ||
import type { RuntimeAdapter, UniversalMiddleware } from "../src/index"; | ||
|
||
describe("pipe", () => { | ||
const request = new Request("http://localhost"); | ||
const context: Universal.Context = {}; | ||
const runtime: RuntimeAdapter = { | ||
runtime: "other", | ||
adapter: "other", | ||
}; | ||
|
||
test("handler", async () => { | ||
const handler = pipe(() => new Response("OK")); | ||
const response = handler(request, context, runtime); | ||
await expect(response).resolves.toBeInstanceOf(Response); | ||
}); | ||
|
||
test("context middleware |> handler", async () => { | ||
const handler = pipe( | ||
() => ({ a: 1 }), | ||
(_: Request, ctx: { a: number }) => new Response(String(ctx.a)), | ||
); | ||
const response = handler(request, context, runtime); | ||
await expect(response).resolves.toBeInstanceOf(Response); | ||
|
||
const body = await (await response).text(); | ||
expect(body).toBe("1"); | ||
}); | ||
|
||
test("context middleware |> empty middlware |> handler", async () => { | ||
const handler = pipe( | ||
() => ({ a: 1 }), | ||
(async () => {}) as UniversalMiddleware<{ a: number }, { a: number }>, | ||
(_: Request, ctx: { a: number }) => new Response(String(ctx.a)), | ||
); | ||
const response = handler(request, context, runtime); | ||
await expect(response).resolves.toBeInstanceOf(Response); | ||
|
||
const body = await (await response).text(); | ||
expect(body).toBe("1"); | ||
}); | ||
|
||
test("context middleware |> context middleware |> handler", async () => { | ||
const handler = pipe( | ||
() => ({ a: 1 }), | ||
async (_: Request, ctx: { a: number }) => { | ||
return { | ||
...ctx, | ||
b: 2, | ||
}; | ||
}, | ||
(_: Request, ctx: { a: number; b: number }) => | ||
new Response(String(ctx.a + ctx.b)), | ||
); | ||
const response = handler(request, context, runtime); | ||
await expect(response).resolves.toBeInstanceOf(Response); | ||
|
||
const body = await (await response).text(); | ||
expect(body).toBe("3"); | ||
}); | ||
|
||
test("context middleware |> response |> handler", async () => { | ||
const handler = pipe( | ||
() => ({ a: 1 }), | ||
async (_: Request) => { | ||
return new Response("STOPPED"); | ||
}, | ||
(_: Request) => new Response(null), | ||
); | ||
const response = handler(request, context, runtime); | ||
await expect(response).resolves.toBeInstanceOf(Response); | ||
|
||
const body = await (await response).text(); | ||
expect(body).toBe("STOPPED"); | ||
}); | ||
|
||
test("context middleware |> response handler |> handler", async () => { | ||
const handler = pipe( | ||
() => ({ a: 1 }), | ||
(_: Request) => { | ||
return async (response: Response) => { | ||
const body = await (await response).text(); | ||
return new Response(body + " World!"); | ||
}; | ||
}, | ||
(_: Request) => new Response("Hello"), | ||
); | ||
const response = handler(request, context, runtime); | ||
await expect(response).resolves.toBeInstanceOf(Response); | ||
|
||
const body = await (await response).text(); | ||
expect(body).toBe("Hello World!"); | ||
}); | ||
}); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.