1.1.2Updated a month ago
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();
  }
}