UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

231 lines (230 loc) 7.56 kB
"use strict"; /** * REST API Route Registry * * Central registry for all REST API routes with their metadata and schemas. * Provides single source of truth for routing, OpenAPI generation, and fixture validation. * * PRD #354: REST API Route Registry with Auto-Generated OpenAPI and Test Fixtures */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RestRouteRegistry = void 0; /** * Registry for managing REST API routes * * Provides: * - Route registration with Zod schemas * - Path matching with parameter extraction * - Route discovery for OpenAPI generation * - Schema access for fixture validation */ class RestRouteRegistry { routes = new Map(); logger; constructor(logger) { this.logger = logger; } /** * Generate a unique key for a route based on method and path pattern */ getRouteKey(method, path) { return `${method}:${path}`; } /** * Compile a path pattern into a regex for matching * * Converts path parameters like :sessionId into capture groups * Example: "/api/v1/visualize/:sessionId" -> /^\/api\/v1\/visualize\/([^/]+)$/ */ compilePath(path) { const paramNames = []; // Escape special regex characters except for parameter placeholders const regexPattern = path .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special chars .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, paramName) => { paramNames.push(paramName); return '([^/]+)'; // Capture group for parameter value }); const regex = new RegExp(`^${regexPattern}$`); return { regex, paramNames }; } /** * Register a route in the registry * * @param route - Route definition with path, method, schemas, and metadata * @throws Error if route with same method and path is already registered */ register(route) { const key = this.getRouteKey(route.method, route.path); if (this.routes.has(key)) { throw new Error(`Route already registered: ${route.method} ${route.path}`); } const { regex, paramNames } = this.compilePath(route.path); this.routes.set(key, { definition: route, regex, paramNames, }); this.logger.debug('Route registered in REST route registry', { method: route.method, path: route.path, tags: route.tags, paramNames, }); } /** * Find a matching route for the given method and path * * Checks routes in order: * 1. Exact matches (no parameters) * 2. Parameterized routes (extracts parameter values) * * @param method - HTTP method * @param path - Request path to match * @returns RouteMatch with route and extracted params, or null if no match */ findRoute(method, path) { const upperMethod = method.toUpperCase(); // First pass: try exact match (more efficient for non-parameterized routes) const exactKey = this.getRouteKey(upperMethod, path); const exactMatch = this.routes.get(exactKey); if (exactMatch) { return { route: exactMatch.definition, params: {}, }; } // Second pass: try parameterized routes for (const [key, compiled] of this.routes) { // Skip if method doesn't match if (!key.startsWith(`${upperMethod}:`)) { continue; } const match = compiled.regex.exec(path); if (match) { // Extract parameter values from capture groups const params = {}; compiled.paramNames.forEach((name, index) => { params[name] = match[index + 1]; // +1 because match[0] is full match }); return { route: compiled.definition, params, }; } } return null; } /** * Find allowed methods for a path (ignoring method) * * Used to return METHOD_NOT_ALLOWED with proper Allow header * when a path matches but method doesn't. * * @param path - Request path to match * @returns Array of allowed methods, or empty array if path doesn't match any route */ findAllowedMethods(path) { const methods = new Set(); for (const compiled of this.routes.values()) { const match = compiled.regex.exec(path); if (match) { methods.add(compiled.definition.method); } } return Array.from(methods); } /** * Get all registered route definitions * * Used by OpenAPI generator to document all endpoints */ getAllRoutes() { return Array.from(this.routes.values()).map((r) => r.definition); } /** * Get the response schema for a specific route * * Used by fixture validator to validate fixture data * * @param method - HTTP method * @param pathPattern - Route path pattern (e.g., "/api/v1/visualize/:sessionId") * @returns Zod schema for the response, or null if route not found */ getResponseSchema(method, pathPattern) { const key = this.getRouteKey(method.toUpperCase(), pathPattern); const route = this.routes.get(key); return route?.definition.response ?? null; } /** * Get the error response schema for a specific route and status code * * @param method - HTTP method * @param pathPattern - Route path pattern * @param statusCode - HTTP status code * @returns Zod schema for the error response, or null if not defined */ getErrorResponseSchema(method, pathPattern, statusCode) { const key = this.getRouteKey(method.toUpperCase(), pathPattern); const route = this.routes.get(key); return route?.definition.errorResponses?.[statusCode] ?? null; } /** * Check if a route is registered */ hasRoute(method, pathPattern) { const key = this.getRouteKey(method.toUpperCase(), pathPattern); return this.routes.has(key); } /** * Get the number of registered routes */ getRouteCount() { return this.routes.size; } /** * Get all unique tags from registered routes */ getTags() { const tags = new Set(); for (const compiled of this.routes.values()) { for (const tag of compiled.definition.tags) { tags.add(tag); } } return Array.from(tags).sort(); } /** * Get routes filtered by tag */ getRoutesByTag(tag) { return this.getAllRoutes().filter((route) => route.tags.includes(tag)); } /** * Clear all registered routes */ clear() { this.routes.clear(); this.logger.debug('REST route registry cleared'); } /** * Get registry statistics */ getStats() { const routesByMethod = { GET: 0, POST: 0, PUT: 0, DELETE: 0, }; for (const compiled of this.routes.values()) { routesByMethod[compiled.definition.method]++; } return { totalRoutes: this.routes.size, tags: this.getTags(), routesByMethod, }; } } exports.RestRouteRegistry = RestRouteRegistry;