/** * Controller registry - stores all registered controllers */ import type { IControllerMetadata, IRegisteredController, ICompiledRoute } from './decorators.types.js'; import type { IRequestContext, IInterceptOptions, THttpMethod } from '../core/smartserve.interfaces.js'; import { getControllerMetadata, combinePaths } from './decorators.metadata.js'; /** * Global registry of all controllers */ export class ControllerRegistry { private static controllers: Map = new Map(); private static instances: Map = new Map(); private static compiledRoutes: ICompiledRoute[] = []; private static routesCompiled = false; /** * Register a controller class */ static registerClass(target: Function): void { const metadata = getControllerMetadata(target); metadata.target = target as new (...args: any[]) => any; this.controllers.set(target, metadata); this.routesCompiled = false; } /** * Register a controller instance */ static registerInstance(instance: any): void { const constructor = instance.constructor; const metadata = getControllerMetadata(constructor); // Store instance this.instances.set(constructor, instance); // Register class if not already registered if (!this.controllers.has(constructor)) { this.registerClass(constructor); } this.routesCompiled = false; } /** * Get all registered controllers */ static getControllers(): IRegisteredController[] { const result: IRegisteredController[] = []; for (const [constructor, metadata] of this.controllers) { // Get or create instance let instance = this.instances.get(constructor); if (!instance && metadata.target) { instance = new metadata.target(); this.instances.set(constructor, instance); } if (instance) { result.push({ instance, metadata }); } } return result; } /** * Compile all routes for fast matching */ static compileRoutes(): ICompiledRoute[] { if (this.routesCompiled) { return this.compiledRoutes; } this.compiledRoutes = []; for (const { instance, metadata } of this.getControllers()) { for (const [methodName, route] of metadata.routes) { const fullPath = combinePaths(metadata.basePath, route.path); const { regex, paramNames } = this.pathToRegex(fullPath); // Combine class and method interceptors const interceptors: IInterceptOptions[] = [ ...metadata.classInterceptors, ...route.interceptors, ]; // Create bound handler const handler = async (ctx: IRequestContext): Promise => { const method = instance[methodName]; if (typeof method !== 'function') { throw new Error(`Method ${String(methodName)} not found on controller`); } return method.call(instance, ctx); }; this.compiledRoutes.push({ pattern: fullPath, regex, paramNames, method: route.method, handler, interceptors, }); } } // Sort routes by specificity (more specific paths first) this.compiledRoutes.sort((a, b) => { // Routes without wildcards come first const aHasWildcard = a.pattern.includes('*'); const bHasWildcard = b.pattern.includes('*'); if (aHasWildcard !== bHasWildcard) return aHasWildcard ? 1 : -1; // Routes with more segments come first const aSegments = a.pattern.split('/').length; const bSegments = b.pattern.split('/').length; if (aSegments !== bSegments) return bSegments - aSegments; // Routes without params come first const aParams = a.paramNames.length; const bParams = b.paramNames.length; return aParams - bParams; }); this.routesCompiled = true; return this.compiledRoutes; } /** * Match a request to a compiled route */ static matchRoute(path: string, method: THttpMethod): { route: ICompiledRoute; params: Record; } | null { const routes = this.compileRoutes(); for (const route of routes) { // Check method match if (route.method !== 'ALL' && route.method !== method) { continue; } // Check path match const match = route.regex.exec(path); if (match) { // Extract params const params: Record = {}; route.paramNames.forEach((name, index) => { params[name] = match[index + 1]; }); return { route, params }; } } return null; } /** * Convert path pattern to regex * Supports :param and * wildcard */ private static pathToRegex(path: string): { regex: RegExp; paramNames: string[] } { const paramNames: string[] = []; let regexStr = path // Escape special regex chars (except : and *) .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Convert :param to capture group .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => { paramNames.push(name); return '([^/]+)'; }) // Convert * to wildcard .replace(/\*/g, '(.*)'); // Anchor the regex regexStr = `^${regexStr}$`; return { regex: new RegExp(regexStr), paramNames, }; } /** * Clear all registered controllers (useful for testing) */ static clear(): void { this.controllers.clear(); this.instances.clear(); this.compiledRoutes = []; this.routesCompiled = false; } }