swagger-tools/src/lib/parser.ts
rimskij 409194781e fix: graceful fallback for specs with broken $refs
- parseSpec now tries dereference first, falls back to parse on failure
- Shows warning when spec has broken references
- Tested with Moravia Symfonie API (293 paths, 381 operations)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 14:55:26 +01:00

107 lines
2.9 KiB
TypeScript

import SwaggerParser from '@apidevtools/swagger-parser';
import type { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
import type { ParsedSpec, OpenAPISpec } from './types.js';
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] as const;
export async function parseSpec(specPath: string, options?: { dereference?: boolean }): Promise<{
spec: OpenAPISpec;
metadata: ParsedSpec;
dereferenced: boolean;
}> {
const shouldDereference = options?.dereference !== false;
let spec: OpenAPISpec;
let dereferenced = false;
if (shouldDereference) {
try {
spec = await SwaggerParser.dereference(specPath);
dereferenced = true;
} catch {
// Fallback to parse without dereferencing if refs are broken
spec = await SwaggerParser.parse(specPath);
}
} else {
spec = await SwaggerParser.parse(specPath);
}
const metadata = extractMetadata(spec);
return { spec, metadata, dereferenced };
}
export async function bundleSpec(specPath: string): Promise<OpenAPISpec> {
return SwaggerParser.bundle(specPath);
}
function extractMetadata(spec: OpenAPISpec): ParsedSpec {
const version = getSpecVersion(spec);
const info = spec.info;
const paths = spec.paths || {};
const pathCount = Object.keys(paths).length;
let operationCount = 0;
const tagsSet = new Set<string>();
for (const pathItem of Object.values(paths)) {
if (!pathItem) continue;
for (const method of HTTP_METHODS) {
const operation = (pathItem as Record<string, unknown>)[method] as OpenAPIV3.OperationObject | undefined;
if (operation) {
operationCount++;
if (operation.tags) {
operation.tags.forEach(tag => tagsSet.add(tag));
}
}
}
}
const schemaCount = getSchemaCount(spec);
const servers = getServers(spec);
return {
version,
title: info.title,
description: info.description,
servers,
pathCount,
schemaCount,
operationCount,
tags: Array.from(tagsSet).sort(),
};
}
function getSpecVersion(spec: OpenAPISpec): string {
if ('openapi' in spec) {
return `OpenAPI ${spec.openapi}`;
}
if ('swagger' in spec) {
return `Swagger ${spec.swagger}`;
}
return 'Unknown';
}
function getSchemaCount(spec: OpenAPISpec): number {
if ('components' in spec && spec.components?.schemas) {
return Object.keys(spec.components.schemas).length;
}
if ('definitions' in spec && spec.definitions) {
return Object.keys(spec.definitions).length;
}
return 0;
}
function getServers(spec: OpenAPISpec): string[] {
if ('servers' in spec && spec.servers) {
return spec.servers.map(s => s.url);
}
if ('host' in spec) {
const swagger2 = spec as { host?: string; basePath?: string; schemes?: string[] };
const scheme = swagger2.schemes?.[0] || 'https';
const host = swagger2.host || 'localhost';
const basePath = swagger2.basePath || '';
return [`${scheme}://${host}${basePath}`];
}
return [];
}