182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
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<string, unknown>;
|
|
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<string, unknown>)[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<string, unknown>,
|
|
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<string, unknown>)[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<string, unknown>,
|
|
tags: endpointTags,
|
|
deprecated: operation.deprecated || false,
|
|
moduleName,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { endpoints, baseUrl };
|
|
}
|
|
|
|
export async function parseOpenApiDocument(input: string | object): Promise<ParseResult> {
|
|
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<string, string | null>();
|
|
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<string>();
|
|
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<string>();
|
|
|
|
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 };
|
|
}
|