import type { InfinityContext } from "@infinity-beyond/infinity.ts";
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type PathParams<Path extends string, Params = {[K in ExtractParams<Path>]: K extends string ? string : never}> = Params
type WithParams = <T extends string, K extends Record<string, string> = PathParams<T>>(path: T, handler: RouteHandler<K>) => void
export class REST_Wrapper {
readonly routes: Map<RegExp, Route> = new Map<RegExp, Route>()
protected get = this.add_handler.bind(this, 'GET') as WithParams;
protected post = this.add_handler.bind(this, 'POST') as WithParams;
protected put = this.add_handler.bind(this, 'PUT') as WithParams;
protected patch = this.add_handler.bind(this, 'PATCH') as WithParams;
protected delete = this.add_handler.bind(this, 'DELETE') as WithParams;
protected head = this.add_handler.bind(this, 'HEAD') as WithParams;
protected options = this.add_handler.bind(this, 'OPTIONS') as WithParams;
protected any = this.add_handler.bind(this, 'ANY') as WithParams;
private add_handler(method: REST_METHOD, path: string, handler: RouteHandler) {
const param_variables: string[] = [];
const regex = new RegExp('^' + path.replace(/\:(\w+)/g, (_, b) => {
param_variables.push(b);
return `(\\w+)`;
}) + '$');
this.routes.set(regex, {
method,
path,
param_variables,
handler,
});
}
async handle(pathname: string, request: Request, ctx: InfinityContext) {
try {
const no_query_pathname = pathname.replace(/\?.+$/, '') || '/';
const valid_routes = this.routes.entries().toArray().filter(([ regex, route ]) => (
route.method == 'ANY' || route.method == request.method
) && regex.test(no_query_pathname));
for(const [regex, route] of valid_routes) {
const [_, ...params] = no_query_pathname.match(regex) || [];
ctx.params = Object.assign({},
...route.param_variables.map((v, i) => ({[v]: params[i]}))
)
const response= await route.handler(request, ctx, () => '__NEXT__');
if(response instanceof Response) return response;
if(response == '__NEXT__') continue;
throw new Deno.errors.UnexpectedEof();
}
throw new Deno.errors.NotFound();
} catch(e: any) {
console.warn(`[${request.url.replace(/\?.+$/, '')}] ${e.message || 'An unlabeled error ocurred'}`);
if(e instanceof Deno.errors.InvalidData) {
return new Response(undefined, {
status: 400
});
}
if(e instanceof Deno.errors.PermissionDenied) {
return new Response(undefined, {
status: 403
});
}
if(e instanceof Deno.errors.NotFound) {
return new Response(undefined, {
status: 404
});
}
return new Response(undefined, { status: 500 });
}
}
}
type Next = '__NEXT__';
type RouteHandler<T extends Record<string, any> = Record<string, any>> = (request: Request, ctx: InfinityContext<T>, next: () => Next) => Promise<Response | Next> | Response | Next;
type REST_METHOD = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'ANY'
interface Route {
method: REST_METHOD
path: string
param_variables: string[]
handler: RouteHandler<any>
}