openapi-spec-master
Version:
🚀 Professional OpenAPI specification analyzer and MCP server. The ultimate toolkit for API developers with VS Code extension integration.
493 lines (492 loc) • 20.5 kB
JavaScript
import yaml from 'js-yaml';
export class OpenAPIParser {
spec = null;
async parseFromText(content) {
try {
// Try JSON first
this.spec = JSON.parse(content);
}
catch {
try {
// Try YAML
this.spec = yaml.load(content);
}
catch (error) {
throw new Error('Invalid OpenAPI specification format. Please provide valid JSON or YAML.');
}
}
if (!this.spec) {
throw new Error('Invalid specification format.');
}
// Check if it's Swagger 2.0 and convert to OpenAPI 3.0
if (this.spec.swagger && this.spec.swagger.startsWith('2.')) {
this.spec = this.convertSwagger2ToOpenAPI3(this.spec);
}
else if (!this.spec.openapi) {
throw new Error('Invalid OpenAPI specification. Missing openapi version.');
}
return this.spec;
}
async parseFromFile(file) {
const content = await file.text();
return this.parseFromText(content);
}
async parseFromUrl(url) {
try {
const response = await fetch(url, {
headers: {
'Accept': 'application/json, application/yaml, text/yaml, text/plain',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || '';
const content = await response.text();
// Determine if the response is JSON or YAML based on content type or content
if (contentType.includes('application/json') || content.trim().startsWith('{')) {
try {
this.spec = JSON.parse(content);
}
catch (error) {
throw new Error('Invalid JSON format in the fetched specification.');
}
}
else {
try {
this.spec = yaml.load(content);
}
catch (error) {
throw new Error('Invalid YAML format in the fetched specification.');
}
}
if (!this.spec) {
throw new Error('Invalid specification format.');
}
// Check if it's Swagger 2.0 and convert to OpenAPI 3.0
if (this.spec.swagger && this.spec.swagger.startsWith('2.')) {
this.spec = this.convertSwagger2ToOpenAPI3(this.spec);
}
else if (!this.spec.openapi) {
throw new Error('Invalid OpenAPI specification. Missing openapi version.');
}
return this.spec;
}
catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error('Failed to fetch or parse the OpenAPI specification from the provided URL.');
}
}
convertSwagger2ToOpenAPI3(swagger2) {
const openapi3 = {
openapi: '3.0.0',
info: {
title: swagger2.info?.title || 'API',
version: swagger2.info?.version || '1.0.0',
description: swagger2.info?.description,
termsOfService: swagger2.info?.termsOfService,
contact: swagger2.info?.contact,
license: swagger2.info?.license
},
paths: {}
};
// Convert servers
if (swagger2.host || swagger2.basePath) {
const protocol = swagger2.schemes?.[0] || 'https';
const host = swagger2.host || 'localhost';
const basePath = swagger2.basePath || '';
openapi3.servers = [{
url: `${protocol}://${host}${basePath}`,
description: 'Converted from Swagger 2.0'
}];
}
// Convert tags
if (swagger2.tags) {
openapi3.tags = swagger2.tags;
}
// Convert paths
if (swagger2.paths) {
Object.entries(swagger2.paths).forEach(([path, pathItem]) => {
const convertedPathItem = {};
// Convert operations
['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].forEach(method => {
if (pathItem[method]) {
convertedPathItem[method] = this.convertSwagger2Operation(pathItem[method], swagger2.definitions);
}
});
// Convert path-level parameters
if (pathItem.parameters) {
convertedPathItem.parameters = pathItem.parameters.map((param) => this.convertSwagger2Parameter(param, swagger2.definitions));
}
openapi3.paths[path] = convertedPathItem;
});
}
// Convert definitions to components/schemas
if (swagger2.definitions) {
openapi3.components = {
schemas: this.convertSwagger2Definitions(swagger2.definitions)
};
}
return openapi3;
}
convertSwagger2Operation(operation, definitions) {
const converted = {
summary: operation.summary,
description: operation.description,
operationId: operation.operationId,
tags: operation.tags,
deprecated: operation.deprecated,
responses: {}
};
// Convert parameters
if (operation.parameters) {
const convertedParams = [];
let requestBody = null;
operation.parameters.forEach((param) => {
if (param.in === 'body') {
// Convert body parameter to requestBody
requestBody = {
description: param.description,
required: param.required,
content: {
'application/json': {
schema: this.convertSwagger2Schema(param.schema, definitions)
}
}
};
}
else if (param.in === 'formData') {
// Convert formData to requestBody
if (!requestBody) {
requestBody = {
content: {
'application/x-www-form-urlencoded': {
schema: {
type: 'object',
properties: {}
}
}
}
};
}
requestBody.content['application/x-www-form-urlencoded'].schema.properties[param.name] = {
type: param.type,
description: param.description
};
}
else {
// Regular parameter
convertedParams.push(this.convertSwagger2Parameter(param, definitions));
}
});
if (convertedParams.length > 0) {
converted.parameters = convertedParams;
}
if (requestBody) {
converted.requestBody = requestBody;
}
}
// Convert responses
if (operation.responses) {
Object.entries(operation.responses).forEach(([code, response]) => {
converted.responses[code] = this.convertSwagger2Response(response, definitions);
});
}
return converted;
}
convertSwagger2Parameter(param, definitions) {
const converted = {
name: param.name,
in: param.in,
description: param.description,
required: param.required,
deprecated: param.deprecated
};
if (param.schema) {
converted.schema = this.convertSwagger2Schema(param.schema, definitions);
}
else {
// Convert simple parameter to schema
converted.schema = {
type: param.type,
format: param.format,
enum: param.enum,
default: param.default
};
}
return converted;
}
convertSwagger2Response(response, definitions) {
const converted = {
description: response.description || 'Response'
};
if (response.schema) {
converted.content = {
'application/json': {
schema: this.convertSwagger2Schema(response.schema, definitions)
}
};
}
if (response.headers) {
converted.headers = response.headers;
}
return converted;
}
convertSwagger2Schema(schema, definitions) {
if (!schema)
return {};
if (schema.$ref) {
// Convert reference
const refName = schema.$ref.replace('#/definitions/', '');
return { $ref: `#/components/schemas/${refName}` };
}
const converted = {
type: schema.type,
format: schema.format,
description: schema.description,
enum: schema.enum,
default: schema.default,
example: schema.example
};
if (schema.items) {
converted.items = this.convertSwagger2Schema(schema.items, definitions);
}
if (schema.properties) {
converted.properties = {};
Object.entries(schema.properties).forEach(([key, prop]) => {
converted.properties[key] = this.convertSwagger2Schema(prop, definitions);
});
}
if (schema.required) {
converted.required = schema.required;
}
if (schema.allOf) {
converted.allOf = schema.allOf.map((s) => this.convertSwagger2Schema(s, definitions));
}
return converted;
}
convertSwagger2Definitions(definitions) {
const converted = {};
Object.entries(definitions).forEach(([key, definition]) => {
converted[key] = this.convertSwagger2Schema(definition, definitions);
});
return converted;
}
extractEndpoints() {
if (!this.spec)
return [];
const endpoints = [];
const paths = this.spec.paths;
Object.entries(paths).forEach(([path, pathItem]) => {
const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
methods.forEach(method => {
const operation = pathItem[method];
if (!operation)
return;
const parameters = this.extractParameters(operation.parameters || [], pathItem.parameters || []);
const pathSegments = path.split('/').filter(Boolean);
const hasPathParams = path.includes('{');
const hasQueryParams = parameters.some(p => p.in === 'query');
const hasRequestBody = !!operation.requestBody;
const responseTypes = Object.keys(operation.responses || {});
const endpoint = {
id: `${method.toUpperCase()}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`,
path,
method: method.toUpperCase(),
operation,
tags: operation.tags || [],
summary: operation.summary,
description: operation.description,
parameters,
requestBody: operation.requestBody,
responses: this.extractResponses(operation.responses),
deprecated: operation.deprecated || false,
businessContext: this.generateBusinessContext(operation),
aiSuggestions: this.generateAISuggestions(operation, path, method),
complexity: this.calculateComplexity(operation, parameters, hasRequestBody),
security: operation.security,
pathSegments,
hasPathParams,
hasQueryParams,
hasRequestBody,
responseTypes,
estimatedResponseTime: this.estimateResponseTime(operation, parameters, hasRequestBody)
};
endpoints.push(endpoint);
});
});
return endpoints;
}
calculateComplexity(operation, parameters, hasRequestBody) {
let score = 0;
// Base complexity from parameters
score += parameters.length * 0.5;
// Request body adds complexity
if (hasRequestBody)
score += 2;
// Multiple response codes add complexity
const responseCount = Object.keys(operation.responses || {}).length;
score += responseCount * 0.3;
// Security requirements add complexity
if (operation.security && operation.security.length > 0)
score += 1;
// Tags suggest organizational complexity
if (operation.tags && operation.tags.length > 1)
score += 0.5;
if (score <= 2)
return 'low';
if (score <= 5)
return 'medium';
return 'high';
}
estimateResponseTime(operation, parameters, hasRequestBody) {
let score = 0;
// GET requests are typically faster
if (operation.operationId?.toLowerCase().includes('get') ||
operation.summary?.toLowerCase().includes('get') ||
operation.summary?.toLowerCase().includes('list')) {
score -= 1;
}
// POST/PUT/PATCH with body are typically slower
if (hasRequestBody)
score += 1;
// Many parameters suggest complex processing
if (parameters.length > 5)
score += 1;
// Search/filter operations might be slower
if (operation.summary?.toLowerCase().includes('search') ||
operation.summary?.toLowerCase().includes('filter') ||
parameters.some(p => p.name.toLowerCase().includes('search') || p.name.toLowerCase().includes('filter'))) {
score += 1;
}
if (score <= 0)
return 'fast';
if (score <= 1)
return 'medium';
return 'slow';
}
extractParameters(operationParams, pathParams) {
const allParams = [...pathParams, ...operationParams];
return allParams.map(param => {
if (param.$ref) {
// Handle reference - would need to resolve from components
return param;
}
return param;
});
}
extractResponses(responses) {
const result = {};
Object.entries(responses).forEach(([code, response]) => {
if (response.$ref) {
// Handle reference
result[code] = response;
}
else {
result[code] = response;
}
});
return result;
}
generateBusinessContext(operation) {
const tags = operation.tags?.join(', ') || 'General';
const summary = operation.summary || 'No summary available';
// Generate business-friendly context based on operation details
if (operation.summary?.toLowerCase().includes('create') || operation.summary?.toLowerCase().includes('add')) {
return `Business Impact: Creates new ${tags.toLowerCase()} resources. Use this endpoint to add new data to the system.`;
}
else if (operation.summary?.toLowerCase().includes('get') || operation.summary?.toLowerCase().includes('list') || operation.summary?.toLowerCase().includes('fetch')) {
return `Business Impact: Retrieves ${tags.toLowerCase()} information. Use this endpoint to access and display data to users.`;
}
else if (operation.summary?.toLowerCase().includes('update') || operation.summary?.toLowerCase().includes('modify')) {
return `Business Impact: Updates existing ${tags.toLowerCase()} resources. Use this endpoint to modify data based on user actions.`;
}
else if (operation.summary?.toLowerCase().includes('delete') || operation.summary?.toLowerCase().includes('remove')) {
return `Business Impact: Removes ${tags.toLowerCase()} resources. Use this endpoint to clean up or delete data as requested by users.`;
}
return `Business Impact: ${summary} - Part of ${tags} functionality.`;
}
generateAISuggestions(operation, path, method) {
const suggestions = [];
// Generate AI-powered suggestions based on operation characteristics
if (method.toLowerCase() === 'get' && path.includes('{id}')) {
suggestions.push('💡 Perfect for fetching specific record details in your app');
suggestions.push('🔍 Can be used for detail views, edit forms, or data validation');
}
else if (method.toLowerCase() === 'get' && !path.includes('{id}')) {
suggestions.push('📋 Ideal for listing data in tables, dropdowns, or search results');
suggestions.push('🔄 Consider implementing pagination if not already present');
}
else if (method.toLowerCase() === 'post') {
suggestions.push('✨ Use for creating new records from user forms');
suggestions.push('💾 Remember to validate input data before submission');
}
else if (method.toLowerCase() === 'put' || method.toLowerCase() === 'patch') {
suggestions.push('✏️ Perfect for edit forms and data updates');
suggestions.push('🔒 Ensure proper authorization before allowing updates');
}
else if (method.toLowerCase() === 'delete') {
suggestions.push('🗑️ Implement confirmation dialogs for better UX');
suggestions.push('⚠️ Consider soft deletes for important business data');
}
// Add parameter-based suggestions
if (operation.parameters?.some(p => p.name?.toLowerCase().includes('limit'))) {
suggestions.push('⚡ Supports pagination - great for performance');
}
if (operation.parameters?.some(p => p.name?.toLowerCase().includes('filter'))) {
suggestions.push('🎯 Supports filtering - perfect for search functionality');
}
return suggestions;
}
getAllTags() {
if (!this.spec)
return [];
const tags = new Set();
// Get tags from spec definition
this.spec.tags?.forEach(tag => tags.add(tag.name));
// Get tags from operations
Object.values(this.spec.paths).forEach(pathItem => {
const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
methods.forEach(method => {
const operation = pathItem[method];
if (operation?.tags) {
operation.tags.forEach(tag => tags.add(tag));
}
});
});
return Array.from(tags).sort();
}
getAllMethods() {
if (!this.spec)
return [];
const methods = new Set();
Object.values(this.spec.paths).forEach(pathItem => {
['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'].forEach(method => {
if (pathItem[method]) {
methods.add(method.toUpperCase());
}
});
});
return Array.from(methods).sort();
}
getAllStatusCodes() {
if (!this.spec)
return [];
const codes = new Set();
Object.values(this.spec.paths).forEach(pathItem => {
const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'];
methods.forEach(method => {
const operation = pathItem[method];
if (operation?.responses) {
Object.keys(operation.responses).forEach(code => codes.add(code));
}
});
});
return Array.from(codes).sort();
}
getSpec() {
return this.spec;
}
}