diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b58b603
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deno-experiments.iml b/.idea/deno-experiments.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/deno-experiments.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deno.xml b/.idea/deno.xml
new file mode 100644
index 0000000..b03feb5
--- /dev/null
+++ b/.idea/deno.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..28a804d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..57f48ac
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..288b36b
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application.ts b/application.ts
new file mode 100644
index 0000000..e2b8f44
--- /dev/null
+++ b/application.ts
@@ -0,0 +1,119 @@
+import { serve, ServeInit } from "https://deno.land/std@0.119.0/http/server.ts";
+import { Handler } from "./handler.ts";
+import {CatchFunc, HandleFunc, Matcher} from "./types.ts";
+import { Request as RoarterRequest } from "./request.ts";
+
+export class Application {
+ handlers: Handler[] = [];
+ catchFunc: CatchFunc = () => {
+ throw new Error("Not Implemented");
+ };
+
+ match(custom: Matcher): Handler {
+ const handler = new Handler().match(custom);
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ get get(): Handler {
+ const handler = new Handler().get;
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ get post(): Handler {
+ const handler = new Handler().post;
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ get put(): Handler {
+ const handler = new Handler().put;
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ get delete(): Handler {
+ const handler = new Handler().delete;
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ get patch(): Handler {
+ const handler = new Handler().patch;
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ path(path: string): Handler {
+ const handler = new Handler().path(path);
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ queries(queries: string[]): Handler {
+ const handler = new Handler().queries(queries);
+ this.handlers.push(handler);
+ return handler;
+ }
+
+ handle(handler: HandleFunc | Application): Handler {
+ const h = new Handler().handle(handler);
+ this.handlers.push(h);
+ return h;
+ }
+
+ catch(fn: CatchFunc) {
+ this.catchFunc = fn;
+ }
+
+ // Handler runs all the registered handlers and returns the first
+ // response it finds. After returning the first response, it runs
+ // the remaining handlers. The purpose of this is to support middleware
+ // after a response has been sent.
+ async runHandlers(req: RoarterRequest): Promise {
+ let runRemaining = true
+ let i = 0;
+ try {
+ try {
+ for (i; i < this.handlers.length; i++) {
+ const handler = this.handlers[i];
+ const response = await handler.run(req);
+ if (response) {
+ i++;
+ return response;
+ }
+ }
+ } catch (e) {
+ // If an error occurs, we wan't to skip all handlers and run the catchFunc
+ runRemaining = false
+ return await this.catchFunc(req, e);
+ }
+ // If after running all the handlers there is no response, throw the error
+ throw new Error(
+ `No Response was sent for ${req.method} ${req.url}`,
+ );
+ } finally {
+ if (runRemaining) {
+ // Since at this point we have already returned a response,
+ // we run the remaining handlers ignoring their response and errors
+ for (i; i < this.handlers.length; i++) {
+ const handler = this.handlers[i];
+ handler.run(req).catch(e => {
+ this.catchFunc(req, e)
+ })
+ }
+ }
+ }
+ }
+
+ async handler(domReq: Request): Promise {
+ const req = new RoarterRequest(domReq);
+ return this.runHandlers(req)
+ }
+
+ async serve(options?: ServeInit) {
+ this.handler = this.handler.bind(this);
+ await serve(this.handler, options);
+ }
+}
diff --git a/handler.ts b/handler.ts
new file mode 100644
index 0000000..62253a3
--- /dev/null
+++ b/handler.ts
@@ -0,0 +1,83 @@
+import { HandleFunc, Matcher } from "./types.ts";
+import {
+ buildPathMatcher,
+ buildQueryMatcher,
+ deleteMatcher,
+ getMatcher,
+ patchMatcher,
+ postMatcher,
+ putMatcher,
+} from "./matchers.ts";
+import { Response } from "./response.ts";
+import { Request } from "./request.ts";
+import {Application} from "./application.ts";
+
+export class Handler {
+ matchers: Matcher[] = [];
+ handler: (HandleFunc | Application) | null = null;
+
+ private isHandlerASubRouter(): boolean {
+ return this.handler instanceof Application
+ }
+
+ runMatchers(req: Request): boolean {
+ for (let i = 0; i < this.matchers.length; i++) {
+ const matcher: Matcher = this.matchers[i];
+ if (!matcher(req, this.isHandlerASubRouter())) return false;
+ }
+ return true;
+ }
+
+ async runHandler(req: Request) {
+ if (!this.handler) return;
+ if (this.isHandlerASubRouter()) {
+ return (this.handler as Application).runHandlers(req)
+ } else {
+ return (this.handler as HandleFunc)(req);
+ }
+ }
+
+ match(custom: Matcher): Handler {
+ this.matchers.push(custom);
+ return this;
+ }
+
+ get get(): Handler {
+ return this.match(getMatcher);
+ }
+
+ get post(): Handler {
+ return this.match(postMatcher);
+ }
+
+ get put(): Handler {
+ return this.match(putMatcher);
+ }
+
+ get delete(): Handler {
+ return this.match(deleteMatcher);
+ }
+
+ get patch(): Handler {
+ return this.match(patchMatcher);
+ }
+
+ path(urlPath: string): Handler {
+ return this.match(buildPathMatcher(urlPath));
+ }
+
+ queries(queries: string[]): Handler {
+ return this.match(buildQueryMatcher(queries));
+ }
+
+ handle(handler: HandleFunc | Application) {
+ this.handler = handler;
+ return this;
+ }
+
+ async run(req: Request) {
+ if (this.runMatchers(req)) {
+ return this.runHandler(req);
+ }
+ }
+}
diff --git a/matchers.ts b/matchers.ts
new file mode 100644
index 0000000..8902dcb
--- /dev/null
+++ b/matchers.ts
@@ -0,0 +1,65 @@
+import { Matcher, ROMap } from "./types.ts";
+import { extract, match } from "./utils/path.ts";
+
+export const getMatcher: Matcher = (req) => {
+ return req.method === "GET";
+};
+
+export const postMatcher: Matcher = (req) => {
+ return req.method === "POST";
+};
+
+export const putMatcher: Matcher = (req) => {
+ return req.method === "PUT";
+};
+
+export const deleteMatcher: Matcher = (req) => {
+ return req.method === "DELETE";
+};
+
+export const patchMatcher: Matcher = (req) => {
+ return req.method === "PATCH";
+};
+
+export const buildPathMatcher: (path: string) => Matcher = (path) => {
+ const prefixKey = "PathMatcher#PathPrefix"
+ return (req, matcherIsForASubRouter) => {
+ const url = new URL(req.url);
+
+ // If the matcher is for a subrouter (as opposed to a handlerFunc), then we want to match the prefix
+ // of the path name and store it for later use
+ if (matcherIsForASubRouter) {
+ let pathCopy = path
+ if (req.vars.has(prefixKey)) {
+ pathCopy = req.vars.get(prefixKey) + pathCopy
+ }
+ if (!match(pathCopy, url.pathname, true)) return false;
+ req.vars.set(prefixKey, pathCopy)
+ const m = extract(pathCopy, url.pathname, true);
+ for (const [key, val] of m) {
+ req.params.set(key, val)
+ }
+ return true
+ } else {
+ let pathCopy = path
+ if (req.vars.has(prefixKey)) {
+ pathCopy = req.vars.get(prefixKey) + pathCopy
+ }
+ if (!match(pathCopy, url.pathname)) return false;
+ const m = extract(pathCopy, url.pathname);
+ for (const [key, val] of m) {
+ req.params.set(key, val)
+ }
+ return true;
+ }
+ };
+};
+
+export const buildQueryMatcher: (queries: string[]) => Matcher = (queries) => {
+ return (req) => {
+ for (const query of queries) {
+ if (!req.queries.has(query)) return false;
+ }
+ return true;
+ };
+};
diff --git a/request.ts b/request.ts
new file mode 100644
index 0000000..69b6d07
--- /dev/null
+++ b/request.ts
@@ -0,0 +1,17 @@
+import { ROMap } from "./types.ts";
+
+class RoarterRequest extends Request {
+ public params: Map = new Map();
+ public vars: Map = new Map();
+ public queries: URLSearchParams;
+ public pathname: string;
+
+ constructor(input: RequestInfo, init?: RequestInit) {
+ super(input, init);
+ const url = new URL(this.url);
+ this.pathname = url.pathname;
+ this.queries = url.searchParams;
+ }
+}
+
+export { RoarterRequest as Request };
diff --git a/response.ts b/response.ts
new file mode 100644
index 0000000..9edea47
--- /dev/null
+++ b/response.ts
@@ -0,0 +1,9 @@
+class RoarterResponse extends Response {
+ // @ts-ignore
+ constructor(body?: BodyInit | null, init?: ResponseInit) {
+ // @ts-ignore
+ super(body, init);
+ }
+}
+
+export { RoarterResponse as Response };
diff --git a/tests/roarter.test.ts b/tests/roarter.test.ts
new file mode 100644
index 0000000..23c09c7
--- /dev/null
+++ b/tests/roarter.test.ts
@@ -0,0 +1,471 @@
+import {
+ assert,
+ assertEquals,
+} from "https://deno.land/std@0.119.0/testing/asserts.ts";
+import { Application } from "../application.ts";
+import { Response } from "../response.ts";
+import { Request } from "../request.ts";
+
+Deno.test("matchers can go before or after handler", async () => {
+ let count = 0;
+
+ const t = new Application();
+ t.get.path("/hello1").handle(async (req) => {
+ count++;
+ });
+ t.handle(async (req) => {
+ count++;
+ }).get.path("/hello2");
+ t.get.handle(async (req) => {
+ count++;
+ }).path("/hello3");
+ t.handle(async (req) => new Response("last"));
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/hello1",
+ {
+ method: "GET",
+ },
+ ),
+ );
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/hello2",
+ {
+ method: "GET",
+ },
+ ),
+ );
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/hello3",
+ {
+ method: "GET",
+ },
+ ),
+ );
+
+ assertEquals(count, 3);
+});
+
+Deno.test("verb matchers", async () => {
+ {
+ const t = new Application();
+ let count = 0;
+ t.get.handle(async (req) => {
+ count++;
+ return new Response();
+ });
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/",
+ {
+ method: "GET",
+ },
+ ),
+ );
+ assertEquals(count, 1);
+ }
+
+ {
+ const t = new Application();
+ let count = 0;
+ t.post.handle(async (req) => {
+ count++;
+ return new Response();
+ });
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/",
+ {
+ method: "POST",
+ },
+ ),
+ );
+ assertEquals(count, 1);
+ }
+
+ {
+ const t = new Application();
+ let count = 0;
+ t.put.handle(async (req) => {
+ count++;
+ return new Response();
+ });
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/",
+ {
+ method: "PUT",
+ },
+ ),
+ );
+ assertEquals(count, 1);
+ }
+
+ {
+ const t = new Application();
+ let count = 0;
+ t.delete.handle(async (req) => {
+ count++;
+ return new Response();
+ });
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/",
+ {
+ method: "DELETE",
+ },
+ ),
+ );
+ assertEquals(count, 1);
+ }
+
+ {
+ const t = new Application();
+ let count = 0;
+ t.patch.handle(async (req) => {
+ count++;
+ return new Response();
+ });
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/",
+ {
+ method: "PATCH",
+ },
+ ),
+ );
+ assertEquals(count, 1);
+ }
+
+ {
+ const t = new Application();
+ let count = 0;
+ t.delete.handle(async (req) => {
+ count++;
+ });
+ t.handle(async (req) => new Response(""));
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "https://example.com/",
+ {
+ method: "GET",
+ },
+ ),
+ );
+ assertEquals(count, 0);
+ }
+});
+
+Deno.test("path matcher", async () => {
+ {
+ const t = new Application();
+ let id;
+ t.handle(async (req) => {
+ id = req.params.get("id");
+ return new Response();
+ }).path("/users/:id");
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/users/2"));
+ assertEquals(id, "2");
+ }
+
+ {
+ const t = new Application();
+ let id1;
+ let id2;
+ let id3;
+ t.handle(async (req) => {
+ id1 = req.params.get("id1");
+ id2 = req.params.get("id2");
+ id3 = req.params.get("id3");
+ return new Response();
+ }).path("/users/:id1/:id2/:id3");
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/users/1/2/3"));
+ assertEquals(id1, "1");
+ assertEquals(id2, "2");
+ assertEquals(id3, "3");
+ }
+
+ {
+ const t = new Application();
+ let id;
+ t.handle(async (req) => {
+ id = req.params.get("id");
+ return new Response();
+ }).path("/users/:id");
+ // @ts-ignore
+ await t.runHandlers(
+ new Request(
+ "http://example.com/users/some-weird.key[with](funny);chars;",
+ ),
+ );
+ assertEquals(id, "some-weird.key[with](funny);chars;");
+ }
+
+ {
+ const t = new Application();
+ let id;
+ t.handle(async (req) => {
+ id = req.params.get("some-weird.key[with](funny);chars;");
+ return new Response();
+ }).path("/users/:some-weird.key[with](funny);chars;");
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/users/1"));
+ assertEquals(id, "1");
+ }
+});
+
+Deno.test("edge cases with paths", async () => {
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = true;
+ return new Response();
+ }).path("/");
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/"));
+ assert(hit);
+ }
+
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = true;
+ return new Response();
+ }).path("/");
+ // @ts-ignore
+ t.runHandlers(new Request("http://example.com"));
+ assert(hit);
+ }
+
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = true;
+ return new Response();
+ }).path("/");
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com?key=val"));
+ assert(hit);
+ }
+
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = true;
+ return new Response();
+ }).path("/");
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/?key=val"));
+ assert(hit);
+ }
+
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = true;
+ return new Response();
+ }).path("/");
+ t.handle(async (req) => new Response());
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/hello?key=val"));
+ assert(!hit);
+ }
+});
+
+Deno.test("query params matching", async () => {
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = req.queries.get("key");
+ return new Response();
+ }).path("/").queries(["key"]);
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/?key=val"));
+ assertEquals(hit, "val");
+ }
+
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = req.queries.get("key1") as string + req.queries.get("key2");
+ return new Response();
+ }).path("/hello/there").queries(["key1", "key2"]);
+ // @ts-ignore
+ await t.runHandlers(
+ new Request("http://example.com/hello/there?key1=val1&key2=val2"),
+ );
+ assertEquals(hit, "val1val2");
+ }
+
+ {
+ const t = new Application();
+ let hit;
+ t.handle(async (req) => {
+ hit = req.queries.get("key1") as string + req.queries.get("key2");
+ }).path("/hello/there").queries(["key"]);
+ t.handle(async (req) => new Response());
+ // @ts-ignore
+ await t.runHandlers(new Request("http://example.com/hello/there?nope=val"));
+ assertEquals(hit, undefined);
+ }
+});
+
+Deno.test("Roarter runs all handlers sequentially UNTIL it receives the first Response. Then runs the remaining asynchronously so as to not keep the client hanging.", async () => {
+ const t = new Application();
+ let res = "";
+
+ t.handle(async (req) => {
+ await new Promise((resolve, reject) => {
+ setTimeout(() => resolve(null), 100);
+ });
+ res += "first";
+ });
+ t.path("/hello").handle(async (req) => {
+ await new Promise((resolve, reject) => {
+ setTimeout(() => resolve(null), 100);
+ });
+ res += "second";
+ return new Response("second");
+ });
+ t.path("/hello").handle(async (req) => {
+ await new Promise((resolve, reject) => {
+ setTimeout(() => resolve(null), 100);
+ });
+ res += "third";
+ return new Response("third");
+ });
+ t.path("/hello2").handle(async (req) => {
+ await new Promise((resolve, reject) => {
+ setTimeout(() => resolve(null), 100);
+ });
+ res += "no";
+ return new Response("no");
+ });
+ t.handle(async (req) => {
+ await new Promise((resolve, reject) => {
+ setTimeout(() => resolve(null), 100);
+ });
+ res += "fourth";
+ });
+ t.handle(async (req) => {
+ await new Promise((resolve, reject) => {
+ setTimeout(() => resolve(null), 100);
+ });
+ res += "fifth";
+ });
+
+ // @ts-ignore
+ const response = await t.runHandlers(
+ new Request("https://example.com/hello", { method: "GET" }),
+ );
+ console.log("received response");
+ const text = await response.text();
+ // Handler returns the first Response it finds
+ assertEquals(text, "second");
+ // Since 'first' and 'second' were executed synchronously and the rest were not,
+ // we only expect 'first' and 'second' to have been waited on
+ assertEquals(res, "firstsecond");
+
+ // The rest will appear after some time
+ await new Promise((resolve, reject) => {
+ setTimeout(() => resolve(null), 1000);
+ });
+ assertEquals(res, "firstsecondthirdfourthfifth");
+});
+
+Deno.test("Roarter successfully catches the error and hands it off to .catch", async () => {
+ const t = new Application();
+ t.handle(async (req) => {
+ throw new Error("error");
+ });
+ t.catch(async (req, err) => {
+ return new Response(err.message);
+ });
+ // @ts-ignore
+ const response = await t.runHandlers(
+ new Request("https://example.com/hello", { method: "GET" }),
+ );
+ const text = await response.text();
+ assertEquals(text, "error");
+});
+
+Deno.test("Roarter catches the error from postfix middleware", async () => {
+ const t = new Application();
+ let caught = false
+ t.get.handle(async req => {
+ return new Response("done")
+ })
+ t.handle(async (req) => {
+ throw new Error("error");
+ });
+ t.catch(async (req, err) => {
+ caught = true
+ return new Response(err.message);
+ });
+ // @ts-ignore
+ const response = await t.runHandlers(
+ new Request("https://example.com/hello", { method: "GET" }),
+ );
+ await new Promise((resolve) => {
+ setTimeout(() => resolve(null), 200)
+ })
+ assert(caught);
+});
+
+Deno.test("subrouters work", async () => {
+ const app1 = new Application();
+
+ app1.path("/hello").handle(async req => {
+ return new Response("hello")
+ })
+
+ const app2 = new Application()
+
+ app2.path("/api").handle(app1)
+
+ const response = await app2.runHandlers(
+ new Request("https://example.com/api/hello", { method: "GET" }),
+ )
+
+ assertEquals(await response.text(), "hello")
+});
+
+Deno.test("subrouters work with params", async () => {
+ const app1 = new Application();
+
+ app1.path("/hello/:p2").handle(async req => {
+ assertEquals(req.params.get("p1"), "v1")
+ assertEquals(req.params.get("p2"), "v2")
+ return new Response("hello")
+ })
+
+ const app2 = new Application()
+
+ app2.path("/api/:p1").handle(app1)
+
+ const response = await app2.runHandlers(
+ new Request("https://example.com/api/v1/hello/v2", { method: "GET" }),
+ )
+
+ assertEquals(await response.text(), "hello")
+});
diff --git a/tests/utils/path.test.ts b/tests/utils/path.test.ts
new file mode 100644
index 0000000..e943380
--- /dev/null
+++ b/tests/utils/path.test.ts
@@ -0,0 +1,104 @@
+import {
+ assert,
+ assertEquals,
+ assertThrows,
+} from "https://deno.land/std@0.119.0/testing/asserts.ts";
+import { extract, match } from "../../utils/path.ts";
+
+Deno.test("match correctly matches different paths", () => {
+ assert(
+ match(
+ "/user/:id",
+ "/user/294",
+ ),
+ );
+
+ assert(
+ !match(
+ "/user/:id/profile",
+ "/user/294",
+ ),
+ );
+
+ assert(
+ match(
+ "/user/:id",
+ "/user/294/",
+ ),
+ );
+
+ assert(
+ match(
+ "/:routeParams",
+ "/jon-ros-lu",
+ ),
+ );
+
+ assert(
+ match(
+ "/api",
+ "/api/v1/hello",
+ true
+ ),
+ );
+
+ assert(
+ match(
+ "/",
+ "/api/v1/hello",
+ true
+ ),
+ );
+
+ assert(
+ match(
+ "/api/v1/hello",
+ "/api/v1/hello",
+ true
+ ),
+ );
+
+ assert(
+ match(
+ "/api/:v1",
+ "/api/v1/hello",
+ true
+ ),
+ );
+});
+
+Deno.test("extracts correctly returns the keys and values", () => {
+ {
+ const m = extract("/user/:id", "/user/2");
+ assert(m.size === 1);
+ assertEquals(m.get("id"), "2");
+ assertEquals(m.get("notexist"), undefined);
+ }
+
+ {
+ const m = extract("/user", "/user");
+ assert(m.size === 0);
+ assertEquals(m.get("notexist"), undefined);
+ }
+
+ {
+ const m = extract("/user/:id", "/user/2/hello/there", true);
+ assert(m.size === 1);
+ assertEquals(m.get("id"), "2");
+ assertEquals(m.get("notexist"), undefined);
+ }
+
+ {
+ const m = extract("/user/:id/:id2", "/user/2/hello/there", true);
+ assert(m.size === 2);
+ assertEquals(m.get("id"), "2");
+ assertEquals(m.get("id2"), "hello");
+ assertEquals(m.get("notexist"), undefined);
+ }
+
+ {
+ assertThrows(() => extract("/user/:id/wow", "/user/2"));
+ assertThrows(() => extract("/user", "/user/2"));
+ assertThrows(() => extract("/user/id", "/user/2"));
+ }
+});
diff --git a/types.ts b/types.ts
new file mode 100644
index 0000000..1cc4f5d
--- /dev/null
+++ b/types.ts
@@ -0,0 +1,10 @@
+import { Response } from "./response.ts";
+import { Request } from "./request.ts";
+
+export type Matcher = (req: Request, matcherIsForASubRouter?: boolean) => boolean;
+export type HandleFunc = (req: Request) => Promise;
+export type CatchFunc = (req: Request, err: Error) => Promise;
+export interface ROMap {
+ get(key: Key): Val | undefined;
+ has(key: Key): boolean;
+}
diff --git a/utils/path.ts b/utils/path.ts
new file mode 100644
index 0000000..94e5404
--- /dev/null
+++ b/utils/path.ts
@@ -0,0 +1,69 @@
+/**
+ * match returns true if a Roarter path, of the form /user/:id, matches a given real-world path,
+ * such as /user/83
+ * @param matchPath
+ * @param realPath
+ * @param prefix
+ */
+export function match(matchPath: string, realPath: string, prefix: boolean = false): boolean {
+ const matchPathSplit = matchPath.split("/").filter(s => s !== '');
+ const realPathSplit = realPath.split("/").filter(s => s !== '');
+
+ if (!prefix && matchPathSplit.length !== realPathSplit.length) {
+ return false;
+ }
+
+ for (let i = 0; i < matchPathSplit.length; i++) {
+ const matchStr = matchPathSplit[i];
+ const realStr = realPathSplit[i];
+
+ // a ':' matches everything, so continue
+ if (matchStr[0] === ":") continue;
+
+ if (matchStr !== realStr) return false;
+ }
+
+ return true;
+}
+
+/**
+ * extract returns a map of the matchPath keys mapped to the realPath values
+ * @param matchPath
+ * @param realPath
+ * @param prefix
+ */
+export function extract(
+ matchPath: string,
+ realPath: string,
+ prefix: boolean = false
+): Map {
+ const matchPathSplit = matchPath.split("/").filter(s => s !== '');
+ const realPathSplit = realPath.split("/").filter(s => s !== '');
+
+ if (!prefix && matchPathSplit.length !== realPathSplit.length) {
+ throw new Error(
+ "matchPath and realPath do not match. Call match before calling this function.",
+ );
+ }
+
+ const m = new Map();
+
+ for (let i = 0; i < matchPathSplit.length; i++) {
+ const matchStr = matchPathSplit[i];
+ const realStr = realPathSplit[i];
+
+ // a ':' matches everything, so continue
+ if (matchStr[0] === ":") {
+ m.set(matchStr.substr(1), realStr);
+ continue;
+ }
+
+ if (matchStr !== realStr) {
+ throw new Error(
+ "matchPath and realPath do not match. Call match before calling this function.",
+ );
+ }
+ }
+
+ return m;
+}