/** * Metadata storage for decorators using Symbol.metadata (TC39 Stage 3) * Falls back to WeakMap for environments without Symbol.metadata */ import type { IControllerMetadata, IRouteMetadata } from './decorators.types.js'; import type { IInterceptOptions } from '../core/smartserve.interfaces.js'; // Symbol for storing metadata when Symbol.metadata is not available const CONTROLLER_METADATA = Symbol('smartserve:controller'); /** * Get or create controller metadata for a class * Uses symbol property on the class itself for metadata storage */ export function getControllerMetadata(target: any): IControllerMetadata { // Store metadata on the class itself using symbol if (!target[CONTROLLER_METADATA]) { target[CONTROLLER_METADATA] = createEmptyMetadata(); } return target[CONTROLLER_METADATA]; } /** * Get controller metadata from prototype (for instance lookup) */ export function getMetadataFromInstance(instance: any): IControllerMetadata | undefined { const constructor = instance.constructor; return getControllerMetadata(constructor); } /** * Set base path for a controller */ export function setBasePath(target: any, path: string): void { const metadata = getControllerMetadata(target); metadata.basePath = normalizePath(path); metadata.target = target; } /** * Add a route to a controller */ export function addRoute( target: any, methodName: string | symbol, route: Omit ): void { const metadata = getControllerMetadata(target.constructor); // Get existing route or create new one let existingRoute = metadata.routes.get(methodName); if (!existingRoute) { existingRoute = { ...route, methodName, interceptors: [], }; metadata.routes.set(methodName, existingRoute); } else { // Update existing route existingRoute.method = route.method; existingRoute.path = route.path; existingRoute.options = { ...existingRoute.options, ...route.options }; } } /** * Add class-level interceptor */ export function addClassInterceptor(target: any, interceptor: IInterceptOptions): void { const metadata = getControllerMetadata(target); metadata.classInterceptors.push(interceptor); } /** * Add method-level interceptor */ export function addMethodInterceptor( target: any, methodName: string | symbol, interceptor: IInterceptOptions ): void { const metadata = getControllerMetadata(target.constructor); let route = metadata.routes.get(methodName); if (!route) { // Create placeholder route (will be completed by @Get/@Post/etc.) route = { method: 'GET', path: '', methodName, interceptors: [], options: {}, }; metadata.routes.set(methodName, route); } route.interceptors.push(interceptor); } /** * Create empty metadata object */ function createEmptyMetadata(): IControllerMetadata { return { basePath: '', classInterceptors: [], routes: new Map(), }; } /** * Normalize path to ensure consistent format */ export function normalizePath(path: string): string { if (!path) return ''; // Ensure leading slash let normalized = path.startsWith('/') ? path : `/${path}`; // Remove trailing slash (unless it's just '/') if (normalized.length > 1 && normalized.endsWith('/')) { normalized = normalized.slice(0, -1); } return normalized; } /** * Combine base path and route path */ export function combinePaths(basePath: string, routePath: string): string { const base = normalizePath(basePath); const route = normalizePath(routePath); if (!base) return route || '/'; if (!route) return base; return `${base}${route}`; }