Files
smartserve/ts/decorators/decorators.metadata.ts

142 lines
3.6 KiB
TypeScript
Raw Normal View History

2025-11-29 15:24:00 +00:00
/**
* 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<IRouteMetadata, 'methodName' | 'interceptors'>
): 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}`;
}