@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
JavaScript
;
/**
* 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;