api-scout
Version:
๐ Automatically scout, discover and generate beautiful interactive API documentation from your codebase. Supports Express.js, NestJS, FastAPI, Spring Boot with interactive testing and security analysis.
786 lines (683 loc) โข 30.2 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const Handlebars = require('handlebars');
const InteractiveTester = require('./interactive-tester');
const AuthenticationAnalyzer = require('../security/auth-analyzer');
class Generator {
constructor(options = {}) {
this.options = options;
this.templatesPath = path.join(__dirname, '../templates');
this.authAnalyzer = new AuthenticationAnalyzer();
}
async generate(apiData) {
const outputPath = path.resolve(this.options.output);
// Ensure output directory exists
await fs.ensureDir(outputPath);
// Perform security analysis
const securityAnalysis = await this.authAnalyzer.analyzeProject(this.options.input);
apiData.security = this.authAnalyzer.generateSecurityReport(securityAnalysis);
// Generate based on template type
switch (this.options.template) {
case 'swagger':
await this.generateSwaggerDocs(apiData, outputPath);
break;
case 'redoc':
await this.generateRedocDocs(apiData, outputPath);
break;
case 'custom':
await this.generateCustomDocs(apiData, outputPath);
break;
default:
await this.generateSwaggerDocs(apiData, outputPath);
}
console.log(`๐ Generated documentation in: ${outputPath}`);
}
async generateSwaggerDocs(apiData, outputPath) {
// Generate OpenAPI specification
const openApiSpec = this.generateOpenAPISpec(apiData);
// Write OpenAPI JSON
await fs.writeFile(
path.join(outputPath, 'openapi.json'),
JSON.stringify(openApiSpec, null, 2)
);
// Generate Swagger UI HTML
const swaggerHTML = await this.generateSwaggerHTML(openApiSpec);
await fs.writeFile(path.join(outputPath, 'index.html'), swaggerHTML);
// Copy Swagger UI assets
await this.copySwaggerAssets(outputPath);
// Generate additional files
await this.generateSummaryReport(apiData, outputPath);
}
async generateRedocDocs(apiData, outputPath) {
// Generate OpenAPI specification
const openApiSpec = this.generateOpenAPISpec(apiData);
// Write OpenAPI JSON
await fs.writeFile(
path.join(outputPath, 'openapi.json'),
JSON.stringify(openApiSpec, null, 2)
);
// Generate ReDoc HTML
const redocHTML = await this.generateRedocHTML(openApiSpec);
await fs.writeFile(path.join(outputPath, 'index.html'), redocHTML);
}
async generateCustomDocs(apiData, outputPath) {
// Generate custom interactive documentation
const customHTML = await this.generateCustomHTML(apiData);
await fs.writeFile(path.join(outputPath, 'index.html'), customHTML);
// Copy custom assets
await this.copyCustomAssets(outputPath);
// Generate JSON data file
await fs.writeFile(
path.join(outputPath, 'api-data.json'),
JSON.stringify(apiData, null, 2)
);
}
generateOpenAPISpec(apiData) {
const spec = {
openapi: '3.0.3',
info: {
title: 'Generated API Documentation',
description: 'Automatically generated API documentation from codebase analysis',
version: '1.0.0',
contact: {
name: 'API Documentation Generator'
}
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server'
}
],
paths: {},
components: {
schemas: {}
},
tags: []
};
// Group endpoints by tags
const tagSet = new Set();
// Process endpoints
for (const endpoint of apiData.endpoints) {
const pathKey = this.normalizePath(endpoint.path);
if (!spec.paths[pathKey]) {
spec.paths[pathKey] = {};
}
const operation = {
summary: endpoint.description || `${endpoint.method} ${endpoint.path}`,
description: endpoint.description || `Generated from ${path.basename(endpoint.file)}`,
operationId: this.generateOperationId(endpoint),
tags: endpoint.tags || [this.inferTag(endpoint.file)],
parameters: this.convertParameters(endpoint.parameters || []),
responses: this.generateResponses(endpoint)
};
// Add request body for POST/PUT/PATCH
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && endpoint.parameters) {
const bodyParam = endpoint.parameters.find(p => p.in === 'body');
if (bodyParam) {
operation.requestBody = {
required: bodyParam.required,
content: {
'application/json': {
schema: { type: 'object' }
}
}
};
}
}
spec.paths[pathKey][endpoint.method.toLowerCase()] = operation;
// Collect tags
if (endpoint.tags) {
endpoint.tags.forEach(tag => tagSet.add(tag));
} else {
tagSet.add(this.inferTag(endpoint.file));
}
}
// Process schemas
for (const schema of apiData.schemas || []) {
spec.components.schemas[schema.name] = {
type: schema.type || 'object',
properties: schema.properties || {},
description: `Generated from ${path.basename(schema.file)}`
};
}
// Add tags
spec.tags = Array.from(tagSet).map(tag => ({
name: tag,
description: `${tag} related endpoints`
}));
return spec;
}
normalizePath(apiPath) {
// Convert path parameters to OpenAPI format
return apiPath.replace(/:([^/]+)/g, '{$1}');
}
generateOperationId(endpoint) {
const pathParts = endpoint.path.split('/').filter(p => p && !p.startsWith(':'));
const baseName = pathParts.join('_') || 'root';
return `${endpoint.method.toLowerCase()}_${baseName}`.replace(/[^a-zA-Z0-9_]/g, '_');
}
inferTag(filePath) {
const fileName = path.basename(filePath, path.extname(filePath));
return fileName.replace(/[^a-zA-Z0-9]/g, '');
}
convertParameters(parameters) {
return parameters
.filter(p => p.in !== 'body')
.map(param => ({
name: param.name,
in: param.in,
required: param.required,
schema: {
type: param.type || 'string'
},
description: param.description
}));
}
generateResponses(endpoint) {
const responses = {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: { type: 'object' }
}
}
}
};
// Add error responses for different methods
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
responses['400'] = {
description: 'Bad Request'
};
}
if (endpoint.method === 'GET') {
responses['404'] = {
description: 'Not Found'
};
}
return responses;
}
async generateSwaggerHTML(openApiSpec) {
const template = `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
<style>
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body { margin:0; background: #fafafa; }
.swagger-ui .topbar { display: none; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
spec: ${JSON.stringify(openApiSpec, null, 2)},
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
tryItOutEnabled: true,
requestInterceptor: function(request) {
console.log('API Request:', request);
return request;
}
});
}
</script>
</body>
</html>`;
return template;
}
async generateRedocHTML(openApiSpec) {
return `
<html>
<head>
<title>API Documentation</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<style>
body { margin: 0; padding: 0; }
</style>
</head>
<body>
<div id="redoc-container"></div>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0/bundles/redoc.standalone.js"></script>
<script>
Redoc.init(${JSON.stringify(openApiSpec, null, 2)}, {
theme: {
colors: {
primary: {
main: '#32329f'
}
},
typography: {
fontSize: '14px',
fontFamily: 'Roboto, sans-serif'
}
}
}, document.getElementById('redoc-container'));
</script>
</body>
</html>`;
}
async generateCustomHTML(apiData) {
const tester = new InteractiveTester();
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๐ Interactive API Documentation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem 0; margin-bottom: 2rem; }
.header h1 { text-align: center; font-size: 2.5rem; }
.header p { text-align: center; font-size: 1.1rem; opacity: 0.9; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; }
.stat-number { font-size: 2rem; font-weight: bold; color: #667eea; }
.stat-label { color: #666; margin-top: 0.5rem; }
.endpoint-list { display: grid; gap: 1rem; }
.endpoint { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }
.endpoint-header { padding: 1rem; background: #f8f9fa; border-bottom: 1px solid #dee2e6; display: flex; align-items: center; gap: 1rem; justify-content: space-between; }
.endpoint-left { display: flex; align-items: center; gap: 1rem; }
.method { padding: 0.25rem 0.75rem; border-radius: 4px; font-weight: bold; font-size: 0.875rem; }
.method.GET { background: #d4edda; color: #155724; }
.method.POST { background: #cce5ff; color: #004085; }
.method.PUT { background: #fff3cd; color: #856404; }
.method.DELETE { background: #f8d7da; color: #721c24; }
.method.PATCH { background: #e2e3e5; color: #383d41; }
.endpoint-path { font-family: 'Courier New', monospace; font-weight: bold; }
.endpoint-details { padding: 1rem; }
.endpoint-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-top: 1rem; }
.meta-section { padding: 1rem; background: #f8f9fa; border-radius: 6px; }
.meta-title { font-weight: bold; margin-bottom: 0.5rem; color: #495057; }
.parameter { padding: 0.5rem 0; border-bottom: 1px solid #dee2e6; }
.parameter:last-child { border-bottom: none; }
.param-name { font-family: 'Courier New', monospace; font-weight: bold; }
.param-type { color: #6c757d; font-size: 0.875rem; }
.search-box { width: 100%; padding: 1rem; border: 2px solid #dee2e6; border-radius: 8px; font-size: 1rem; margin-bottom: 2rem; }
.filter-buttons { display: flex; gap: 0.5rem; margin-bottom: 2rem; flex-wrap: wrap; }
.filter-btn { padding: 0.5rem 1rem; border: 2px solid #dee2e6; background: white; border-radius: 6px; cursor: pointer; transition: all 0.2s; }
.filter-btn.active { background: #667eea; color: white; border-color: #667eea; }
.filter-btn:hover { border-color: #667eea; }
.test-btn { background: #FF9800; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.875rem; transition: background 0.2s; }
.test-btn:hover { background: #F57C00; }
.code-examples { margin-top: 1rem; }
.example-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.example-tab { padding: 0.5rem 1rem; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px 4px 0 0; cursor: pointer; font-size: 0.875rem; }
.example-tab.active { background: white; border-bottom: 1px solid white; margin-bottom: -1px; }
.example-content { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 0 6px 6px 6px; padding: 1rem; }
.example-content pre { margin: 0; background: #2d3748; color: #e2e8f0; padding: 1rem; border-radius: 4px; overflow-x: auto; }
.example-content code { font-family: 'Courier New', monospace; font-size: 0.875rem; }
</style>
${tester.generateTesterHTML()}
</head>
<body>
<div class="header">
<div class="container">
<h1>๐ API Documentation</h1>
<p>Generated automatically from your codebase</p>
</div>
</div>
<div class="container">
<div class="stats">
<div class="stat-card">
<div class="stat-number">${apiData.endpoints.length}</div>
<div class="stat-label">Total Endpoints</div>
</div>
<div class="stat-card">
<div class="stat-number">${apiData.frameworks.length}</div>
<div class="stat-label">Frameworks Detected</div>
</div>
<div class="stat-card">
<div class="stat-number">${apiData.schemas?.length || 0}</div>
<div class="stat-label">Data Models</div>
</div>
<div class="stat-card">
<div class="stat-number">${apiData.metadata.totalFiles}</div>
<div class="stat-label">Files Scanned</div>
</div>
</div>
<input type="text" class="search-box" placeholder="๐ Search endpoints..." id="searchBox">
<div class="filter-buttons">
<button class="filter-btn active" data-method="ALL">All Methods</button>
<button class="filter-btn" data-method="GET">GET</button>
<button class="filter-btn" data-method="POST">POST</button>
<button class="filter-btn" data-method="PUT">PUT</button>
<button class="filter-btn" data-method="DELETE">DELETE</button>
<button class="filter-btn" data-method="PATCH">PATCH</button>
</div>
<div class="endpoint-list" id="endpointList">
${apiData.endpoints.map(endpoint => `
<div class="endpoint" data-method="${endpoint.method}">
<div class="endpoint-header">
<div class="endpoint-left">
<span class="method ${endpoint.method}">${endpoint.method}</span>
<span class="endpoint-path">${endpoint.path}</span>
</div>
<button class="test-btn" onclick="openTester('${endpoint.method}', '${endpoint.path}', ${JSON.stringify(endpoint.parameters || [])})">๐งช Test API</button>
</div>
<div class="endpoint-details">
${endpoint.description ? `<p><strong>Description:</strong> ${endpoint.description}</p>` : ''}
<p><strong>File:</strong> <code>${path.basename(endpoint.file)}</code> (Line ${endpoint.line || 'N/A'})</p>
${endpoint.parameters && endpoint.parameters.length > 0 ? `
<div class="endpoint-meta">
<div class="meta-section">
<div class="meta-title">Parameters</div>
${endpoint.parameters.map(param => `
<div class="parameter">
<div class="param-name">${param.name}</div>
<div class="param-type">${param.type} โข ${param.in} โข ${param.required ? 'Required' : 'Optional'}</div>
${param.description ? `<div>${param.description}</div>` : ''}
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="code-examples">
<div class="example-tabs">
<div class="example-tab active" onclick="showExample('curl-${endpoint.method}-${endpoint.path.replace(/[^a-zA-Z0-9]/g, '')}')">cURL</div>
<div class="example-tab" onclick="showExample('js-${endpoint.method}-${endpoint.path.replace(/[^a-zA-Z0-9]/g, '')}')">JavaScript</div>
<div class="example-tab" onclick="showExample('python-${endpoint.method}-${endpoint.path.replace(/[^a-zA-Z0-9]/g, '')}')">Python</div>
</div>
<div class="example-content">
<div id="curl-${endpoint.method}-${endpoint.path.replace(/[^a-zA-Z0-9]/g, '')}" class="example-section active">
<pre><code>${this.generateCurlExample(endpoint)}</code></pre>
</div>
<div id="js-${endpoint.method}-${endpoint.path.replace(/[^a-zA-Z0-9]/g, '')}" class="example-section" style="display: none;">
<pre><code>${this.generateJavaScriptExample(endpoint)}</code></pre>
</div>
<div id="python-${endpoint.method}-${endpoint.path.replace(/[^a-zA-Z0-9]/g, '')}" class="example-section" style="display: none;">
<pre><code>${this.generatePythonExample(endpoint)}</code></pre>
</div>
</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
<script>
${tester.generateTesterJS()}
// Search functionality
const searchBox = document.getElementById('searchBox');
const endpointList = document.getElementById('endpointList');
const filterButtons = document.querySelectorAll('.filter-btn');
let currentFilter = 'ALL';
function filterEndpoints() {
const searchTerm = searchBox.value.toLowerCase();
const endpoints = endpointList.querySelectorAll('.endpoint');
endpoints.forEach(endpoint => {
const method = endpoint.dataset.method;
const text = endpoint.textContent.toLowerCase();
const matchesSearch = text.includes(searchTerm);
const matchesFilter = currentFilter === 'ALL' || method === currentFilter;
endpoint.style.display = matchesSearch && matchesFilter ? 'block' : 'none';
});
}
searchBox.addEventListener('input', filterEndpoints);
filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
filterButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.method;
filterEndpoints();
});
});
// Example tab switching
function showExample(exampleId) {
// Hide all example sections in the same container
const container = document.getElementById(exampleId).closest('.code-examples');
container.querySelectorAll('.example-section').forEach(section => {
section.style.display = 'none';
});
container.querySelectorAll('.example-tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected example
document.getElementById(exampleId).style.display = 'block';
event.target.classList.add('active');
}
</script>
</body>
</html>`;
}
async generateSummaryReport(apiData, outputPath) {
const report = {
summary: {
totalEndpoints: apiData.endpoints.length,
frameworks: apiData.frameworks,
totalSchemas: apiData.schemas?.length || 0,
filesScanned: apiData.metadata.totalFiles,
generatedAt: new Date().toISOString()
},
endpointsByMethod: this.groupEndpointsByMethod(apiData.endpoints),
endpointsByFile: this.groupEndpointsByFile(apiData.endpoints),
coverage: this.calculateCoverage(apiData)
};
await fs.writeFile(
path.join(outputPath, 'report.json'),
JSON.stringify(report, null, 2)
);
}
groupEndpointsByMethod(endpoints) {
const groups = {};
endpoints.forEach(endpoint => {
if (!groups[endpoint.method]) {
groups[endpoint.method] = [];
}
groups[endpoint.method].push(endpoint);
});
return groups;
}
groupEndpointsByFile(endpoints) {
const groups = {};
endpoints.forEach(endpoint => {
const fileName = path.basename(endpoint.file);
if (!groups[fileName]) {
groups[fileName] = [];
}
groups[fileName].push(endpoint);
});
return groups;
}
calculateCoverage(apiData) {
return {
filesWithEndpoints: new Set(apiData.endpoints.map(e => e.file)).size,
totalFiles: apiData.metadata.totalFiles,
coveragePercentage: Math.round((new Set(apiData.endpoints.map(e => e.file)).size / apiData.metadata.totalFiles) * 100)
};
}
async copySwaggerAssets(outputPath) {
// In a real implementation, you might want to copy local Swagger UI assets
// For now, we're using CDN links in the HTML
}
async copyCustomAssets(outputPath) {
// Copy any custom CSS/JS assets if needed
}
generateCurlExample(endpoint) {
const url = `http://localhost:3000${endpoint.path}`;
const pathParams = endpoint.parameters?.filter(p => p.in === 'path') || [];
const queryParams = endpoint.parameters?.filter(p => p.in === 'query') || [];
const bodyParams = endpoint.parameters?.filter(p => p.in === 'body') || [];
let finalUrl = url;
// Replace path parameters
pathParams.forEach(param => {
finalUrl = finalUrl.replace(`:${param.name}`, `{${param.name}}`);
});
// Add query parameters
if (queryParams.length > 0) {
const queryString = queryParams.map(p => `${p.name}={${p.name}}`).join('&');
finalUrl += `?${queryString}`;
}
let curlCommand = `curl -X ${endpoint.method} "${finalUrl}"`;
// Add headers
curlCommand += ` \\
-H "Content-Type: application/json"`;
// Add authentication example
if (endpoint.middleware?.includes('authenticateToken') || endpoint.middleware?.includes('authenticate')) {
curlCommand += ` \\
-H "Authorization: Bearer YOUR_TOKEN"`;
}
// Add body for POST/PUT/PATCH
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && bodyParams.length > 0) {
const bodyExample = this.generateBodyExample(bodyParams);
curlCommand += ` \\
-d '${bodyExample}'`;
}
return curlCommand;
}
generateJavaScriptExample(endpoint) {
const url = `http://localhost:3000${endpoint.path}`;
const pathParams = endpoint.parameters?.filter(p => p.in === 'path') || [];
const queryParams = endpoint.parameters?.filter(p => p.in === 'query') || [];
const bodyParams = endpoint.parameters?.filter(p => p.in === 'body') || [];
let finalUrl = url;
// Replace path parameters
pathParams.forEach(param => {
finalUrl = finalUrl.replace(`:${param.name}`, `\${${param.name}}`);
});
let jsCode = '';
// Define parameters
if (pathParams.length > 0) {
jsCode += `// Path parameters\n`;
pathParams.forEach(param => {
jsCode += `const ${param.name} = 'example_${param.name}';\n`;
});
jsCode += '\n';
}
if (queryParams.length > 0) {
jsCode += `// Query parameters\n`;
jsCode += `const params = new URLSearchParams({\n`;
queryParams.forEach(param => {
jsCode += ` ${param.name}: 'example_value',\n`;
});
jsCode += `});\n\n`;
}
// Build fetch request
jsCode += `const response = await fetch('${finalUrl}${queryParams.length > 0 ? '?' + '${params}' : ''}', {\n`;
jsCode += ` method: '${endpoint.method}',\n`;
jsCode += ` headers: {\n`;
jsCode += ` 'Content-Type': 'application/json',\n`;
if (endpoint.middleware?.includes('authenticateToken') || endpoint.middleware?.includes('authenticate')) {
jsCode += ` 'Authorization': 'Bearer YOUR_TOKEN',\n`;
}
jsCode += ` },\n`;
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
const bodyExample = this.generateBodyExample(bodyParams);
jsCode += ` body: JSON.stringify(${bodyExample}),\n`;
}
jsCode += `});\n\n`;
jsCode += `const data = await response.json();\n`;
jsCode += `console.log(data);`;
return jsCode;
}
generatePythonExample(endpoint) {
const url = `http://localhost:3000${endpoint.path}`;
const pathParams = endpoint.parameters?.filter(p => p.in === 'path') || [];
const queryParams = endpoint.parameters?.filter(p => p.in === 'query') || [];
const bodyParams = endpoint.parameters?.filter(p => p.in === 'body') || [];
let pythonCode = `import requests\nimport json\n\n`;
// Define parameters
if (pathParams.length > 0) {
pythonCode += `# Path parameters\n`;
pathParams.forEach(param => {
pythonCode += `${param.name} = 'example_${param.name}'\n`;
});
pythonCode += '\n';
}
// Build URL
let finalUrl = url;
pathParams.forEach(param => {
finalUrl = finalUrl.replace(`:${param.name}`, `{${param.name}}`);
});
pythonCode += `url = f"${finalUrl}"\n\n`;
// Headers
pythonCode += `headers = {\n`;
pythonCode += ` 'Content-Type': 'application/json',\n`;
if (endpoint.middleware?.includes('authenticateToken') || endpoint.middleware?.includes('authenticate')) {
pythonCode += ` 'Authorization': 'Bearer YOUR_TOKEN',\n`;
}
pythonCode += `}\n\n`;
// Query parameters
if (queryParams.length > 0) {
pythonCode += `params = {\n`;
queryParams.forEach(param => {
pythonCode += ` '${param.name}': 'example_value',\n`;
});
pythonCode += `}\n\n`;
}
// Body data
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
const bodyExample = this.generateBodyExample(bodyParams);
pythonCode += `data = ${bodyExample.replace(/"/g, "'")}\n\n`;
}
// Make request
pythonCode += `response = requests.${endpoint.method.toLowerCase()}(\n`;
pythonCode += ` url,\n`;
pythonCode += ` headers=headers,\n`;
if (queryParams.length > 0) {
pythonCode += ` params=params,\n`;
}
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
pythonCode += ` json=data,\n`;
}
pythonCode += `)\n\n`;
pythonCode += `print(response.status_code)\n`;
pythonCode += `print(response.json())`;
return pythonCode;
}
generateBodyExample(bodyParams) {
if (!bodyParams || bodyParams.length === 0) {
// Generate a simple example based on common patterns
return JSON.stringify({
name: "example_name",
description: "example_description"
});
}
const example = {};
bodyParams.forEach(param => {
switch (param.type) {
case 'string':
example[param.name] = `example_${param.name}`;
break;
case 'integer':
case 'number':
example[param.name] = 123;
break;
case 'boolean':
example[param.name] = true;
break;
case 'array':
example[param.name] = ['item1', 'item2'];
break;
default:
example[param.name] = `example_${param.name}`;
}
});
return JSON.stringify(example, null, 2);
}
}
module.exports = Generator;