Skip to content

Commit

Permalink
Renaming
Browse files Browse the repository at this point in the history
  • Loading branch information
timonson committed Jun 30, 2023
1 parent 7c26e81 commit 019c6c4
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 267 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,18 @@ const handleDate = routeAllAndEverything(date);
const handleVerify = routePrivate(verify);
const handleSubdomain = getSubdomain(sub);

const mainHandler = compose(
const mainMiddleware = compose(
handleSubdomain,
handleWelcome,
handleVerify,
handleDate,
) as any; // TS WTF!
const catchHandler = routeAllAndEverything(fix);
const finallyHandler = routeAllAndEverything(log, setHeader);
const catchMiddleware = routeAllAndEverything(fix);
const finallyMiddleware = routeAllAndEverything(log, setHeader);

const handler = createHandler(Ctx)(mainHandler)(catchHandler)(finallyHandler);
const handler = createHandler(Ctx)(mainMiddleware)(catchMiddleware)(
finallyMiddleware,
);

await listen(handler)({ port: 8080 });
```
33 changes: 33 additions & 0 deletions context.ts
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);
}
}
10 changes: 6 additions & 4 deletions example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ const handleDate = routeAllAndEverything(date);
const handleVerify = routePrivate(verify);
const handleSubdomain = getSubdomain(sub);

const mainHandler = compose(
const mainMiddleware = compose(
handleSubdomain,
handleWelcome,
handleVerify,
handleDate,
) as any; // TS WTF!
const catchHandler = routeAllAndEverything(fix);
const finallyHandler = routeAllAndEverything(log, setHeader);
const catchMiddleware = routeAllAndEverything(fix);
const finallyMiddleware = routeAllAndEverything(log, setHeader);

const handler = createHandler(Ctx)(mainHandler)(catchHandler)(finallyHandler);
const handler = createHandler(Ctx)(mainMiddleware)(catchMiddleware)(
finallyMiddleware,
);

await listen(handler)({ port: 8080 });
54 changes: 54 additions & 0 deletions handler.ts
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 };
49 changes: 49 additions & 0 deletions handler_test.ts
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",
);
});
11 changes: 4 additions & 7 deletions mod.ts
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";
54 changes: 54 additions & 0 deletions route.ts
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");
64 changes: 64 additions & 0 deletions route_test.ts
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,
);
});
Loading

0 comments on commit 019c6c4

Please sign in to comment.