import SwaggerParser from '@apidevtools/swagger-parser'; import type { OpenAPI, OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; type OpenApiDoc = OpenAPIV3.Document | OpenAPIV3_1.Document; type SwaggerDoc = OpenAPIV2.Document; export type ParsedModule = { name: string; description: string | null; source: 'tag' | 'path_prefix'; }; export type ParsedEndpoint = { method: string; path: string; summary: string | null; description: string | null; operationId: string | null; parameters: unknown[]; requestBody: unknown | null; responses: Record; tags: string[]; deprecated: boolean; moduleName: string; }; export type ParseResult = { name: string; description: string | null; version: string; openApiVersion: string; baseUrl: string | null; spec: unknown; modules: ParsedModule[]; endpoints: ParsedEndpoint[]; }; function isSwagger2(api: OpenAPI.Document): api is SwaggerDoc { return 'swagger' in api && (api as any).swagger?.startsWith('2.'); } function parseSwagger2Endpoints(api: SwaggerDoc): { endpoints: ParsedEndpoint[]; baseUrl: string | null } { const baseUrl = api.basePath || (api.host ? `http://${api.host}${api.basePath || ''}` : null); const endpoints: ParsedEndpoint[] = []; const paths = api.paths || {}; for (const [pathStr, pathItem] of Object.entries(paths)) { if (!pathItem) continue; const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as const; for (const method of methods) { const operation = (pathItem as Record)[method] as OpenAPIV2.OperationObject | undefined; if (!operation) continue; const endpointTags = operation.tags || []; const prefix = pathStr.split('/').filter(Boolean)[0] || 'default'; const moduleName = endpointTags[0] || prefix; // Convert Swagger 2 body parameter to requestBody-like structure const params = (operation.parameters || []) as OpenAPIV2.Parameter[]; const bodyParam = params.find((p: any) => p.in === 'body'); const nonBodyParams = params.filter((p: any) => p.in !== 'body'); endpoints.push({ method: method.toUpperCase(), path: pathStr, summary: operation.summary || null, description: operation.description || null, operationId: operation.operationId || null, parameters: nonBodyParams as unknown[], requestBody: bodyParam ? { schema: (bodyParam as any).schema } : null, responses: (operation.responses || {}) as Record, tags: endpointTags, deprecated: operation.deprecated || false, moduleName, }); } } return { endpoints, baseUrl }; } function parseOpenApi3Endpoints(api: OpenApiDoc): { endpoints: ParsedEndpoint[]; baseUrl: string | null } { const baseUrl = api.servers?.[0]?.url || null; const endpoints: ParsedEndpoint[] = []; const paths = api.paths || {}; for (const [pathStr, pathItem] of Object.entries(paths)) { if (!pathItem) continue; const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as const; for (const method of methods) { const operation = (pathItem as Record)[method] as OpenAPIV3.OperationObject | undefined; if (!operation) continue; const endpointTags = operation.tags || []; const prefix = pathStr.split('/').filter(Boolean)[0] || 'default'; const moduleName = endpointTags[0] || prefix; endpoints.push({ method: method.toUpperCase(), path: pathStr, summary: operation.summary || null, description: operation.description || null, operationId: operation.operationId || null, parameters: (operation.parameters || []) as unknown[], requestBody: operation.requestBody || null, responses: (operation.responses || {}) as Record, tags: endpointTags, deprecated: operation.deprecated || false, moduleName, }); } } return { endpoints, baseUrl }; } export async function parseOpenApiDocument(input: string | object): Promise { let specInput: string | object = input; // If input is a URL, fetch the content first so that swagger-parser // works on a plain object and doesn't need network access for $ref resolution if (typeof input === 'string' && input.startsWith('http')) { const res = await fetch(input); if (!res.ok) throw new Error(`Failed to fetch spec from URL: ${res.status} ${res.statusText}`); specInput = await res.json(); } // Bundle resolves all $refs into a single document, then dereference inlines them const bundled = await SwaggerParser.bundle(specInput as any) as OpenAPI.Document; const api = await SwaggerParser.dereference(bundled, { dereference: { circular: 'ignore' }, }) as OpenAPI.Document; const isV2 = isSwagger2(api); const openApiVersion = isV2 ? (api as SwaggerDoc).swagger : ('openapi' in api ? (api as any).openapi : 'unknown'); const name = api.info.title; const description = api.info.description || null; const version = api.info.version; // Collect tags const tagMap = new Map(); if (api.tags) { for (const tag of api.tags) { tagMap.set(tag.name, tag.description || null); } } // Parse endpoints based on spec version const { endpoints, baseUrl } = isV2 ? parseSwagger2Endpoints(api as SwaggerDoc) : parseOpenApi3Endpoints(api as OpenApiDoc); // Track used tags const usedTags = new Set(); for (const ep of endpoints) { for (const tag of ep.tags) { usedTags.add(tag); if (!tagMap.has(tag)) tagMap.set(tag, null); } } // Build modules const modules: ParsedModule[] = []; const moduleNames = new Set(); for (const [tagName, tagDesc] of tagMap) { if (usedTags.has(tagName)) { modules.push({ name: tagName, description: tagDesc, source: 'tag' }); moduleNames.add(tagName); } } for (const endpoint of endpoints) { if (endpoint.tags.length === 0 && !moduleNames.has(endpoint.moduleName)) { modules.push({ name: endpoint.moduleName, description: null, source: 'path_prefix' }); moduleNames.add(endpoint.moduleName); } } return { name, description, version, openApiVersion, baseUrl, spec: api, modules, endpoints }; }