Skip to content

Commit

Permalink
Merge pull request #6 from yldrmzffr/feature/new-routing-system
Browse files Browse the repository at this point in the history
feat(major restructuring): implement controller and request handler c…
  • Loading branch information
yldrmzffr authored Nov 12, 2023
2 parents 63db545 + 5af4c20 commit 3c05cc4
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 162 deletions.
36 changes: 36 additions & 0 deletions lib/controller/controller-factory.ts
Original file line number Diff line number Diff line change
@@ -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) => {

Check warning on line 10 in lib/controller/controller-factory.ts

View workflow job for this annotation

GitHub Actions / publish-npm

Unexpected any. Specify a different type
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;
};
};
}
72 changes: 72 additions & 0 deletions lib/handlers/request-handler.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
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,
});
}
}
}
161 changes: 30 additions & 131 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<void> {
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<void> {
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);
};
}
46 changes: 46 additions & 0 deletions lib/methods/method-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {RequestMethod} from '../types';

type HttpMethodDecorator = (
target: any,

Check warning on line 4 in lib/methods/method-factory.ts

View workflow job for this annotation

GitHub Actions / publish-npm

Unexpected any. Specify a different type
propertyKey: string,
descriptor: PropertyDescriptor
) => PropertyDescriptor;

export type MethodDecorator = (url?: string) => HttpMethodDecorator;

export class MethodFactory {
public HttpMethod(method: RequestMethod, url: string): HttpMethodDecorator {
return (
target: any,

Check warning on line 14 in lib/methods/method-factory.ts

View workflow job for this annotation

GitHub Actions / publish-npm

Unexpected any. Specify a different type
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);
};
}
30 changes: 0 additions & 30 deletions lib/route-manager.ts

This file was deleted.

Loading

0 comments on commit 3c05cc4

Please sign in to comment.