add pathParams

This commit is contained in:
HorizonCode 2023-05-11 06:46:41 +02:00
parent 5da902fb3b
commit 129366c087
2 changed files with 147 additions and 59 deletions

View File

@ -2,12 +2,14 @@ import { Status } from "https://deno.land/std@0.186.0/http/http_status.ts";
import { HTTPServer } from "../http_server.ts"; import { HTTPServer } from "../http_server.ts";
const httpServer = new HTTPServer(); const httpServer = new HTTPServer();
httpServer.add("GET", "/", (_req, rep) => { httpServer.add("GET", "/", (req, rep) => {
rep.status(Status.Teapot) rep.status(Status.Teapot)
.header("working", "true") .header("working", "true")
.type("application/json")
.cookie("working", "true"); .cookie("working", "true");
console.log(_req.cookie("working")); console.log(req.cookie("working"));
return JSON.stringify( return JSON.stringify(
{ {
code: Status.Teapot, code: Status.Teapot,
@ -17,6 +19,19 @@ httpServer.add("GET", "/", (_req, rep) => {
2, 2,
); );
}); });
httpServer.add("GET", "/api/user/:userId", (req, rep) => {
rep.status(Status.Teapot)
.type("application/json");
return JSON.stringify(
{
code: Status.Teapot,
message: `UserID is ${req.pathParams["userId"]}`,
},
null,
2,
);
});
httpServer.listen({ httpServer.listen({
port: 8080, port: 8080,
staticLocalDir: "/static", staticLocalDir: "/static",

View File

@ -18,6 +18,10 @@ type RouteHandler = (
) => ) =>
| Promise<unknown> | Promise<unknown>
| unknown; | unknown;
export type RouteParam = {
idx: number;
paramKey: string;
};
export class HTTPServer { export class HTTPServer {
private server?: Deno.Listener; private server?: Deno.Listener;
@ -47,75 +51,109 @@ export class HTTPServer {
} }
} }
private async handleHttp(conn: Deno.Conn){ private async handleHttp(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn); const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) { for await (const requestEvent of httpConn) {
const url = new URL(requestEvent.request.url); const url = new URL(requestEvent.request.url);
const filepath = decodeURIComponent(url.pathname); const filepath = decodeURIComponent(url.pathname);
if (this.staticServePath && filepath.startsWith(this.staticServePath)) { if (this.staticServePath && filepath.startsWith(this.staticServePath)) {
const fileDir = filepath.split("/").slice(2).join("/"); const fileDir = filepath.split("/").slice(2).join("/");
const pathLoc = path.join( const pathLoc = path.join(
Deno.cwd(), Deno.cwd(),
this.staticLocalDir as string, this.staticLocalDir as string,
fileDir, fileDir,
); );
let file; let file;
try { try {
file = await Deno.open(pathLoc, { read: true }); file = await Deno.open(pathLoc, { read: true });
} catch { } catch {
// If the file cannot be opened, return a "404 Not Found" response // If the file cannot be opened, return a "404 Not Found" response
await requestEvent.respondWith(
new Response(
JSON.stringify({
code: 404,
message: `File ${filepath} not found!`,
}),
{
status: Status.NotFound,
},
),
);
continue;
}
const readableStream = file.readable;
const response = new Response(readableStream);
await requestEvent.respondWith(response);
return;
}
const routeName = `${requestEvent.request.method}@${filepath}`;
const route = this.routes.has(routeName)
? this.routes.get(routeName)
: undefined;
if (route) {
const routeReply: RouteReply = new RouteReply();
const handler = await route.handler(
new RouteRequest(requestEvent.request),
routeReply,
);
await requestEvent.respondWith(
new Response(handler as string, {
status: routeReply.statusCode,
headers: routeReply.headers,
statusText: STATUS_TEXT[routeReply.statusCode],
}),
);
} else {
await requestEvent.respondWith( await requestEvent.respondWith(
new Response( new Response(
JSON.stringify({ JSON.stringify({
code: 404, code: 404,
message: `Route ${routeName} not found!`, message: `File ${filepath} not found!`,
}), }),
{ {
status: Status.NotFound, status: Status.NotFound,
}, },
), ),
); );
continue;
} }
const readableStream = file.readable;
const response = new Response(readableStream);
await requestEvent.respondWith(response);
return;
} }
const routeRequest = new RouteRequest(requestEvent.request);
const routeReply: RouteReply = new RouteReply();
const routeName = `${requestEvent.request.method}@${filepath}`;
let route = this.routes.has(routeName)
? this.routes.get(routeName)
: undefined;
if (route) {
const handler = await route.handler(
routeRequest,
routeReply,
);
await requestEvent.respondWith(
new Response(handler as string, {
status: routeReply.statusCode,
headers: routeReply.headers,
statusText: STATUS_TEXT[routeReply.statusCode],
}),
);
continue;
}
route = Array.from(this.routes.values()).find((route) =>
routeWithParamsRouteMatcher(routeRequest, route)
);
if (route) {
const routeParamsMap: RouteParam[] = extractRouteParams(route.path);
const routeSegments: string[] = filepath.split("/");
routeRequest.pathParams = routeParamsMap.reduce(
(accum: { [key: string]: string | number }, curr: RouteParam) => {
return {
...accum,
[curr.paramKey]: routeSegments[curr.idx],
};
},
{},
);
const handler = await route.handler(
routeRequest,
routeReply,
);
await requestEvent.respondWith(
new Response(handler as string, {
status: routeReply.statusCode,
headers: routeReply.headers,
statusText: STATUS_TEXT[routeReply.statusCode],
}),
);
continue;
}
await requestEvent.respondWith(
new Response(
JSON.stringify({
code: 404,
message: `Route ${routeName} not found!`,
}),
{
status: Status.NotFound,
},
),
);
}
} }
get(path: string, handler: RouteHandler) { get(path: string, handler: RouteHandler) {
@ -145,6 +183,31 @@ export class HTTPServer {
} }
} }
export const routeWithParamsRouteMatcher = (
req: RouteRequest,
route: Route,
): boolean => {
const routeMatcherRegEx = new RegExp(`^${routeParamPattern(route.path)}$`);
return (
req.method === route.method &&
route.path.includes("/:") &&
routeMatcherRegEx.test(req.path)
);
};
export const routeParamPattern: (route: string) => string = (route) =>
route.replace(/\/\:[^/]{1,}/gi, "/[^/]{1,}").replace(/\//g, "\\/");
export const extractRouteParams: (route: string) => RouteParam[] = (route) =>
route.split("/").reduce((accum: RouteParam[], curr: string, idx: number) => {
if (/:[A-Za-z1-9]{1,}/.test(curr)) {
const paramKey: string = curr.replace(":", "");
const param: RouteParam = { idx, paramKey };
return [...accum, param];
}
return accum;
}, []);
export class Route { export class Route {
routeName: string; routeName: string;
path: string; path: string;
@ -160,10 +223,19 @@ export class Route {
} }
export class RouteRequest { export class RouteRequest {
url: string;
path: string;
headers: Headers; headers: Headers;
method: string;
pathParams: { [key: string]: string | number };
constructor(request: Request) { constructor(request: Request) {
this.url = request.url;
const urlObj = new URL(request.url);
this.path = decodeURIComponent(urlObj.pathname);
this.headers = request.headers; this.headers = request.headers;
this.method = request.method;
this.pathParams = {};
} }
header(name: string) { header(name: string) {
@ -208,7 +280,7 @@ export class RouteReply {
httpOnly?: boolean; httpOnly?: boolean;
sameSite?: "Strict" | "Lax" | "None"; sameSite?: "Strict" | "Lax" | "None";
unparsed?: string[]; unparsed?: string[];
}) { }): RouteReply {
if (!value) { if (!value) {
cookie.deleteCookie(this.headers, name, { cookie.deleteCookie(this.headers, name, {
domain: attributes?.domain, domain: attributes?.domain,
@ -228,5 +300,6 @@ export class RouteReply {
unparsed: attributes?.unparsed, unparsed: attributes?.unparsed,
}); });
} }
return this;
} }
} }