Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

router class rearchitecture #207

Merged
merged 2 commits into from
Jul 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 60 additions & 8 deletions src/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type {
FindResult,
HandlerOptions,
HttpMethod,
Nextable,
RouteMatch,
RouteShortcutMethod,
ValueOrPromise,
} from "./types.js";

Expand All @@ -11,13 +14,51 @@ export type RequestHandler<Req extends Request, Ctx> = (
ctx: Ctx
) => ValueOrPromise<Response | void>;

export class EdgeRouter<
Req extends Request = Request,
Ctx = unknown
> extends Router<RequestHandler<Req, Ctx>> {
constructor() {
super();
export class EdgeRouter<Req extends Request = Request, Ctx = unknown> {
private router = new Router<RequestHandler<Req, Ctx>>();

private add(
method: HttpMethod | "",
route: RouteMatch | Nextable<RequestHandler<Req, Ctx>>,
...fns: Nextable<RequestHandler<Req, Ctx>>[]
) {
this.router.add(method, route, ...fns);
return this;
}

public all: RouteShortcutMethod<this, RequestHandler<Req, Ctx>> =
this.add.bind(this, "");
public get: RouteShortcutMethod<this, RequestHandler<Req, Ctx>> =
this.add.bind(this, "GET");
public head: RouteShortcutMethod<this, RequestHandler<Req, Ctx>> =
this.add.bind(this, "HEAD");
public post: RouteShortcutMethod<this, RequestHandler<Req, Ctx>> =
this.add.bind(this, "POST");
public put: RouteShortcutMethod<this, RequestHandler<Req, Ctx>> =
this.add.bind(this, "PUT");
public patch: RouteShortcutMethod<this, RequestHandler<Req, Ctx>> =
this.add.bind(this, "PATCH");
public delete: RouteShortcutMethod<this, RequestHandler<Req, Ctx>> =
this.add.bind(this, "DELETE");

public use(
base:
| RouteMatch
| Nextable<RequestHandler<Req, Ctx>>
| EdgeRouter<Req, Ctx>,
...fns: (Nextable<RequestHandler<Req, Ctx>> | EdgeRouter<Req, Ctx>)[]
) {
if (typeof base === "function" || base instanceof EdgeRouter) {
fns.unshift(base);
base = "/";
}
this.router.use(
base,
...fns.map((fn) => (fn instanceof EdgeRouter ? fn.router : fn))
);
return this;
}

private prepareRequest(
req: Req & { params?: Record<string, unknown> },
ctx: Ctx,
Expand All @@ -28,17 +69,28 @@ export class EdgeRouter<
...req.params, // original params will take precedence
};
}

public clone() {
const r = new EdgeRouter();
r.router = this.router.clone();
return r;
}

async run(req: Req, ctx: Ctx) {
const result = this.find(req.method as HttpMethod, getPathname(req));
const result = this.router.find(req.method as HttpMethod, getPathname(req));
if (!result.fns.length) return;
this.prepareRequest(req, ctx, result);
return Router.exec(result.fns, req, ctx);
}

handler(options: HandlerOptions<RequestHandler<Req, Ctx>> = {}) {
const onNoMatch = options.onNoMatch || onnomatch;
const onError = options.onError || onerror;
return async (req: Req, ctx: Ctx): Promise<any> => {
const result = this.find(req.method as HttpMethod, getPathname(req));
const result = this.router.find(
req.method as HttpMethod,
getPathname(req)
);
this.prepareRequest(req, ctx, result);
try {
if (result.fns.length === 0 || result.middleOnly) {
Expand Down
63 changes: 58 additions & 5 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type {
FindResult,
HandlerOptions,
HttpMethod,
Nextable,
RouteMatch,
RouteShortcutMethod,
ValueOrPromise,
} from "./types.js";

Expand All @@ -15,10 +18,51 @@ export type RequestHandler<
export class NodeRouter<
Req extends IncomingMessage = IncomingMessage,
Res extends ServerResponse = ServerResponse
> extends Router<RequestHandler<Req, Res>> {
constructor() {
super();
> {
private router = new Router<RequestHandler<Req, Res>>();

private add(
method: HttpMethod | "",
route: RouteMatch | Nextable<RequestHandler<Req, Res>>,
...fns: Nextable<RequestHandler<Req, Res>>[]
) {
this.router.add(method, route, ...fns);
return this;
}

public all: RouteShortcutMethod<this, RequestHandler<Req, Res>> =
this.add.bind(this, "");
public get: RouteShortcutMethod<this, RequestHandler<Req, Res>> =
this.add.bind(this, "GET");
public head: RouteShortcutMethod<this, RequestHandler<Req, Res>> =
this.add.bind(this, "HEAD");
public post: RouteShortcutMethod<this, RequestHandler<Req, Res>> =
this.add.bind(this, "POST");
public put: RouteShortcutMethod<this, RequestHandler<Req, Res>> =
this.add.bind(this, "PUT");
public patch: RouteShortcutMethod<this, RequestHandler<Req, Res>> =
this.add.bind(this, "PATCH");
public delete: RouteShortcutMethod<this, RequestHandler<Req, Res>> =
this.add.bind(this, "DELETE");

public use(
base:
| RouteMatch
| Nextable<RequestHandler<Req, Res>>
| NodeRouter<Req, Res>,
...fns: (Nextable<RequestHandler<Req, Res>> | NodeRouter<Req, Res>)[]
) {
if (typeof base === "function" || base instanceof NodeRouter) {
fns.unshift(base);
base = "/";
}
this.router.use(
base,
...fns.map((fn) => (fn instanceof NodeRouter ? fn.router : fn))
);
return this;
}

private prepareRequest(
req: Req & { params?: Record<string, unknown> },
res: Res,
Expand All @@ -29,20 +73,28 @@ export class NodeRouter<
...req.params, // original params will take precedence
};
}

public clone() {
const r = new NodeRouter();
r.router = this.router.clone();
return r;
}

async run(req: Req, res: Res) {
const result = this.find(
const result = this.router.find(
req.method as HttpMethod,
getPathname(req.url as string)
);
if (!result.fns.length) return;
this.prepareRequest(req, res, result);
return Router.exec(result.fns, req, res);
}

handler(options: HandlerOptions<RequestHandler<Req, Res>> = {}) {
const onNoMatch = options.onNoMatch || onnomatch;
const onError = options.onError || onerror;
return async (req: Req, res: Res) => {
const result = this.find(
const result = this.router.find(
req.method as HttpMethod,
getPathname(req.url as string)
);
Expand All @@ -68,6 +120,7 @@ function onnomatch(req: IncomingMessage, res: ServerResponse) {
: undefined
);
}

function onerror(err: unknown, req: IncomingMessage, res: ServerResponse) {
res.statusCode = 500;
console.error(err);
Expand Down
13 changes: 0 additions & 13 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
} from "./types.js";

export type Route<H> = {
prefix?: string;
method: HttpMethod | "";
fns: (H | Router<H extends FunctionLike ? H : never>)[];
isMiddle: boolean;
Expand All @@ -25,11 +24,6 @@ export type Route<H> = {
| { matchAll: true }
);

type RouteShortcutMethod<This, H extends FunctionLike> = (
route: RouteMatch | Nextable<H>,
...fns: Nextable<H>[]
) => This;

export class Router<H extends FunctionLike> {
constructor(
public base: string = "/",
Expand All @@ -52,13 +46,6 @@ export class Router<H extends FunctionLike> {
}
return this;
}
public all: RouteShortcutMethod<this, H> = this.add.bind(this, "");
public get: RouteShortcutMethod<this, H> = this.add.bind(this, "GET");
public head: RouteShortcutMethod<this, H> = this.add.bind(this, "HEAD");
public post: RouteShortcutMethod<this, H> = this.add.bind(this, "POST");
public put: RouteShortcutMethod<this, H> = this.add.bind(this, "PUT");
public patch: RouteShortcutMethod<this, H> = this.add.bind(this, "PATCH");
public delete: RouteShortcutMethod<this, H> = this.add.bind(this, "DELETE");

public use(
base: RouteMatch | Nextable<H> | Router<H>,
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export interface HandlerOptions<Handler extends FunctionLike> {
}

export type ValueOrPromise<T> = T | Promise<T>;

export type RouteShortcutMethod<This, H extends FunctionLike> = (
route: RouteMatch | Nextable<H>,
...fns: Nextable<H>[]
) => This;
95 changes: 88 additions & 7 deletions test/edge.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { test } from "tap";
import { spyOn } from "tinyspy";
import { createEdgeRouter, EdgeRouter, getPathname } from "../src/edge.js";
import { Router } from "../src/router.js";

type AnyHandler = (...args: any[]) => any;

const noop: AnyHandler = async () => {
/** noop */
};

const METHODS = ["GET", "HEAD", "PATCH", "DELETE", "POST", "PUT"];

test("internals", (t) => {
const ctx = new EdgeRouter();
t.ok(ctx instanceof EdgeRouter, "creates new `Router` instance");
t.ok(Array.isArray(ctx.routes), "~> has `routes` key (Array)");
t.type(ctx.add, "function", "~> has `add` method");
t.type(ctx.find, "function", "~> has `find` method");
t.ok(ctx instanceof EdgeRouter, "creates new `EdgeRouter` instance");
// @ts-expect-error: internal
t.ok(ctx.router instanceof Router, "~> has a `Router` instance");

t.type(ctx.all, "function", "~> has `all` method");
METHODS.forEach((str) => {
t.type(ctx[str.toLowerCase()], "function", `~> has \`${str}\` method`);
Expand All @@ -21,6 +28,78 @@ test("createEdgeRouter() returns an instance", async (t) => {
t.ok(createEdgeRouter() instanceof EdgeRouter);
});

test("add()", async (t) => {
const ctx = new EdgeRouter();
// @ts-expect-error: private property
const routerAddStub = spyOn(ctx.router, "add");
// @ts-expect-error: private property
const returned = ctx.add("GET", "/", noop);
t.same(routerAddStub.calls, [["GET", "/", noop]], "call router.add()");
t.equal(returned, ctx, "returned itself");
});

test("use()", async (t) => {
t.test("it defaults to / if base is not provided", async (t) => {
const ctx = new EdgeRouter();

// @ts-expect-error: private field
const useSpy = spyOn(ctx.router, "use");

ctx.use(noop);

t.same(useSpy.calls, [["/", noop]]);
});

t.test("it call this.router.use() with fn", async (t) => {
const ctx = new EdgeRouter();

// @ts-expect-error: private field
const useSpy = spyOn(ctx.router, "use");

ctx.use("/test", noop, noop);

t.same(useSpy.calls, [["/test", noop, noop]]);
});

t.test("it call this.router.use() with fn.router", async (t) => {
const ctx = new EdgeRouter();
const ctx2 = new EdgeRouter();

// @ts-expect-error: private field
const useSpy = spyOn(ctx.router, "use");

ctx.use("/test", ctx2, ctx2);

// @ts-expect-error: private field
t.same(useSpy.calls, [["/test", ctx2.router, ctx2.router]]);
});
});

test("clone()", (t) => {
const ctx = new EdgeRouter();
// @ts-expect-error: private property
ctx.router.routes = [noop, noop] as any[];
t.ok(ctx.clone() instanceof EdgeRouter, "is a NodeRouter instance");
t.not(ctx, ctx.clone(), "not the same identity");
// @ts-expect-error: private property
t.not(ctx.router, ctx.clone().router, "not the same router identity");
t.not(
// @ts-expect-error: private property
ctx.router.routes,
// @ts-expect-error: private property
ctx.clone().router.routes,
"routes are deep cloned (identity)"
);
t.same(
// @ts-expect-error: private property
ctx.router.routes,
// @ts-expect-error: private property
ctx.clone().router.routes,
"routes are deep cloned"
);
t.end();
});

test("run() - runs req and evt through fns and return last value", async (t) => {
t.plan(7);
const ctx = createEdgeRouter();
Expand Down Expand Up @@ -328,7 +407,7 @@ test("prepareRequest() - attach params", async (t) => {

const ctx2 = createEdgeRouter().get("/hello/:name");
// @ts-expect-error: internal
ctx2.prepareRequest(req, {}, ctx2.find("GET", "/hello/world"));
ctx2.prepareRequest(req, {}, ctx2.router.find("GET", "/hello/world"));
t.same(req.params, { name: "world" }, "params are attached");

const reqWithParams = {
Expand All @@ -338,7 +417,8 @@ test("prepareRequest() - attach params", async (t) => {
ctx2.prepareRequest(
reqWithParams as unknown as Request,
{},
ctx2.find("GET", "/hello/world")
// @ts-expect-error: internal
ctx2.router.find("GET", "/hello/world")
);
t.same(
reqWithParams.params,
Expand All @@ -353,7 +433,8 @@ test("prepareRequest() - attach params", async (t) => {
ctx2.prepareRequest(
reqWithParams2 as unknown as Request,
{},
ctx2.find("GET", "/hello/world")
// @ts-expect-error: internal
ctx2.router.find("GET", "/hello/world")
);
t.same(
reqWithParams2.params,
Expand Down
Loading