a cache for slack profile pictures and emojis
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271/** * Generates Swagger/OpenAPI specifications from typed route definitions */
import { version } from "../../package.json";import type { HttpMethod, RouteDefinition, RouteMetadata, RouteParam,} from "../types/routes";
interface SecurityScheme { type: string; scheme: string;}
interface SwaggerSpec { openapi: string; info: { title: string; version: string; description: string; contact: { name: string; email: string; }; license: { name: string; url: string; }; }; paths: Record<string, Record<string, unknown>>; components?: { securitySchemes?: Record<string, SecurityScheme>; schemas?: Record<string, Record<string, unknown>>; };}
export class SwaggerGenerator { private spec: SwaggerSpec;
constructor() { this.spec = { openapi: "3.0.0", info: { title: "Cachet", version: version, description: "A high-performance cache and proxy for Slack profile pictures and emojis with comprehensive analytics.", contact: { name: "Kieran Klukas", email: "me@dunkirk.sh", }, license: { name: "AGPL 3.0", url: "https://github.com/taciturnaxolotl/cachet/blob/main/LICENSE.md", }, }, paths: {}, components: { securitySchemes: { bearerAuth: { type: "http", scheme: "bearer", }, }, schemas: { ErrorResponse: { type: "object", required: ["error"], properties: { error: { type: "object", required: ["code", "message", "hint"], properties: { code: { type: "string", example: "ROUTE_NOT_FOUND" }, message: { type: "string", example: "No endpoint exists at /example.", }, hint: { type: "string", example: "See /swagger.json for available endpoints.", }, }, }, }, }, }, }, }; }
/** * Add routes to the Swagger specification */ addRoutes(routes: Record<string, RouteDefinition>) { Object.entries(routes).forEach(([path, routeConfig]) => { // Skip non-API routes if ( typeof routeConfig === "function" || path.includes("dashboard") || path.includes("swagger") || path.includes("favicon") ) { return; }
this.addRoute(path, routeConfig); }); }
/** * Add a single route to the specification */ private addRoute(path: string, routeConfig: RouteDefinition) { const swaggerPath = this.convertPathToSwagger(path);
if (!this.spec.paths[swaggerPath]) { this.spec.paths[swaggerPath] = {}; }
// Process each HTTP method Object.entries(routeConfig).forEach(([method, typedRoute]) => { if ( typeof typedRoute === "object" && "handler" in typedRoute && "metadata" in typedRoute ) { const swaggerMethod = method.toLowerCase(); const methodSpec = this.buildMethodSpec( method as HttpMethod, typedRoute.metadata, ); // Ensure spec.paths is properly initialized before adding method if (this.spec.paths[swaggerPath]) { this.spec.paths[swaggerPath][swaggerMethod] = methodSpec; } } }); }
/** * Convert Express-style path to Swagger format * /users/:id -> /users/{id} */ private convertPathToSwagger(path: string): string { return path.replace(/:([^/]+)/g, "{$1}"); }
/** * Build Swagger specification for a single method */ private buildMethodSpec(method: HttpMethod, metadata: RouteMetadata) { const spec: Record<string, unknown> = { summary: metadata.summary, description: metadata.description, tags: metadata.tags || ["API"], responses: {}, };
// Add parameters if (metadata.parameters) { spec.parameters = [] as Record<string, unknown>[];
// Path parameters if (metadata.parameters.path) { metadata.parameters.path.forEach((param) => { (spec.parameters as Record<string, unknown>[]).push( this.buildParameterSpec(param, "path"), ); }); }
// Query parameters if (metadata.parameters.query) { metadata.parameters.query.forEach((param) => { (spec.parameters as Record<string, unknown>[]).push( this.buildParameterSpec(param, "query"), ); }); }
// Request body if ( metadata.parameters.body && ["POST", "PUT", "PATCH"].includes(method) ) { spec.requestBody = { required: true, content: { "application/json": { schema: metadata.parameters.body, }, }, }; } }
// Add responses Object.entries(metadata.responses).forEach(([status, response]) => { const isError = Number(status) >= 400; const schema = response.schema ?? (isError ? { $ref: "#/components/schemas/ErrorResponse" } : undefined); (spec.responses as Record<string, unknown>)[status] = { description: response.description, ...(schema && { content: { "application/json": { schema }, }, }), }; });
if (!("500" in (spec.responses as Record<string, unknown>))) { (spec.responses as Record<string, unknown>)["500"] = { description: "Unexpected server error", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" }, }, }, }; }
// Add security if required if (metadata.requiresAuth) { spec.security = [{ bearerAuth: [] }]; }
return spec; }
/** * Build parameter specification */ private buildParameterSpec(param: RouteParam, location: "path" | "query") { const schema: Record<string, unknown> = { type: param.type }; if (param.example !== undefined) { schema.example = param.example; }
return { name: param.name, in: location, required: param.required, description: param.description, schema, }; }
/** * Get the complete Swagger specification */ getSpec(): SwaggerSpec { return this.spec; }
/** * Generate JSON string of the specification */ toJSON(): string { return JSON.stringify(this.spec, null, 2); }}
// Export singleton instanceexport const swaggerGenerator = new SwaggerGenerator();