diff --git a/lib/core/__test__/context.spec.ts b/lib/core/__test__/context.spec.ts new file mode 100644 index 0000000..52cc77c --- /dev/null +++ b/lib/core/__test__/context.spec.ts @@ -0,0 +1,57 @@ +import sinon from 'sinon'; +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { WebContext } from '../context'; +import { IncomingMessage } from 'http'; +import { ServerResponse } from 'http'; +import { RQ, RS } from '../../types'; +describe('CONTEXT', () => { + let webContext: WebContext; + let mockRequest: sinon.SinonStubbedInstance; + let mockResponse: sinon.SinonStubbedInstance; + beforeEach(() => { + mockRequest = sinon.createStubInstance(IncomingMessage); + mockResponse = sinon.createStubInstance(ServerResponse); + webContext = new WebContext(mockRequest, mockResponse); + }); + + afterEach(() => { + sinon.restore(); + }); + it('should initialize the context with default values', () => { + expect(webContext.body).to.be.null; + expect(webContext.params).to.deep.equal({}); + expect(webContext.query).to.deep.equal({}); + }); + it('should return response property when accessed via proxy', () => { + // Mocking a response property (e.g., "statusCode") + mockResponse.statusCode = 200; + + const statusCode = webContext.ctx.statusCode; + + expect(statusCode).to.equal(200); + }); + + it('should set request or response properties correctly', () => { + // Setting a response property + webContext.ctx.statusCode = 404; + expect(mockResponse.statusCode).to.equal(404); + + // Setting a request property + webContext.ctx.method = 'POST'; + expect(webContext.ctx.method).to.equal('POST'); + }); + it('should return true if property exists in request or response', () => { + mockRequest.method = 'GET'; + expect('method' in webContext.ctx).to.be.true; + }); + + it('should return false if property does not exist in request or response', () => { + expect('nonexistent' in webContext.ctx).to.be.false; + }); + + it('should delete properties from request or response', () => { + expect(delete webContext.ctx.method).to.be.true; + expect(webContext.ctx.method).to.be.undefined; + }); +}); diff --git a/lib/core/__test__/decorators.spec.ts b/lib/core/__test__/decorators.spec.ts new file mode 100644 index 0000000..eba0270 --- /dev/null +++ b/lib/core/__test__/decorators.spec.ts @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { mid, mids, Route, getEx } from '../decorators'; +import { ServerUtils } from '../../helper'; +import Reflect from '../../metadata'; +import { routes } from '../router'; + +describe('Decorators', () => { + let reflectInitStub: sinon.SinonStub; + let serverUtilsNormalizeStub: sinon.SinonStub; + + beforeEach(() => { + reflectInitStub = sinon.stub(Reflect, 'init'); + serverUtilsNormalizeStub = sinon.stub(ServerUtils, 'normalize').callsFake((middleware: any) => (Array.isArray(middleware) ? middleware : [middleware])); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('mid decorator', () => { + it('should add middleware to the method using Reflect', () => { + const middleware = sinon.stub(); + const target = {}; + const propertyKey = 'someMethod'; + const descriptor = {}; + const mockGet = sinon.stub(Reflect, 'get').returns([]); + + mid(middleware)(target, propertyKey, descriptor as PropertyDescriptor); + + expect(mockGet.calledOnceWith('middlewares', target.constructor.prototype, propertyKey)).to.be.true; + expect(serverUtilsNormalizeStub.calledOnceWith(middleware)).to.be.true; + expect(reflectInitStub.calledOnceWith('middlewares', [middleware], target.constructor.prototype, propertyKey)).to.be.true; + + mockGet.restore(); + }); + }); + describe('mids decorator', () => { + it('should add class-level middleware using Reflect', () => { + const middlewareArray = [sinon.stub(), sinon.stub()]; + class SomeClass {} + mids(middlewareArray)(SomeClass); + + expect(serverUtilsNormalizeStub.calledOnceWith(middlewareArray)).to.be.true; + expect(reflectInitStub.calledOnceWith('classMiddlewares', middlewareArray, SomeClass.prototype)).to.be.true; + }); + }); + + describe('Route decorator', () => { + it('should register the route and path in Reflect and routes', () => { + const path = '/test-path'; + class SomeClass {} + Route(path)(SomeClass); + + expect(reflectInitStub.calledOnceWith('route', path, SomeClass.prototype)).to.be.true; + expect(routes.get(path)).to.equal(SomeClass); + }); + }); +}); diff --git a/lib/core/__test__/middleware.spec.ts b/lib/core/__test__/middleware.spec.ts new file mode 100644 index 0000000..97fc479 --- /dev/null +++ b/lib/core/__test__/middleware.spec.ts @@ -0,0 +1,78 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Gmids, midManager } from '../middleware'; +import { ServerUtils } from '../../helper'; +import { Gland } from '../../types/index'; + +describe('Gmids and midManager', () => { + let serverUtilsNormalizeStub: sinon.SinonStub; + + beforeEach(() => { + serverUtilsNormalizeStub = sinon.stub(ServerUtils, 'normalize').callsFake((middleware: any) => (Array.isArray(middleware) ? middleware : [middleware])); + (Gmids as any).mids = []; + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Gmids', () => { + describe('set', () => { + it('should add middleware to the mids array', () => { + const middleware = sinon.stub(); + Gmids.set(middleware); + + expect(serverUtilsNormalizeStub.calledOnceWith(middleware)).to.be.true; + expect(Gmids.get()).to.include(middleware); + }); + + it('should handle multiple middlewares', () => { + const middlewareArray = [sinon.stub(), sinon.stub()]; + Gmids.set(middlewareArray); + expect(serverUtilsNormalizeStub.calledOnceWith(middlewareArray)).to.be.true; + expect(Gmids.get()).to.deep.equal(middlewareArray); + }); + }); + + describe('get', () => { + it('should return an empty array when no middlewares are set', () => { + sinon.stub(Gmids, 'get').returns([]); + expect(Gmids.get()).to.deep.equal([]); + }); + + it('should return the current middlewares', () => { + const middleware = sinon.stub(); + Gmids.set(middleware); + expect(Gmids.get()).to.include(middleware); + }); + }); + }); + + describe('midManager', () => { + let middlewares: Gland.Middleware[]; + + beforeEach(() => { + middlewares = []; + }); + + describe('process', () => { + it('should throw an error for invalid middleware/handler signature', () => { + const path = '/test'; + const invalidHandler: any = sinon.stub().callsFake(() => {}); + + expect(() => midManager.process(path, [invalidHandler], middlewares)).to.throw('Invalid middleware/handler function signature'); + }); + + it('should handle non-string path and add unique middlewares', () => { + const handler1: Gland.GlandMiddleware = sinon.stub(); + const handler2: Gland.GlandMiddleware = sinon.stub(); + + midManager.process(handler1, [handler1, handler2], middlewares); + + expect(middlewares).to.have.lengthOf(2); + expect(middlewares).to.include(handler1); + expect(middlewares).to.include(handler2); + }); + }); + }); +}); diff --git a/lib/core/context.ts b/lib/core/context.ts new file mode 100644 index 0000000..0065cc2 --- /dev/null +++ b/lib/core/context.ts @@ -0,0 +1,58 @@ +import { IncomingMessage, ServerResponse } from 'http'; +import { Context, RQ, RS } from '../types'; +import './router/request'; +import { Queue } from '../helper/queue'; +export class WebContext { + public rq: RQ; + public rs: RS; + public ctx: Context; + public body: any; + public params: any; + public query: any; + constructor(request: IncomingMessage, response: ServerResponse) { + this.rq = request; + this.rs = response; + this.body = null; + this.params = {}; + this.query = {}; + this.ctx = new Proxy(this, this.opts()) as WebContext & Context; + } + private opts() { + return { + get: (t: WebContext, p: string | symbol) => { + // Explicitly handle `on` method + if (p === 'on' || p === 'once') { + // Check if this method is being used in the request or response context + if (typeof t.rq[p as keyof RQ] === 'function') { + return t.rq[p as keyof RQ].bind(t.rq); + } + if (typeof t.rs[p as keyof RS] === 'function') { + return t.rs[p as keyof RS].bind(t.rs); + } + } + if (p in t) return t[p as keyof WebContext]; + if (p in t.rs) return typeof t.rs[p as keyof RS] === 'function' ? t.rs[p as keyof RS].bind(t.rs) : t.rs[p as keyof RS]; + if (p in t.rq) return t.rq[p as keyof RQ]; + }, + set: (t: WebContext, p: string | symbol, v: any) => { + if (p in t.rs) { + t.rs[p as keyof RS] = v; + } else if (p in t.rq) { + t.rq[p as keyof RQ] = v; + } else { + t[p as keyof WebContext] = v; + } + return true; + }, + has: (t: WebContext, p: string | symbol) => { + return p in t.rs || p in t.rq; + }, + deleteperty: (t: any, p: string | symbol) => { + if (p in (t || t.rq)) { + return delete (t || t.rq)[p as keyof WebContext]; + } + return false; + }, + }; + } +} diff --git a/lib/core/decorators.ts b/lib/core/decorators.ts new file mode 100644 index 0000000..ad66894 --- /dev/null +++ b/lib/core/decorators.ts @@ -0,0 +1,30 @@ +import { ServerUtils } from '../helper'; +import Reflect from '../metadata'; +import { MidsFn, RouteHandler } from '../types'; +import { routes } from './router'; +const classes: Set = new Set(); +function getEx(): any[] { + return Array.from(classes); +} +export { getEx }; +export function mid(middleware: MidsFn | MidsFn[]): MethodDecorator | any { + return (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor): void => { + const existingMids = Reflect.get('middlewares', target.constructor.prototype, propertyKey) || []; + const newMids = ServerUtils.normalize(middleware); + Reflect.init('middlewares', [...existingMids, ...newMids], target.constructor.prototype, propertyKey); + }; +} + +export function mids(middlewareArray: MidsFn[] | MidsFn): ClassDecorator { + return (target: any) => { + const newMids = ServerUtils.normalize(middlewareArray); + Reflect.init('classMiddlewares', newMids, target.prototype); + }; +} + +export function Route(path: string): ClassDecorator { + return (target: Function): void => { + Reflect.init('route', path, target.prototype); + routes.set(path, target as RouteHandler); + }; +} diff --git a/lib/core/middleware.ts b/lib/core/middleware.ts new file mode 100644 index 0000000..9219241 --- /dev/null +++ b/lib/core/middleware.ts @@ -0,0 +1,53 @@ +import { ServerUtils } from '../helper'; +import { Context, MidsFn, NxtFunction } from '../types'; +import { Gland } from '../types'; +import { Router } from './router'; +export namespace Gmids { + export let mids: MidsFn[] = []; + + export function set(middleware: MidsFn | MidsFn[]) { + mids = [...mids, ...ServerUtils.normalize(middleware)]; + } + + export function get(): MidsFn[] | [] { + return mids || []; + } +} +export namespace midManager { + export function process(path: string | Gland.Middleware, handlers: (Gland.Middleware | Gland.Middleware[])[], middlewares: Gland.Middleware[]) { + if (typeof path === 'string') { + handlers.flat().forEach((handler) => { + Router.set(handler as any, path); + if (handler.length === 2 || handler.length === 3) { + middlewares.push(async (ctx: Context, next: NxtFunction) => { + if (ctx.url!.startsWith(path)) { + if (handler.length === 2) { + await (handler as Gland.GlandMiddleware)(ctx, next); + } else if (handler.length === 3) { + await new Promise((resolve, reject) => { + (handler as Gland.ExpressMiddleware)(ctx.rq, ctx.rs, (err?: any) => { + if (err) reject(err); + else resolve(); + }); + }); + } else { + throw new Error('Invalid middleware/handler function signature'); + } + } else { + await next(); + } + }); + } else if (handler.length === 1) { + middlewares.push(handler as Gland.GlandMiddleware); + } else { + throw new Error('Invalid middleware/handler function signature'); + } + }); + } else { + // Handle when path is not a string + const allMiddlewares = [path, ...handlers].flat(); + const uniqueMiddlewares = Array.from(new Set(allMiddlewares)); + middlewares.push(...uniqueMiddlewares); + } + } +} diff --git a/lib/types/index.ts b/lib/types/index.ts index 2ec5d09..985d33f 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -63,3 +63,10 @@ export type ListenArgs = | [path: string, backlog?: number, listeningListener?: () => void] | [path: string, listeningListener?: () => void] | [options: Gland.ListenOptions, listeningListener?: () => void]; +export interface ModuleConfig { + path: string; + recursive?: boolean; + pattern?: string; + cacheModules?: boolean; + logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug'; +}