import { Injectable, Logger } from '@nestjs/common'; import { AppError, ErrorCode } from '../../common/errors/app.error'; import { OpenAPISpec, OpenAPIPathItem, OpenAPIOperation, OpenAPIParameter, OpenAPIRequestBody, OpenAPISchema, } from '../../common/interfaces/swagger.interface'; import { ParsedEndpoint } from './har-parser.service'; @Injectable() export class SwaggerGeneratorService { private readonly logger = new Logger(SwaggerGeneratorService.name); generateSwagger( endpoints: ParsedEndpoint[], title: string, version: string, ): OpenAPISpec { try { const baseUrl = this.extractBaseUrl(endpoints); const paths = this.generatePaths(endpoints); const securitySchemes = this.generateSecuritySchemes(endpoints); const spec: OpenAPISpec = { openapi: '3.0.0', info: { title, version, description: 'Generated automatically from HAR files and enriched with AI descriptions.', contact: { name: 'API Support', url: 'https://example.com/support', email: 'support@example.com', }, license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT', }, }, servers: [ { url: baseUrl, description: 'Production Server', }, { url: 'http://localhost:3000', description: 'Local Development Server', }, ], paths, components: { schemas: { ErrorResponse: { type: 'object', properties: { statusCode: { type: 'integer', example: 400, }, message: { type: 'string', example: 'Bad Request', }, error: { type: 'string', example: 'Invalid parameters', }, }, }, SuccessResponse: { type: 'object', properties: { statusCode: { type: 'integer', example: 200, }, data: { type: 'object', example: {}, }, }, }, }, securitySchemes, }, }; this.logger.log(`Generated Swagger spec with ${Object.keys(paths).length} paths`); return spec; } catch (error) { throw new AppError( 'Failed to generate Swagger specification', ErrorCode.SWAGGER_GENERATION_ERROR, { service: 'SwaggerGeneratorService', function: 'generateSwagger', originalError: error as Error, }, ); } } private extractBaseUrl(endpoints: ParsedEndpoint[]): string { if (endpoints.length === 0) { return 'http://localhost'; } try { const firstUrl = new URL(endpoints[0].url); return `${firstUrl.protocol}//${firstUrl.host}`; } catch { return 'http://localhost'; } } private generatePaths(endpoints: ParsedEndpoint[]): Record { const pathsMap = new Map>(); for (const endpoint of endpoints) { const normalizedPath = this.normalizePath(endpoint.path); if (!pathsMap.has(normalizedPath)) { pathsMap.set(normalizedPath, new Map()); } const methodMap = pathsMap.get(normalizedPath)!; methodMap.set(endpoint.method.toLowerCase(), endpoint); } const paths: Record = {}; for (const [path, methodMap] of pathsMap.entries()) { const pathItem: OpenAPIPathItem = {}; for (const [method, endpoint] of methodMap.entries()) { const operation = this.generateOperation(endpoint); (pathItem as any)[method] = operation; } paths[path] = pathItem; } return paths; } private generateOperation(endpoint: ParsedEndpoint): OpenAPIOperation { const authDescription = this.getAuthDescription(endpoint); const operation: OpenAPIOperation = { summary: `${endpoint.method} ${endpoint.path}`, description: authDescription, operationId: this.generateOperationId(endpoint), parameters: this.generateParameters(endpoint), responses: { [endpoint.responseStatus.toString()]: { description: `Response with status ${endpoint.responseStatus}`, content: endpoint.responseBody ? { [endpoint.responseContentType || 'application/json']: { schema: this.inferSchema(endpoint.responseBody, endpoint.responseContentType), }, } : undefined, }, }, }; if (endpoint.requestBody && ['POST', 'PUT', 'PATCH'].includes(endpoint.method)) { operation.requestBody = this.generateRequestBody(endpoint); } // Apply security if auth detected if (endpoint.authType && endpoint.authType !== 'none') { operation.security = [this.mapAuthToSecurity(endpoint)]; } return operation; } private generateOperationId(endpoint: ParsedEndpoint): string { const pathParts = endpoint.path .split('/') .filter((p) => p && !p.startsWith('{')) .join('_'); return `${endpoint.method.toLowerCase()}_${pathParts || 'root'}`; } private generateParameters(endpoint: ParsedEndpoint): OpenAPIParameter[] { const parameters: OpenAPIParameter[] = []; for (const qp of endpoint.queryParams) { parameters.push({ name: qp.name, in: 'query', required: false, schema: { type: 'string', example: qp.value, }, }); } const pathParams = endpoint.path.match(/\{[^}]+\}/g) || []; for (const param of pathParams) { const paramName = param.slice(1, -1); parameters.push({ name: paramName, in: 'path', required: true, schema: { type: 'string', }, }); } return parameters; } private generateRequestBody(endpoint: ParsedEndpoint): OpenAPIRequestBody { const contentType = endpoint.requestContentType || 'application/json'; return { required: true, content: { [contentType]: { schema: this.inferSchema(endpoint.requestBody, contentType), }, }, }; } private inferSchema(body: string | undefined, contentType: string | undefined): OpenAPISchema { if (!body) { return { type: 'object' }; } if (contentType?.includes('json')) { try { const parsed = JSON.parse(body); return this.generateSchemaFromObject(parsed); } catch { return { type: 'string' }; } } return { type: 'string' }; } private generateSchemaFromObject(obj: unknown): OpenAPISchema { if (obj === null) { return { type: 'null' }; } if (Array.isArray(obj)) { return { type: 'array', items: obj.length > 0 ? this.generateSchemaFromObject(obj[0]) : { type: 'object' }, }; } if (typeof obj === 'object') { const properties: Record = {}; for (const [key, value] of Object.entries(obj)) { properties[key] = this.generateSchemaFromObject(value); } return { type: 'object', properties, }; } if (typeof obj === 'number') { return { type: 'number' }; } if (typeof obj === 'boolean') { return { type: 'boolean' }; } return { type: 'string' }; } private normalizePath(path: string): string { return path.replace(/\/\d+/g, '/{id}').replace(/\/[a-f0-9-]{36}/gi, '/{uuid}'); } private generateSecuritySchemes(endpoints: ParsedEndpoint[]): Record { const schemes: Record = {}; const seenSchemes = new Set(); for (const endpoint of endpoints) { if (!endpoint.authType || endpoint.authType === 'none') continue; const schemeKey = this.getSecuritySchemeKey(endpoint); if (seenSchemes.has(schemeKey)) continue; seenSchemes.add(schemeKey); if (endpoint.authType === 'bearer') { schemes[schemeKey] = { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', description: 'JWT Bearer token authentication', }; } else if (endpoint.authType === 'basic') { schemes[schemeKey] = { type: 'http', scheme: 'basic', description: 'HTTP Basic authentication', }; } else if (endpoint.authType === 'apiKey' && endpoint.authDetails) { const location = endpoint.authDetails.location || 'header'; const name = endpoint.authDetails.headerName || 'x-api-key'; schemes[schemeKey] = { type: 'apiKey', in: location, name: name, description: `API Key authentication via ${location}: ${name}`, }; } else if (endpoint.authType === 'cookie' && endpoint.authDetails?.cookieName) { schemes[schemeKey] = { type: 'apiKey', in: 'cookie', name: endpoint.authDetails.cookieName, description: `Cookie-based authentication using ${endpoint.authDetails.cookieName}`, }; } } return schemes; } private getSecuritySchemeKey(endpoint: ParsedEndpoint): string { if (endpoint.authType === 'bearer') return 'bearerAuth'; if (endpoint.authType === 'basic') return 'basicAuth'; if (endpoint.authType === 'apiKey' && endpoint.authDetails) { const headerName = endpoint.authDetails.headerName || 'x-api-key'; return `apiKey_${headerName.replace(/[^a-zA-Z0-9]/g, '_')}`; } if (endpoint.authType === 'cookie' && endpoint.authDetails?.cookieName) { return `cookieAuth_${endpoint.authDetails.cookieName.replace(/[^a-zA-Z0-9]/g, '_')}`; } return 'unknownAuth'; } private mapAuthToSecurity(endpoint: ParsedEndpoint): Record { const schemeKey = this.getSecuritySchemeKey(endpoint); return { [schemeKey]: [] }; } private getAuthDescription(endpoint: ParsedEndpoint): string { if (!endpoint.authType || endpoint.authType === 'none') { return '🔓 No authentication required'; } let authInfo = ''; if (endpoint.authType === 'bearer') { authInfo = '🔐 **Requires Authentication**: Bearer token (JWT)'; } else if (endpoint.authType === 'basic') { authInfo = '🔐 **Requires Authentication**: HTTP Basic (username:password)'; } else if (endpoint.authType === 'apiKey' && endpoint.authDetails) { const location = endpoint.authDetails.location || 'header'; const name = endpoint.authDetails.headerName || 'x-api-key'; authInfo = `🔐 **Requires Authentication**: API Key in ${location} (${name})`; } else if (endpoint.authType === 'cookie' && endpoint.authDetails?.cookieName) { authInfo = `🔐 **Requires Authentication**: Cookie-based (${endpoint.authDetails.cookieName})`; } return authInfo; } }