import type { VP_Request } from "../modules/vp_request.ts";
type ObjectResponse = Record<string, any>
export type ValidResponseType = Response | number | ObjectResponse | void
type HTTP_METHOD = 'ANY' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'| 'HEAD' | 'OPTIONS'
type Next = () => void
type RouterHandler = (request: VP_Request, next: Next) => ValidResponseType | Promise<ValidResponseType>
interface Route {
method: HTTP_METHOD
path: string
handler: RouterHandler
full_path: string
params: string[]
regex: RegExp
}
interface RouteRouter {
path: string
router: Router
full_path: string
}
export class Router {
routes: Route[] = []
routers: RouteRouter[] = []
parent: Router | undefined
nested_routers(path: string = ''): RouteRouter[] {
return this.routers.map(route_router => {
const r: RouteRouter = {
path: route_router.path,
router: route_router.router.clone(),
full_path: `${path}/${route_router.path}`.replace(/\/{2,}/g, '/')
}
for(const route of r.router.routes) {
if(route.path == '*') route.path = '.*';
route.full_path = `${path}/${route_router.path}/${route.path}`.replace(/\/{2,}/g, '/');
}
return [r, r.router.nested_routers(r.full_path)]
}).flat(5) as RouteRouter[]
}
clone() {
const r = new Router();
r.routes = [...this.routes.map(route => { return { ...route } })];
r.routers = [...this.routers.map(r => {
return {
path: r.path,
router: r.router.clone(),
full_path: r.full_path,
} satisfies RouteRouter
})];
return r;
}
use(path: string, router: Router) {
this.routers.push({ path, router, full_path: '' });
}
on(method: HTTP_METHOD, path: string, handler: RouterHandler) {
this.routes.push({
method,
path,
handler,
} as Route);
return this;
}
any(path: string, handler: RouterHandler) {
this.routes.push({
method: 'ANY',
path,
handler,
} as Route);
return this;
}
get(path: string, handler: RouterHandler) {
this.routes.push({
method: 'GET',
path,
handler,
} as Route);
return this;
}
post(path: string, handler: RouterHandler) {
this.routes.push({
method: 'POST',
path,
handler,
} as Route);
return this;
}
put(path: string, handler: RouterHandler) {
this.routes.push({
method: 'PUT',
path,
handler,
} as Route);
return this;
}
patch(path: string, handler: RouterHandler) {
this.routes.push({
method: 'PATCH',
path,
handler,
} as Route);
return this;
}
delete(path: string, handler: RouterHandler) {
this.routes.push({
method: 'DELETE',
path,
handler,
} as Route);
return this;
}
head(path: string, handler: RouterHandler) {
this.routes.push({
method: 'HEAD',
path,
handler,
} as Route);
return this;
}
options(path: string, handler: RouterHandler) {
this.routes.push({
method: 'OPTIONS',
path,
handler,
} as Route);
return this;
}
async handle(request: VP_Request) {
return await new RouterClimber(this, request).process();
}
}
class RouterClimber {
router: Router
request: VP_Request
routes: Route[] = []
routers: RouteRouter[] = []
current_route: string
constructor(router: Router, request: VP_Request) {
this.router = router;
this.request = request;
this.current_route = ('/' + this.request.path_parts.splice(1).join('/')).replace(/\/{2,}/g, '').replace(/\/$/, '');
}
private async next() {
const route = this.routes.shift();
if(!route) return null;
this.request.params = {}
const match_results = this.current_route.match(route.regex) ?? [];
for(const [i, param] of route.params.entries()) {
this.request.params[param] = match_results[i + 1];
}
return await route.handler(this.request, this.next.bind(this));
}
async process() {
this.routers = [{path: '/', router: this.router, full_path: '/'}, ...this.router.nested_routers()].filter(r => r.router);
this.routes = this.routers.map(r => r.router.routes).flat();
this.routes = this.routes.filter(r => r.method == this.request.method || r.method == 'ANY');
this.routes = this.routes.filter(r => {
r.params = [];
const route_match_string = '^' + r.full_path.replace(/\/\:\w+/g, (param) => {
r.params!.push(param.replace('/:', ''));
return '/([^\/]+)';
}).replace(/\/+$/, '').replace(/\/\.\*$/, '.*') + '$';
r.regex = new RegExp(route_match_string);
return r.regex.exec(this.current_route);
});
return await this.next();
}
}