a cache for slack profile pictures and emojis
at main 5.0 kB view raw
1/** 2 * Generates Swagger/OpenAPI specifications from typed route definitions 3 */ 4 5import { version } from "../../package.json"; 6import type { 7 HttpMethod, 8 RouteDefinition, 9 RouteMetadata, 10 RouteParam, 11} from "../types/routes"; 12 13interface SecurityScheme { 14 type: string; 15 scheme: string; 16} 17 18interface SwaggerSpec { 19 openapi: string; 20 info: { 21 title: string; 22 version: string; 23 description: string; 24 contact: { 25 name: string; 26 email: string; 27 }; 28 license: { 29 name: string; 30 url: string; 31 }; 32 }; 33 paths: Record<string, Record<string, unknown>>; 34 components?: { 35 securitySchemes?: Record<string, SecurityScheme>; 36 }; 37} 38 39export class SwaggerGenerator { 40 private spec: SwaggerSpec; 41 42 constructor() { 43 this.spec = { 44 openapi: "3.0.0", 45 info: { 46 title: "Cachet", 47 version: version, 48 description: 49 "A high-performance cache and proxy for Slack profile pictures and emojis with comprehensive analytics.", 50 contact: { 51 name: "Kieran Klukas", 52 email: "me@dunkirk.sh", 53 }, 54 license: { 55 name: "AGPL 3.0", 56 url: "https://github.com/taciturnaxolotl/cachet/blob/main/LICENSE.md", 57 }, 58 }, 59 paths: {}, 60 components: { 61 securitySchemes: { 62 bearerAuth: { 63 type: "http", 64 scheme: "bearer", 65 }, 66 }, 67 }, 68 }; 69 } 70 71 /** 72 * Add routes to the Swagger specification 73 */ 74 addRoutes(routes: Record<string, RouteDefinition>) { 75 Object.entries(routes).forEach(([path, routeConfig]) => { 76 // Skip non-API routes 77 if ( 78 typeof routeConfig === "function" || 79 path.includes("dashboard") || 80 path.includes("swagger") || 81 path.includes("favicon") 82 ) { 83 return; 84 } 85 86 this.addRoute(path, routeConfig); 87 }); 88 } 89 90 /** 91 * Add a single route to the specification 92 */ 93 private addRoute(path: string, routeConfig: RouteDefinition) { 94 const swaggerPath = this.convertPathToSwagger(path); 95 96 if (!this.spec.paths[swaggerPath]) { 97 this.spec.paths[swaggerPath] = {}; 98 } 99 100 // Process each HTTP method 101 Object.entries(routeConfig).forEach(([method, typedRoute]) => { 102 if ( 103 typeof typedRoute === "object" && 104 "handler" in typedRoute && 105 "metadata" in typedRoute 106 ) { 107 const swaggerMethod = method.toLowerCase(); 108 const methodSpec = this.buildMethodSpec( 109 method as HttpMethod, 110 typedRoute.metadata, 111 ); 112 // Ensure spec.paths is properly initialized before adding method 113 if (this.spec.paths[swaggerPath]) { 114 this.spec.paths[swaggerPath][swaggerMethod] = methodSpec; 115 } 116 } 117 }); 118 } 119 120 /** 121 * Convert Express-style path to Swagger format 122 * /users/:id -> /users/{id} 123 */ 124 private convertPathToSwagger(path: string): string { 125 return path.replace(/:([^/]+)/g, "{$1}"); 126 } 127 128 /** 129 * Build Swagger specification for a single method 130 */ 131 private buildMethodSpec(method: HttpMethod, metadata: RouteMetadata) { 132 const spec: Record<string, unknown> = { 133 summary: metadata.summary, 134 description: metadata.description, 135 tags: metadata.tags || ["API"], 136 responses: {}, 137 }; 138 139 // Add parameters 140 if (metadata.parameters) { 141 spec.parameters = [] as Record<string, unknown>[]; 142 143 // Path parameters 144 if (metadata.parameters.path) { 145 metadata.parameters.path.forEach((param) => { 146 (spec.parameters as Record<string, unknown>[]).push( 147 this.buildParameterSpec(param, "path"), 148 ); 149 }); 150 } 151 152 // Query parameters 153 if (metadata.parameters.query) { 154 metadata.parameters.query.forEach((param) => { 155 (spec.parameters as Record<string, unknown>[]).push( 156 this.buildParameterSpec(param, "query"), 157 ); 158 }); 159 } 160 161 // Request body 162 if ( 163 metadata.parameters.body && 164 ["POST", "PUT", "PATCH"].includes(method) 165 ) { 166 spec.requestBody = { 167 required: true, 168 content: { 169 "application/json": { 170 schema: metadata.parameters.body, 171 }, 172 }, 173 }; 174 } 175 } 176 177 // Add responses 178 Object.entries(metadata.responses).forEach(([status, response]) => { 179 (spec.responses as Record<string, unknown>)[status] = { 180 description: response.description, 181 ...(response.schema && { 182 content: { 183 "application/json": { 184 schema: response.schema, 185 }, 186 }, 187 }), 188 }; 189 }); 190 191 // Add security if required 192 if (metadata.requiresAuth) { 193 spec.security = [{ bearerAuth: [] }]; 194 } 195 196 return spec; 197 } 198 199 /** 200 * Build parameter specification 201 */ 202 private buildParameterSpec(param: RouteParam, location: "path" | "query") { 203 const schema: Record<string, unknown> = { type: param.type }; 204 if (param.example !== undefined) { 205 schema.example = param.example; 206 } 207 208 return { 209 name: param.name, 210 in: location, 211 required: param.required, 212 description: param.description, 213 schema, 214 }; 215 } 216 217 /** 218 * Get the complete Swagger specification 219 */ 220 getSpec(): SwaggerSpec { 221 return this.spec; 222 } 223 224 /** 225 * Generate JSON string of the specification 226 */ 227 toJSON(): string { 228 return JSON.stringify(this.spec, null, 2); 229 } 230} 231 232// Export singleton instance 233export const swaggerGenerator = new SwaggerGenerator();