refactored context and router
This commit is contained in:
@ -3,13 +3,13 @@ import { type ExtractRouteParams } from "@lib/router.ts";
|
|||||||
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
|
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
|
||||||
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
|
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
|
||||||
import { type Cookie } from "@std/http/cookie";
|
import { type Cookie } from "@std/http/cookie";
|
||||||
import { getMessageFromError } from "@shared/utils/result.ts";
|
import { getMessageFromError, ok } from "@shared/utils/result.ts";
|
||||||
import {
|
import {
|
||||||
InferSchemaType,
|
InferSchemaType,
|
||||||
Schema,
|
Schema,
|
||||||
SchemaValidationError,
|
SchemaValidationError,
|
||||||
} from "@shared/utils/validator.ts";
|
} from "@shared/utils/validator.ts";
|
||||||
import { ResultAsync } from "@shared/utils/resultasync.ts";
|
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
||||||
import log from "@shared/utils/logger.ts";
|
import log from "@shared/utils/logger.ts";
|
||||||
import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts";
|
import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts";
|
||||||
|
|
||||||
@ -25,63 +25,79 @@ const SECURITY_HEADERS: Headers = new Headers({
|
|||||||
//"Content-Security-Policy":
|
//"Content-Security-Policy":
|
||||||
// "default-src 'self'; script-src 'self' 'unsafe-inline'",
|
// "default-src 'self'; script-src 'self' 'unsafe-inline'",
|
||||||
});
|
});
|
||||||
const HTML_CONTENT_TYPE: [string, string] = [
|
|
||||||
"Content-Type",
|
const HTML_CONTENT_TYPE: string = "text/html; charset=UTF-8";
|
||||||
"text/html; charset=UTF-8",
|
const JSON_CONTENT_TYPE: string = "application/json; charset=utf-8";
|
||||||
];
|
|
||||||
const JSON_CONTENT_TYPE: [string, string] = [
|
|
||||||
"Content-Type",
|
|
||||||
"application/json; charset=utf-8",
|
|
||||||
];
|
|
||||||
|
|
||||||
function mergeHeaders(...headers: Headers[]): Headers {
|
function mergeHeaders(...headers: Headers[]): Headers {
|
||||||
const mergedHeaders = new Headers();
|
const merged = new Headers();
|
||||||
for (const _headers of headers) {
|
for (const hdr of headers) {
|
||||||
for (const [key, value] of _headers.entries()) {
|
hdr.forEach((value, key) => merged.set(key, value));
|
||||||
mergedHeaders.set(key, value);
|
|
||||||
}
|
}
|
||||||
}
|
return merged;
|
||||||
return mergedHeaders;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ContextWithSchema<
|
||||||
|
C extends Context<string, any, any>,
|
||||||
|
ReqSchema extends Schema<any>,
|
||||||
|
ResSchema extends Schema<any>,
|
||||||
|
> = C extends Context<infer S, any, any> ? Context<S, ReqSchema, ResSchema> & {
|
||||||
|
schema: { req: ReqSchema; res: ResSchema };
|
||||||
|
}
|
||||||
|
: never;
|
||||||
|
|
||||||
export class Context<
|
export class Context<
|
||||||
S extends string = string,
|
S extends string = string,
|
||||||
ReqSchema extends Schema<any> | never = never,
|
ReqSchema extends Schema<any> = Schema<unknown>,
|
||||||
ResSchema extends Schema<any> | never = never,
|
ResSchema extends Schema<any> = Schema<unknown>,
|
||||||
> {
|
> {
|
||||||
private _url?: URL;
|
private _url?: URL;
|
||||||
private _hostname?: string;
|
private _hostname?: string;
|
||||||
private _port?: number;
|
private _port?: number;
|
||||||
private _cookies?: Record<string, string>;
|
private _cookies?: Record<string, string>;
|
||||||
private _responseHeaders: Headers = new Headers();
|
|
||||||
public res: Response = new Response();
|
public res = new Response();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly req: Request,
|
public readonly req: Request,
|
||||||
public readonly info: Deno.ServeHandlerInfo<Deno.Addr>,
|
public readonly info: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||||
public readonly params: Params<ExtractRouteParams<S>>,
|
public readonly params: Params<ExtractRouteParams<S>>,
|
||||||
public schema: [ReqSchema, ResSchema] extends [never, never] ? never
|
|
||||||
: {
|
|
||||||
req: ReqSchema;
|
|
||||||
res: ResSchema;
|
|
||||||
},
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public schema?: { req: ReqSchema; res: ResSchema };
|
||||||
|
|
||||||
|
public withSchema<
|
||||||
|
Req extends Schema<any>,
|
||||||
|
Res extends Schema<any>,
|
||||||
|
>(
|
||||||
|
schema: { req: Req; res: Res },
|
||||||
|
): Context<S, Req, Res> & { schema: { req: Req; res: Res } } {
|
||||||
|
const ctx = new Context<S, Req, Res>(this.req, this.info, this.params);
|
||||||
|
ctx._url = this._url;
|
||||||
|
ctx._hostname = this._hostname;
|
||||||
|
ctx._port = this._port;
|
||||||
|
ctx._cookies = this._cookies;
|
||||||
|
ctx.res = this.res;
|
||||||
|
ctx.schema = schema;
|
||||||
|
return ctx as Context<S, Req, Res> & { schema: { req: Req; res: Res } };
|
||||||
|
}
|
||||||
|
|
||||||
public parseBody(): ResultAsync<
|
public parseBody(): ResultAsync<
|
||||||
InferSchemaType<ReqSchema>,
|
InferSchemaType<ReqSchema>,
|
||||||
SchemaValidationError | FailedToParseRequestAsJSON
|
SchemaValidationError | FailedToParseRequestAsJSON
|
||||||
> {
|
> {
|
||||||
if (!this.schema || !this.schema.req) {
|
|
||||||
log.error("No schema provided!");
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResultAsync
|
return ResultAsync
|
||||||
.fromPromise(
|
.fromPromise(
|
||||||
this.req.json(),
|
this.req.json(),
|
||||||
(e) => new FailedToParseRequestAsJSON(getMessageFromError(e)),
|
(e) => new FailedToParseRequestAsJSON(getMessageFromError(e)),
|
||||||
)
|
)
|
||||||
.andThen((data: unknown) => this.schema.req.parse(data));
|
.andThen((data: unknown) => {
|
||||||
|
if (!this.schema) {
|
||||||
|
return ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.schema?.req.parse(data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get url(): URL {
|
get url(): URL {
|
||||||
@ -93,29 +109,19 @@ export class Context<
|
|||||||
}
|
}
|
||||||
|
|
||||||
get preferredType(): Option<"json" | "html"> {
|
get preferredType(): Option<"json" | "html"> {
|
||||||
const headers = new Headers(this.req.headers);
|
const accept = this.req.headers.get("accept");
|
||||||
return fromNullableVal(headers.get("accept")).andThen(
|
if (!accept) return none;
|
||||||
(types_header) => {
|
const types = accept
|
||||||
const types = types_header.split(";")[0].trim().split(",");
|
.split(",")
|
||||||
|
.map((t) => t.split(";")[0].trim());
|
||||||
for (const type of types) {
|
if (types.includes("text/html")) return some("html");
|
||||||
if (type === "text/html") {
|
if (types.includes("application/json")) return some("json");
|
||||||
return some("html");
|
|
||||||
}
|
|
||||||
if (type === "application/json") {
|
|
||||||
return some("json");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return none;
|
return none;
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hostname(): Option<string> {
|
get hostname(): Option<string> {
|
||||||
if (this._hostname) return some(this._hostname);
|
if (this._hostname) return some(this._hostname);
|
||||||
|
|
||||||
const remoteAddr = this.info.remoteAddr;
|
const remoteAddr = this.info.remoteAddr;
|
||||||
|
|
||||||
if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
|
if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
|
||||||
this._hostname = remoteAddr.hostname;
|
this._hostname = remoteAddr.hostname;
|
||||||
return some(remoteAddr.hostname);
|
return some(remoteAddr.hostname);
|
||||||
@ -125,9 +131,7 @@ export class Context<
|
|||||||
|
|
||||||
get port(): Option<number> {
|
get port(): Option<number> {
|
||||||
if (this._port) return some(this._port);
|
if (this._port) return some(this._port);
|
||||||
|
|
||||||
const remoteAddr = this.info.remoteAddr;
|
const remoteAddr = this.info.remoteAddr;
|
||||||
|
|
||||||
if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
|
if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
|
||||||
this._port = remoteAddr.port;
|
this._port = remoteAddr.port;
|
||||||
return some(remoteAddr.port);
|
return some(remoteAddr.port);
|
||||||
@ -135,19 +139,27 @@ export class Context<
|
|||||||
return none;
|
return none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildHeaders(
|
||||||
|
initHeaders?: HeadersInit,
|
||||||
|
contentType?: string,
|
||||||
|
): Headers {
|
||||||
|
const merged = mergeHeaders(
|
||||||
|
SECURITY_HEADERS,
|
||||||
|
this.res.headers,
|
||||||
|
new Headers(initHeaders),
|
||||||
|
);
|
||||||
|
if (contentType) merged.set("Content-Type", contentType);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
public json(
|
public json(
|
||||||
body?: ResSchema extends Schema<infer T> ? T : object | string,
|
body?: ResSchema extends Schema<infer T> ? T : object | string,
|
||||||
init: ResponseInit = {},
|
init: ResponseInit = {},
|
||||||
): Response {
|
): Response {
|
||||||
const headers = mergeHeaders(
|
const headers = this.buildHeaders(init.headers, JSON_CONTENT_TYPE);
|
||||||
SECURITY_HEADERS,
|
let status = init.status ?? 200;
|
||||||
this._responseHeaders,
|
|
||||||
new Headers(init.headers),
|
|
||||||
);
|
|
||||||
headers.set(...JSON_CONTENT_TYPE);
|
|
||||||
let status = init.status || 200;
|
|
||||||
|
|
||||||
let responseBody: BodyInit | null = null;
|
let responseBody: BodyInit | null = null;
|
||||||
|
|
||||||
if (typeof body === "string") {
|
if (typeof body === "string") {
|
||||||
responseBody = body;
|
responseBody = body;
|
||||||
} else if (body !== undefined) {
|
} else if (body !== undefined) {
|
||||||
@ -162,82 +174,67 @@ export class Context<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(responseBody, {
|
this.res = new Response(responseBody, {
|
||||||
status,
|
status,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
return this.res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public json400(body?: object | string, init: ResponseInit = {}) {
|
public json400(
|
||||||
init.status = 400;
|
body?: ResSchema extends Schema<infer T> ? T : object | string,
|
||||||
return this.json(body, init);
|
init: ResponseInit = {},
|
||||||
|
): Response {
|
||||||
|
return this.json(body, { ...init, status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
public json500(body?: object | string, init: ResponseInit = {}) {
|
public json500(
|
||||||
init.status = 400;
|
body?: ResSchema extends Schema<infer T> ? T : object | string,
|
||||||
return this.json(body, init);
|
init: ResponseInit = {},
|
||||||
|
) {
|
||||||
|
return this.json(body, { ...init, status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
public html(body?: BodyInit | null, init: ResponseInit = {}): Response {
|
public html(body?: BodyInit | null, init: ResponseInit = {}): Response {
|
||||||
const headers = mergeHeaders(
|
const headers = this.buildHeaders(init.headers, HTML_CONTENT_TYPE);
|
||||||
SECURITY_HEADERS,
|
|
||||||
this._responseHeaders,
|
|
||||||
new Headers(init.headers),
|
|
||||||
);
|
|
||||||
headers.set(...HTML_CONTENT_TYPE);
|
|
||||||
const status = init.status ?? 200;
|
const status = init.status ?? 200;
|
||||||
|
this.res = new Response(body ?? null, { status, headers });
|
||||||
return new Response(body ?? null, {
|
return this.res;
|
||||||
status,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public redirect(url: string, permanent = false): Response {
|
public redirect(url: string, permanent = false): Response {
|
||||||
const headers = mergeHeaders(
|
const headers = mergeHeaders(
|
||||||
this._responseHeaders,
|
this.res.headers,
|
||||||
new Headers({ location: url }),
|
new Headers({ location: url }),
|
||||||
);
|
);
|
||||||
|
this.res = new Response(null, {
|
||||||
return new Response(null, {
|
|
||||||
status: permanent ? 301 : 302,
|
status: permanent ? 301 : 302,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
return this.res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public cookies = (() => {
|
public get cookies() {
|
||||||
const self = this;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get(name: string): Option<string> {
|
get: (name: string): Option<string> => {
|
||||||
if (!self._cookies) {
|
this._cookies ??= getCookies(this.req.headers);
|
||||||
self._cookies = getCookies(self.req.headers);
|
return fromNullableVal(this._cookies[name]);
|
||||||
}
|
|
||||||
|
|
||||||
return fromNullableVal(self._cookies[name]);
|
|
||||||
},
|
|
||||||
|
|
||||||
set(cookie: Cookie) {
|
|
||||||
setCookie(self._responseHeaders, cookie);
|
|
||||||
},
|
|
||||||
|
|
||||||
delete(name: string) {
|
|
||||||
deleteCookie(self._responseHeaders, name);
|
|
||||||
},
|
},
|
||||||
|
set: (cookie: Cookie) => setCookie(this.res.headers, cookie),
|
||||||
|
delete: (name: string) => deleteCookie(this.res.headers, name),
|
||||||
};
|
};
|
||||||
})();
|
}
|
||||||
|
|
||||||
static setParams<S extends string>(
|
static setParams<S extends string>(
|
||||||
ctx: Context<string>,
|
ctx: Context<string>,
|
||||||
params: Params<ExtractRouteParams<S>>,
|
params: Params<ExtractRouteParams<S>>,
|
||||||
): Context<S> {
|
): Context<S> {
|
||||||
const newCtx = new Context(ctx.req, ctx.info, params);
|
const newCtx = new Context(ctx.req, ctx.info, params) as typeof ctx;
|
||||||
|
|
||||||
newCtx._url = ctx._url;
|
newCtx._url = ctx._url;
|
||||||
newCtx._hostname = ctx._hostname;
|
newCtx._hostname = ctx._hostname;
|
||||||
newCtx._port = ctx._port;
|
newCtx._port = ctx._port;
|
||||||
newCtx._cookies = ctx._cookies;
|
newCtx._cookies = ctx._cookies;
|
||||||
newCtx._responseHeaders = ctx._responseHeaders;
|
|
||||||
newCtx.schema = ctx.schema;
|
newCtx.schema = ctx.schema;
|
||||||
|
|
||||||
return newCtx;
|
return newCtx;
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
Schema,
|
||||||
SchemaValidationError,
|
SchemaValidationError,
|
||||||
ValidationErrorDetail,
|
ValidationErrorDetail,
|
||||||
|
z,
|
||||||
} from "@shared/utils/validator.ts";
|
} from "@shared/utils/validator.ts";
|
||||||
|
|
||||||
export class ErrorBase extends Error {
|
export class ErrorBase extends Error {
|
||||||
|
|||||||
@ -8,9 +8,8 @@ type RequestHandler<
|
|||||||
S extends string,
|
S extends string,
|
||||||
ReqSchema extends Schema<any> = never,
|
ReqSchema extends Schema<any> = never,
|
||||||
ResSchema extends Schema<any> = never,
|
ResSchema extends Schema<any> = never,
|
||||||
> = (
|
> = (c: Context<S, ReqSchema, ResSchema>) => Promise<Response> | Response;
|
||||||
c: Context<S, ReqSchema, ResSchema>,
|
|
||||||
) => Promise<Response> | Response;
|
|
||||||
type RequestHandlerWithSchema<S extends string> = {
|
type RequestHandlerWithSchema<S extends string> = {
|
||||||
handler: RequestHandler<S>;
|
handler: RequestHandler<S>;
|
||||||
schema?: {
|
schema?: {
|
||||||
@ -37,20 +36,22 @@ const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found");
|
|||||||
|
|
||||||
class HttpRouter {
|
class HttpRouter {
|
||||||
public readonly routerTree = new RouterTree<MethodHandlers<any>>();
|
public readonly routerTree = new RouterTree<MethodHandlers<any>>();
|
||||||
pathPreprocessor?: (path: string) => string;
|
public pathPreprocessor?: (path: string) => string;
|
||||||
private middlewares: Middleware[] = [];
|
private middlewares: Middleware[] = [];
|
||||||
defaultNotFoundHandler: RequestHandler<string> = DEFAULT_NOT_FOUND_HANDLER;
|
public defaultNotFoundHandler: RequestHandler<string> =
|
||||||
|
DEFAULT_NOT_FOUND_HANDLER;
|
||||||
|
|
||||||
setPathProcessor(processor: (path: string) => string) {
|
public setPathProcessor(processor: (path: string) => string) {
|
||||||
this.pathPreprocessor = processor;
|
this.pathPreprocessor = processor;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
use(mw: Middleware): HttpRouter {
|
public use(mw: Middleware): this {
|
||||||
this.middlewares.push(mw);
|
this.middlewares.push(mw);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
add<S extends string>(
|
public add<S extends string>(
|
||||||
path: S,
|
path: S,
|
||||||
method: string,
|
method: string,
|
||||||
handler: RequestHandler<S>,
|
handler: RequestHandler<S>,
|
||||||
@ -59,7 +60,7 @@ class HttpRouter {
|
|||||||
req: Schema<any>;
|
req: Schema<any>;
|
||||||
},
|
},
|
||||||
): HttpRouter;
|
): HttpRouter;
|
||||||
add<S extends string>(
|
public add<S extends string>(
|
||||||
path: S[],
|
path: S[],
|
||||||
method: string,
|
method: string,
|
||||||
handler: RequestHandler<string>,
|
handler: RequestHandler<string>,
|
||||||
@ -68,7 +69,7 @@ class HttpRouter {
|
|||||||
req: Schema<any>;
|
req: Schema<any>;
|
||||||
},
|
},
|
||||||
): HttpRouter;
|
): HttpRouter;
|
||||||
add(
|
public add(
|
||||||
path: string | string[],
|
path: string | string[],
|
||||||
method: string,
|
method: string,
|
||||||
handler: RequestHandler<string>,
|
handler: RequestHandler<string>,
|
||||||
@ -95,31 +96,43 @@ class HttpRouter {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get<S extends string>(path: S, handler: RequestHandler<S>): HttpRouter;
|
public get<S extends string>(
|
||||||
get<S extends string>(
|
path: S,
|
||||||
|
handler: RequestHandler<S>,
|
||||||
|
): HttpRouter;
|
||||||
|
public get<S extends string>(
|
||||||
path: S[],
|
path: S[],
|
||||||
handler: RequestHandler<string>,
|
handler: RequestHandler<string>,
|
||||||
): HttpRouter;
|
): HttpRouter;
|
||||||
get(path: string | string[], handler: RequestHandler<string>): HttpRouter {
|
public get(
|
||||||
|
path: string | string[],
|
||||||
|
handler: RequestHandler<string>,
|
||||||
|
): HttpRouter {
|
||||||
if (Array.isArray(path)) {
|
if (Array.isArray(path)) {
|
||||||
return this.add(path, "GET", handler);
|
return this.add(path, "GET", handler);
|
||||||
}
|
}
|
||||||
return this.add(path, "GET", handler);
|
return this.add(path, "GET", handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
post<S extends string>(path: S, handler: RequestHandler<S>): HttpRouter;
|
public post<S extends string>(
|
||||||
post<S extends string>(
|
path: S,
|
||||||
|
handler: RequestHandler<S>,
|
||||||
|
): HttpRouter;
|
||||||
|
public post<S extends string>(
|
||||||
path: string[],
|
path: string[],
|
||||||
handler: RequestHandler<string>,
|
handler: RequestHandler<string>,
|
||||||
): HttpRouter;
|
): HttpRouter;
|
||||||
post(path: string | string[], handler: RequestHandler<string>): HttpRouter {
|
public post(
|
||||||
|
path: string | string[],
|
||||||
|
handler: RequestHandler<string>,
|
||||||
|
): HttpRouter {
|
||||||
if (Array.isArray(path)) {
|
if (Array.isArray(path)) {
|
||||||
return this.add(path, "POST", handler);
|
return this.add(path, "POST", handler);
|
||||||
}
|
}
|
||||||
return this.add(path, "POST", handler);
|
return this.add(path, "POST", handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
api<
|
public api<
|
||||||
Path extends string,
|
Path extends string,
|
||||||
ReqSchema extends Schema<any>,
|
ReqSchema extends Schema<any>,
|
||||||
ResSchema extends Schema<any>,
|
ResSchema extends Schema<any>,
|
||||||
@ -134,31 +147,23 @@ class HttpRouter {
|
|||||||
req: Request,
|
req: Request,
|
||||||
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
|
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const c = new Context(req, connInfo, {});
|
let ctx = new Context(req, connInfo, {});
|
||||||
|
let routeParams: Record<string, string> = {};
|
||||||
const path = this.pathPreprocessor
|
const path = this.pathPreprocessor
|
||||||
? this.pathPreprocessor(c.path)
|
? this.pathPreprocessor(ctx.path)
|
||||||
: c.path;
|
: ctx.path;
|
||||||
|
|
||||||
let params: Record<string, string> = {};
|
|
||||||
|
|
||||||
const handler = this.routerTree
|
const handler = this.routerTree
|
||||||
.find(path)
|
.find(path)
|
||||||
.andThen((routeMatch) => {
|
.andThen((match) => {
|
||||||
const { value: methodToHandler, params: paramsMatched } =
|
const { value: methodHandler, params: params } = match;
|
||||||
routeMatch;
|
routeParams = params;
|
||||||
|
const route = methodHandler[req.method];
|
||||||
params = paramsMatched;
|
if (!route) return none;
|
||||||
|
if (route.schema) {
|
||||||
const handlerAndSchema = methodToHandler[req.method];
|
ctx = ctx.withSchema(route.schema);
|
||||||
|
|
||||||
if (!handlerAndSchema) {
|
|
||||||
return none;
|
|
||||||
}
|
}
|
||||||
|
const handler = route.handler;
|
||||||
const handler = handlerAndSchema.handler;
|
|
||||||
|
|
||||||
c.schema = handlerAndSchema.schema;
|
|
||||||
|
|
||||||
return some(handler);
|
return some(handler);
|
||||||
})
|
})
|
||||||
@ -167,7 +172,7 @@ class HttpRouter {
|
|||||||
const res = (await this.executeMiddlewareChain(
|
const res = (await this.executeMiddlewareChain(
|
||||||
this.middlewares,
|
this.middlewares,
|
||||||
handler,
|
handler,
|
||||||
Context.setParams(c, params),
|
Context.setParams(ctx, routeParams),
|
||||||
)).res;
|
)).res;
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@ -148,83 +148,31 @@ export class RouterTree<T> {
|
|||||||
|
|
||||||
public add(path: string, handler: T): void {
|
public add(path: string, handler: T): void {
|
||||||
const segments = this.splitPath(path);
|
const segments = this.splitPath(path);
|
||||||
const paramNames: string[] = this.extractParams(segments);
|
const paramNames: string[] = this.extractParamNames(segments);
|
||||||
let current: Node<T> = this.root;
|
const node: Node<T> = this.traverseOrCreate(segments);
|
||||||
|
|
||||||
for (const segment of segments) {
|
node.paramNames = node.isWildcardNode()
|
||||||
current = current
|
? [...paramNames, "restOfThePath"]
|
||||||
.getChild(segment)
|
: paramNames;
|
||||||
.unwrapOrElse(() =>
|
node.handler = some(handler);
|
||||||
current.addChild(
|
|
||||||
segment,
|
|
||||||
this.wildcardSymbol,
|
|
||||||
this.paramPrefix,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (current.isWildcardNode()) {
|
|
||||||
current.paramNames = paramNames;
|
|
||||||
current.paramNames.push("restOfThePath");
|
|
||||||
current.handler = some(handler);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
current.paramNames = paramNames;
|
|
||||||
current.handler = some(handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public find(path: string): Option<RouteMatch<T>> {
|
public find(path: string): Option<RouteMatch<T>> {
|
||||||
const segments = this.splitPath(path);
|
return this.traverse(path).andThen(({ node, paramValues }) => {
|
||||||
const paramValues: string[] = [];
|
|
||||||
let current: Node<T> = this.root;
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
for (; i < segments.length; i++) {
|
|
||||||
const segment = segments[i];
|
|
||||||
if (current.isWildcardNode()) break;
|
|
||||||
|
|
||||||
const nextNode = current.getChild(segment).ifSome((child) => {
|
|
||||||
if (child.isDynamicNode()) {
|
|
||||||
paramValues.push(segment);
|
|
||||||
}
|
|
||||||
current = child;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nextNode.isNone()) return none;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.isWildcardNode()) {
|
|
||||||
const rest = segments.slice(i - 1);
|
|
||||||
if (rest.length > 0) {
|
|
||||||
paramValues.push(rest.join(this.pathSeparator));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: Params = {};
|
const params: Params = {};
|
||||||
|
for (
|
||||||
for (let i = 0; i < paramValues.length; i++) {
|
let i = 0;
|
||||||
params[current.paramNames[i]] = paramValues[i];
|
i < Math.min(paramValues.length, node.paramNames.length);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
params[node.paramNames[i]] = paramValues[i];
|
||||||
}
|
}
|
||||||
|
return node.handler.map((handler) => ({ value: handler, params }));
|
||||||
return current.handler.map((value) => ({ value, params }));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getHandler(path: string): Option<T> {
|
public getHandler(path: string): Option<T> {
|
||||||
const segments = this.splitPath(path);
|
return this.traverse(path).andThen(({ node }) => node.handler);
|
||||||
let current: Node<T> = this.root;
|
|
||||||
|
|
||||||
for (const segment of segments) {
|
|
||||||
if (current.isWildcardNode()) break;
|
|
||||||
|
|
||||||
const child = current.getChild(segment).ifSome((child) => {
|
|
||||||
current = child;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (child.isNone()) return none;
|
|
||||||
}
|
|
||||||
|
|
||||||
return current.handler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private traverseOrCreate(segments: string[]): Node<T> {
|
private traverseOrCreate(segments: string[]): Node<T> {
|
||||||
@ -235,20 +183,49 @@ export class RouterTree<T> {
|
|||||||
node.addChild(segment, this.wildcardSymbol, this.paramPrefix)
|
node.addChild(segment, this.wildcardSymbol, this.paramPrefix)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private traverse(
|
||||||
|
path: string,
|
||||||
|
): Option<{ node: Node<T>; paramValues: string[] }> {
|
||||||
|
const segments = this.splitPath(path);
|
||||||
|
const paramValues: string[] = [];
|
||||||
|
let node: Node<T> = this.root;
|
||||||
|
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
if (node.isWildcardNode()) {
|
||||||
|
const remaining = segments.slice(i).join(this.pathSeparator);
|
||||||
|
if (remaining) paramValues.push(remaining);
|
||||||
|
return some({ node, paramValues });
|
||||||
|
}
|
||||||
|
|
||||||
|
const childOpt = node.getChild(segments[i]);
|
||||||
|
if (childOpt.isNone()) return none;
|
||||||
|
|
||||||
|
node = childOpt.unwrap();
|
||||||
|
if (node.isDynamicNode()) {
|
||||||
|
paramValues.push(segments[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return some({ node, paramValues });
|
||||||
}
|
}
|
||||||
|
|
||||||
private splitPath(path: string): string[] {
|
private splitPath(path: string): string[] {
|
||||||
const trimmed = path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
return path
|
||||||
return trimmed ? trimmed.split(this.pathSeparator) : [];
|
.trim()
|
||||||
|
.split(this.pathSeparator)
|
||||||
|
.filter((segment) => segment.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public extractParams(segments: string[]): string[] {
|
public extractParamNames(segments: string[]): string[] {
|
||||||
return segments.filter((segment) =>
|
return segments.filter((segment) =>
|
||||||
segment.startsWith(this.paramPrefix)
|
segment.startsWith(this.paramPrefix)
|
||||||
).map((segment) => this.stripParamPrefix(segment));
|
).map((segment) => this.removeParamPrefix(segment));
|
||||||
}
|
}
|
||||||
|
|
||||||
public stripParamPrefix(segment: string): string {
|
public removeParamPrefix(segment: string): string {
|
||||||
return segment.slice(this.paramPrefix.length);
|
return segment.slice(this.paramPrefix.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
server/test.db
BIN
server/test.db
Binary file not shown.
Reference in New Issue
Block a user