From 5af4c201137faf834f38c83bc71b8fa1a6e27c00 Mon Sep 17 00:00:00 2001 From: yldrmzffr Date: Sun, 12 Nov 2023 10:11:47 +0300 Subject: [PATCH] feat(major restructuring): implement controller and request handler classes, refactor routing This update involves implementing new controller and request handler classes, along with refactoring the routing system. The new controllers are responsible for managing distinct functionalities of the application, enhancing modularity and clarity in the codebase. Request handlers are designed to process incoming HTTP requests, ensuring they're directed to the appropriate controllers, while handling validation and error management. The refactoring of the routing system aligns with these new structures, improving the efficiency and clarity of URL routing and request processing. Overall, these changes aim to make the application more robust, maintainable, and scalable, facilitating easier updates and expansions in the future. --- lib/controller/controller-factory.ts | 36 ++++++ lib/handlers/request-handler.ts | 72 ++++++++++++ lib/index.ts | 161 +++++---------------------- lib/methods/method-factory.ts | 46 ++++++++ lib/route-manager.ts | 30 ----- lib/routing/route-manager.ts | 32 ++++++ package.json | 2 +- 7 files changed, 217 insertions(+), 162 deletions(-) create mode 100644 lib/controller/controller-factory.ts create mode 100644 lib/handlers/request-handler.ts create mode 100644 lib/methods/method-factory.ts delete mode 100644 lib/route-manager.ts create mode 100644 lib/routing/route-manager.ts diff --git a/lib/controller/controller-factory.ts b/lib/controller/controller-factory.ts new file mode 100644 index 0000000..598d349 --- /dev/null +++ b/lib/controller/controller-factory.ts @@ -0,0 +1,36 @@ +import {RouteManager} from '../routing/route-manager'; +import * as pathLib from 'path'; +import {Route} from '../interfaces'; + +export type ControllerDecorator = (path?: string) => ClassDecorator; + +export class ControllerFactory { + constructor(private routeManager: RouteManager) {} + public Controller: ControllerDecorator = (path = '') => { + return (target: any) => { + Reflect.defineMetadata('path', path, target); + + const properties = Object.getOwnPropertyNames(target.prototype); + + const routers: (Route | undefined)[] = properties.map(property => { + const routeHandler = target.prototype[property]; + const method = Reflect.getMetadata('method', routeHandler); + const url = Reflect.getMetadata('url', routeHandler); + + if (!method || !url) return; + + const fullPath = pathLib.join(path, url); + + return { + method, + url: fullPath, + handler: routeHandler.bind(target.prototype), + } as Route; + }); + + this.routeManager.addRoutes(routers.filter(Boolean) as Route[]); + + return target; + }; + }; +} diff --git a/lib/handlers/request-handler.ts b/lib/handlers/request-handler.ts new file mode 100644 index 0000000..5763acf --- /dev/null +++ b/lib/handlers/request-handler.ts @@ -0,0 +1,72 @@ +import {Request} from '../interfaces'; +import {Response} from '../types'; +import {RouteManager} from '../routing/route-manager'; +import {NotFoundException} from '../exceptions/not-found.exception'; +import {getRequestBody, parseQueryParams} from '../utils'; +import {MuzuException} from '../exceptions/muzu.exception'; +import {BadRequestException} from '../exceptions/bad-request.exception'; +export class RequestHandler { + private routeManager: RouteManager; + + constructor(routeManager: RouteManager) { + this.routeManager = routeManager; + } + + private async sendResponse( + res: Response, + statusCode: number, + body: Object + ): Promise { + res.writeHead(statusCode, {'Content-Type': 'application/json'}); + res.end(JSON.stringify(body)); + console.log('📤 Response', {statusCode, body}); + } + + public async handleRequest(req: Request, res: Response): Promise { + try { + const {url, method} = req; + const path = url!.split('?')[0]; + + req.params = parseQueryParams(url!); + + const route = await this.routeManager.find(path, method); + + if (!route) { + throw new NotFoundException(`Route ${method} ${path} not found`, { + method, + path, + }); + } + + try { + const body = await getRequestBody(req); + req.body = body; + } catch (error) { + console.log('🚨 Error parsing body', error); + const err = error as MuzuException; + throw new BadRequestException('Error parsing body', err.details); + } + + let result: Object = route.handler(req, res); + + if (result instanceof Promise) { + result = await result; + } + + const statusCode = res.statusCode || 200; + return this.sendResponse(res, statusCode, result); + } catch (error) { + console.log('🚨 Error handling request', error); + const knownError = error as MuzuException; + + if (knownError.kind === 'MuzuException') { + return this.sendResponse(res, knownError.status, knownError); + } + + return this.sendResponse(res, 500, { + message: '500 Internal Server Error', + stack: knownError.stack, + }); + } + } +} diff --git a/lib/index.ts b/lib/index.ts index fac48ad..86ab1ba 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,26 +1,47 @@ import 'reflect-metadata'; -import * as pathLib from 'path'; import {createServer, Server} from 'http'; import {HttpException} from './exceptions/http.exception'; -import {Request, Route} from './interfaces'; -import {RequestMethod, Response, RouteHandler} from './types'; -import {getRequestBody, parseQueryParams} from './utils'; -import {MuzuException} from './exceptions/muzu.exception'; -import {NotFoundException} from './exceptions/not-found.exception'; -import {BadRequestException} from './exceptions/bad-request.exception'; -import RouteManager from './route-manager'; +import {Request} from './interfaces'; +import {Response, RouteHandler} from './types'; +import {RouteManager} from './routing/route-manager'; +import {RequestHandler} from './handlers/request-handler'; +import { + ControllerDecorator, + ControllerFactory, +} from './controller/controller-factory'; +import {MethodDecorator, MethodFactory} from './methods/method-factory'; export {Request, Response, RouteHandler, HttpException}; export class MuzuServer { public readonly server: Server; public readonly routeManager: RouteManager; + public readonly requestHandler: RequestHandler; + public readonly Controller: ControllerDecorator; + public readonly Get: MethodDecorator; + public readonly Post: MethodDecorator; + public readonly Delete: MethodDecorator; + public readonly Put: MethodDecorator; + public readonly Patch: MethodDecorator; constructor() { this.routeManager = new RouteManager(); - this.server = createServer(this.handleRequest.bind(this)); + const methods = new MethodFactory(); + + this.Controller = new ControllerFactory(this.routeManager).Controller; + this.requestHandler = new RequestHandler(this.routeManager); + + this.server = createServer( + this.requestHandler.handleRequest.bind(this.requestHandler) + ); + + this.Get = methods.Get; + this.Post = methods.Post; + this.Delete = methods.Delete; + this.Put = methods.Put; + this.Patch = methods.Patch; } public listen(port: number, callback?: () => void): void { @@ -32,126 +53,4 @@ export class MuzuServer { public stop(callback?: () => void): void { this.server.close(callback); } - - private async sendResponse( - res: Response, - statusCode: number, - body: Object - ): Promise { - res.writeHead(statusCode, {'Content-Type': 'application/json'}); - res.end(JSON.stringify(body)); - console.log('📤 Response', {statusCode, body}); - } - - private async handleRequest(req: Request, res: Response): Promise { - try { - const {url, method} = req; - const path = url!.split('?')[0]; - - req.params = parseQueryParams(url!); - - const route = await this.routeManager.find(path, method); - - if (!route) { - throw new NotFoundException(`Route ${method} ${path} not found`, { - method, - path, - }); - } - - try { - const body = await getRequestBody(req); - req.body = body; - } catch (error) { - console.log('🚨 Error parsing body', error); - const err = error as MuzuException; - throw new BadRequestException('Error parsing body', err.details); - } - - let result: Object = route.handler(req, res); - - if (result instanceof Promise) { - result = await result; - } - - const statusCode = res.statusCode || 200; - return this.sendResponse(res, statusCode, result); - } catch (error) { - console.log('🚨 Error handling request', error); - const knownError = error as MuzuException; - - if (knownError.kind === 'MuzuException') { - return this.sendResponse(res, knownError.status, knownError); - } - - return this.sendResponse(res, 500, { - message: '500 Internal Server Error', - stack: knownError.stack, - }); - } - } - - public HttpMethod(method: RequestMethod, url: string) { - return ( - target: any, - propertyKey: string, - descriptor: PropertyDescriptor - ) => { - Reflect.defineMetadata( - 'url', - `${url}`.toLowerCase(), - target[propertyKey] - ); - Reflect.defineMetadata('method', method, target[propertyKey]); - return descriptor; - }; - } - - Controller = (path = '') => { - return (target: any) => { - Reflect.defineMetadata('path', path, target); - - const properties = Object.getOwnPropertyNames(target.prototype); - - const routers: (Route | undefined)[] = properties.map(property => { - const routeHandler = target.prototype[property]; - const method = Reflect.getMetadata('method', routeHandler); - const url = Reflect.getMetadata('url', routeHandler); - - if (!method || !url) return; - - const fullPath = pathLib.join(path, url); - - return { - method, - url: fullPath, - handler: routeHandler.bind(target.prototype), - } as Route; - }); - - this.routeManager.addRoutes(routers.filter(Boolean) as Route[]); - - return target; - }; - }; - - Get = (url = '/') => { - return this.HttpMethod(RequestMethod.GET, url); - }; - - Post = (url = '/') => { - return this.HttpMethod(RequestMethod.POST, url); - }; - - Delete = (url = '/') => { - return this.HttpMethod(RequestMethod.DELETE, url); - }; - - Put = (url = '/') => { - return this.HttpMethod(RequestMethod.PUT, url); - }; - - Patch = (url = '/') => { - return this.HttpMethod(RequestMethod.PATCH, url); - }; } diff --git a/lib/methods/method-factory.ts b/lib/methods/method-factory.ts new file mode 100644 index 0000000..441d889 --- /dev/null +++ b/lib/methods/method-factory.ts @@ -0,0 +1,46 @@ +import {RequestMethod} from '../types'; + +type HttpMethodDecorator = ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor +) => PropertyDescriptor; + +export type MethodDecorator = (url?: string) => HttpMethodDecorator; + +export class MethodFactory { + public HttpMethod(method: RequestMethod, url: string): HttpMethodDecorator { + return ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor + ) => { + Reflect.defineMetadata( + 'url', + `${url}`.toLowerCase(), + target[propertyKey] + ); + Reflect.defineMetadata('method', method, target[propertyKey]); + return descriptor; + }; + } + Get = (url = '/'): HttpMethodDecorator => { + return this.HttpMethod(RequestMethod.GET, url); + }; + + Post = (url = '/'): HttpMethodDecorator => { + return this.HttpMethod(RequestMethod.POST, url); + }; + + Delete = (url = '/'): HttpMethodDecorator => { + return this.HttpMethod(RequestMethod.DELETE, url); + }; + + Put = (url = '/'): HttpMethodDecorator => { + return this.HttpMethod(RequestMethod.PUT, url); + }; + + Patch = (url = '/'): HttpMethodDecorator => { + return this.HttpMethod(RequestMethod.PATCH, url); + }; +} diff --git a/lib/route-manager.ts b/lib/route-manager.ts deleted file mode 100644 index f2c24a9..0000000 --- a/lib/route-manager.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {Route} from './interfaces'; -import {removeSlash} from './utils'; - -export default class RouteManager { - public routes: Route[]; - constructor() { - this.routes = []; - } - - public addRoute(route: Route): void { - this.routes.push(route); - } - - public addRoutes(routes: Route[]): void { - this.routes.push(...routes); - } - - public async getRoutes(): Promise { - return this.routes; - } - - public async find( - url: string, - method: string | undefined - ): Promise { - return this.routes.find( - req => removeSlash(req.url) === removeSlash(url) && req.method === method - ); - } -} diff --git a/lib/routing/route-manager.ts b/lib/routing/route-manager.ts new file mode 100644 index 0000000..c9050fc --- /dev/null +++ b/lib/routing/route-manager.ts @@ -0,0 +1,32 @@ +import {Route} from '../interfaces'; +import {removeSlash} from '../utils'; + +export class RouteManager { + private routeMap: Map; + + constructor() { + this.routeMap = new Map(); + } + + private getRouteKey(url: string, method: string | undefined): string { + return `${removeSlash(url)}|${method}`; + } + + public addRoute(route: Route): void { + const key = this.getRouteKey(route.url, route.method); + this.routeMap.set(key, route); + } + + public addRoutes(routes: Route[]): void { + routes.forEach(route => this.addRoute(route)); + } + + public getRoutes(): Route[] { + return Array.from(this.routeMap.values()); + } + + public find(url: string, method: string | undefined): Route | undefined { + const key = this.getRouteKey(url, method); + return this.routeMap.get(key); + } +} diff --git a/package.json b/package.json index 51a7571..1ddb1b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "muzu", - "version": "0.0.6", + "version": "0.1.0", "description": "", "main": "build/lib/index.js", "types": "build/lib/index.d.ts",