-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
303 additions
and
267 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
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,33 @@ | ||
import { type ConnInfo } from "./deps.ts"; | ||
|
||
/** Any object can be assigned to the property `state` of the `Context` object. */ | ||
type State = Record<string | number | symbol, unknown>; | ||
// deno-lint-ignore no-explicit-any | ||
type DefaultState = Record<string, any>; | ||
|
||
/** | ||
* An instance of the extendable `Context` is passed as only argument to your | ||
* `Middleware`s. You can optionally extend the default `Context` object or pass | ||
* a `State` type. | ||
* ```ts | ||
* export class Ctx extends Context<{ start: number }> { | ||
* pathname = this.url.pathname; | ||
* } | ||
* ``` | ||
*/ | ||
export class Context<S extends State = DefaultState> { | ||
connInfo: ConnInfo; | ||
error: Error | null = null; | ||
result: URLPatternResult = {} as URLPatternResult; | ||
request: Request; | ||
response: Response = new Response("Not Found", { status: 404 }); | ||
state: S; | ||
url: URL; | ||
|
||
constructor(request: Request, connInfo: ConnInfo, state?: S) { | ||
this.connInfo = connInfo; | ||
this.request = request; | ||
this.state = state || {} as S; | ||
this.url = new URL(request.url); | ||
} | ||
} |
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,54 @@ | ||
import { type ConnInfo, type Handler } from "./deps.ts"; | ||
import { compose } from "./composition.ts"; | ||
import { Middleware } from "./route.ts"; | ||
import { Context } from "./context.ts"; | ||
|
||
function setXResponseTimeHeader<C extends Context>(ctx: C, startTime: number) { | ||
const ms = Date.now() - startTime; | ||
ctx.response.headers.set("X-Response-Time", `${ms}ms`); | ||
} | ||
|
||
function assertError(caught: unknown): Error { | ||
return caught instanceof Error ? caught : new Error("[non-error thrown]"); | ||
} | ||
|
||
/** | ||
* A curried function which takes a `Context` class, `tryMiddlewares`, | ||
* `catchMiddlewares` and `finallyMiddlewares` and returns in the end a `Handler` | ||
* function which can be passed to `listen`. It also handles the HTTP method | ||
* `HEAD` appropriately and sets the `X-Response-Time` header. You can pass | ||
* an initial `state` object or disable the `X-Response-Time` header optionally. | ||
* ```ts | ||
* createHandler(Ctx)(tryHandler)(catchHandler)(finallyHandler) | ||
* ``` | ||
*/ | ||
export function createHandler<C extends Context, S>( | ||
Context: new (request: Request, connInfo: ConnInfo, state?: S) => C, | ||
{ state, enableXResponseTimeHeader = true }: { | ||
state?: S; | ||
enableXResponseTimeHeader?: boolean; | ||
} = {}, | ||
) { | ||
let startTime = NaN; | ||
return (...tryMiddlewares: Middleware<C>[]) => | ||
(...catchMiddlewares: Middleware<C>[]) => | ||
(...finallyMiddlewares: Middleware<C>[]): Handler => | ||
async (request: Request, connInfo: ConnInfo): Promise<Response> => { | ||
const ctx = new Context(request, connInfo, state); | ||
try { | ||
if (enableXResponseTimeHeader) startTime = Date.now(); | ||
await (compose(...tryMiddlewares)(ctx)); | ||
} catch (caught) { | ||
ctx.error = assertError(caught); | ||
await (compose(...catchMiddlewares)(ctx)); | ||
} finally { | ||
await (compose(...finallyMiddlewares)(ctx)); | ||
if (enableXResponseTimeHeader) setXResponseTimeHeader(ctx, startTime); | ||
} | ||
return request.method === "HEAD" | ||
? new Response(null, ctx.response) | ||
: ctx.response; | ||
}; | ||
} | ||
|
||
export type { Handler }; |
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,49 @@ | ||
import { Context, createHandler } from "./mod.ts"; | ||
import { | ||
connInfo, | ||
mainMiddleware, | ||
subtract5DelayedMiddleware, | ||
} from "./test_util.ts"; | ||
import { assertEquals } from "./test_deps.ts"; | ||
|
||
type State = { result: number }; | ||
|
||
const request = new Request("https:example.com/books"); | ||
|
||
class Ctx extends Context<State> {} | ||
|
||
function catchMiddleware(ctx: Ctx) { | ||
ctx.state.result = 1; | ||
return ctx; | ||
} | ||
|
||
function finallyMiddleware(ctx: Ctx) { | ||
ctx.response = new Response(ctx.state.result.toString()); | ||
return ctx; | ||
} | ||
|
||
function throwMiddleware(_ctx: Ctx): never { | ||
throw new Error("uups"); | ||
} | ||
|
||
Deno.test("createHandler", async function () { | ||
assertEquals( | ||
await (await createHandler(Ctx, { state: { result: 10 } })(mainMiddleware)( | ||
subtract5DelayedMiddleware, | ||
)( | ||
finallyMiddleware, | ||
)(request, connInfo)).text(), | ||
"28", | ||
); | ||
assertEquals( | ||
await (await createHandler(Ctx, { state: { result: 10 } })( | ||
mainMiddleware, | ||
throwMiddleware, | ||
)( | ||
catchMiddleware, | ||
)( | ||
finallyMiddleware, | ||
)(request, connInfo)).text(), | ||
"1", | ||
); | ||
}); |
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 |
---|---|---|
@@ -1,8 +1,5 @@ | ||
export { | ||
Context, | ||
createHandler, | ||
createRoute, | ||
listen, | ||
type Method, | ||
} from "./server.ts"; | ||
export { listen } from "./server.ts"; | ||
export { Context } from "./context.ts"; | ||
export * from "./route.ts"; | ||
export { createHandler, type Handler } from "./handler.ts"; | ||
export { compose, composeSync } from "./composition.ts"; |
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,54 @@ | ||
import { compose } from "./composition.ts"; | ||
import { type Context } from "./context.ts"; | ||
|
||
export type Middleware<C extends Context> = (ctx: C) => C | Promise<C>; | ||
export type Method = | ||
| "ALL" | ||
| "CONNECT" | ||
| "DELETE" | ||
| "GET" | ||
| "HEAD" | ||
| "OPTIONS" | ||
| "PATCH" | ||
| "POST" | ||
| "PUT" | ||
| "TRACE"; | ||
|
||
/** | ||
* A curried function which takes HTTP `Method`s, a `URLPatternInput` and | ||
* `Middleware`s and returns in the end a composed route function. | ||
* ```ts | ||
* createRoute("GET")({ pathname: "*" })(middleware) | ||
* ``` | ||
*/ | ||
export function createRoute(...methods: Method[]) { | ||
return (urlPatternInput: URLPatternInput) => { | ||
const urlPattern = new URLPattern(urlPatternInput); | ||
return <C extends Context>(...middlewares: Middleware<C>[]) => | ||
async (ctx: C): Promise<C> => { | ||
if ( | ||
methods.includes("ALL") || | ||
methods.includes(ctx.request.method as Method) || | ||
(ctx.request.method === "HEAD" && methods.includes("GET")) | ||
) { | ||
const urlPatternResult = urlPattern.exec(ctx.url); | ||
if (urlPatternResult) { | ||
ctx.result = urlPatternResult; | ||
return await (compose<C | Promise<C>>(...middlewares))(ctx); | ||
} | ||
} | ||
return ctx; | ||
}; | ||
}; | ||
} | ||
|
||
export const createAllRoute = createRoute("ALL"); | ||
export const createConnectRoute = createRoute("CONNECT"); | ||
export const createDeleteRoute = createRoute("DELETE"); | ||
export const createGetRoute = createRoute("GET"); | ||
export const createHeadRoute = createRoute("HEAD"); | ||
export const createOptionsRoute = createRoute("OPTIONS"); | ||
export const createPatchRoute = createRoute("PATCH"); | ||
export const createPostRoute = createRoute("POST"); | ||
export const createPutRoute = createRoute("PUT"); | ||
export const createTraceRoute = createRoute("TRACE"); |
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,64 @@ | ||
import { Context, createRoute } from "./mod.ts"; | ||
import { | ||
add10Middleware, | ||
connInfo, | ||
divide5DelayedMiddleware, | ||
mainMiddleware, | ||
} from "./test_util.ts"; | ||
import { assertEquals } from "./test_deps.ts"; | ||
|
||
type State = { result: number }; | ||
|
||
const request = new Request("https:example.com/books"); | ||
|
||
class Ctx extends Context<State> {} | ||
|
||
Deno.test("createRoute", async function () { | ||
assertEquals( | ||
(await createRoute("ALL")({ pathname: "/books" })(add10Middleware)( | ||
new Ctx(request, connInfo, { result: 10 }), | ||
)).state.result, | ||
20, | ||
); | ||
assertEquals( | ||
(await createRoute("GET")({ pathname: "/books" })( | ||
add10Middleware, | ||
divide5DelayedMiddleware, | ||
)( | ||
new Ctx(request, connInfo, { result: 10 }), | ||
)).state.result, | ||
12, | ||
); | ||
assertEquals( | ||
(await createRoute("POST", "GET")({ pathname: "/books" })(mainMiddleware)( | ||
new Ctx( | ||
request, | ||
connInfo, | ||
{ result: 10 }, | ||
), | ||
)).state.result, | ||
28, | ||
); | ||
assertEquals( | ||
(await createRoute("POST", "DELETE")({ pathname: "/books" })( | ||
mainMiddleware, | ||
)( | ||
new Ctx( | ||
request, | ||
connInfo, | ||
{ result: 10 }, | ||
), | ||
)).state.result, | ||
10, | ||
); | ||
assertEquals( | ||
(await createRoute("GET")({ pathname: "/ups" })(mainMiddleware)( | ||
new Ctx( | ||
request, | ||
connInfo, | ||
{ result: 10 }, | ||
), | ||
)).state.result, | ||
10, | ||
); | ||
}); |
Oops, something went wrong.