UNPKG

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.

310 lines (243 loc) 9.14 kB
const fs = require('fs-extra'); const path = require('path'); class FastAPIParser { constructor(options = {}) { this.options = options; } async parseFile(filePath) { const content = await fs.readFile(filePath, 'utf8'); const results = { endpoints: [], schemas: [] }; try { // Parse FastAPI decorators and functions results.endpoints = this.extractFastAPIEndpoints(content, filePath); results.schemas = this.extractPydanticModels(content, filePath); } catch (error) { console.warn(`⚠️ Failed to parse FastAPI file ${filePath}:`, error.message); } return results; } extractFastAPIEndpoints(content, filePath) { const endpoints = []; const lines = content.split('\n'); // Regex patterns for FastAPI decorators const patterns = { get: /@app\.get\s*\(\s*["']([^"']+)["']/g, post: /@app\.post\s*\(\s*["']([^"']+)["']/g, put: /@app\.put\s*\(\s*["']([^"']+)["']/g, delete: /@app\.delete\s*\(\s*["']([^"']+)["']/g, patch: /@app\.patch\s*\(\s*["']([^"']+)["']/g }; for (const [method, pattern] of Object.entries(patterns)) { let match; pattern.lastIndex = 0; // Reset regex while ((match = pattern.exec(content)) !== null) { const path = match[1]; const lineNumber = this.getLineNumber(content, match.index); // Extract function definition following the decorator const functionInfo = this.extractFunctionInfo(lines, lineNumber); const endpoint = { method: method.toUpperCase(), path, file: filePath, line: lineNumber, description: functionInfo.docstring, parameters: this.extractParametersFromFunction(functionInfo), response: this.extractResponseInfo(functionInfo), tags: this.extractTags(lines, lineNumber - 1) }; endpoints.push(endpoint); } } return endpoints; } extractFunctionInfo(lines, decoratorLine) { let functionLine = decoratorLine; // Find the function definition after the decorator while (functionLine < lines.length && !lines[functionLine].trim().startsWith('def ')) { functionLine++; } if (functionLine >= lines.length) { return { name: null, params: [], docstring: null }; } const functionDef = lines[functionLine].trim(); const functionMatch = functionDef.match(/def\s+(\w+)\s*\(([^)]*)\)/); if (!functionMatch) { return { name: null, params: [], docstring: null }; } const name = functionMatch[1]; const params = this.parseParameterString(functionMatch[2]); const docstring = this.extractDocstring(lines, functionLine + 1); return { name, params, docstring, startLine: functionLine }; } parseParameterString(paramString) { if (!paramString.trim()) return []; const params = []; const paramList = paramString.split(','); for (const param of paramList) { const trimmed = param.trim(); if (trimmed) { const [name, type] = this.parseParameter(trimmed); params.push({ name, type }); } } return params; } parseParameter(paramStr) { // Handle type hints: param: int = None const typeMatch = paramStr.match(/(\w+):\s*([^=]+)(?:\s*=\s*(.+))?/); if (typeMatch) { const name = typeMatch[1]; const type = typeMatch[2].trim(); const defaultValue = typeMatch[3]; return [name, { type, default: defaultValue, required: !defaultValue }]; } // Simple parameter without type hint const simpleMatch = paramStr.match(/(\w+)(?:\s*=\s*(.+))?/); if (simpleMatch) { const name = simpleMatch[1]; const defaultValue = simpleMatch[2]; return [name, { type: 'any', default: defaultValue, required: !defaultValue }]; } return [paramStr.trim(), { type: 'any', required: true }]; } extractParametersFromFunction(functionInfo) { const parameters = []; for (const param of functionInfo.params) { if (param.name === 'self') continue; // Skip self parameter parameters.push({ name: param.name, type: param.type?.type || 'string', required: param.type?.required !== false, default: param.type?.default, in: this.determineParameterLocation(param.name, param.type) }); } return parameters; } determineParameterLocation(name, type) { // Simple heuristics for parameter location if (type?.type?.includes('Query')) return 'query'; if (type?.type?.includes('Path')) return 'path'; if (type?.type?.includes('Body')) return 'body'; if (type?.type?.includes('Header')) return 'header'; // Default based on common patterns if (name.includes('id') || name.includes('Id')) return 'path'; return 'query'; } extractDocstring(lines, startLine) { if (startLine >= lines.length) return null; const line = lines[startLine].trim(); // Check for triple quotes if (line.startsWith('"""') || line.startsWith("'''")) { const quote = line.startsWith('"""') ? '"""' : "'''"; let docstring = ''; let currentLine = startLine; // Single line docstring if (line.length > 3 && line.endsWith(quote)) { return line.slice(3, -3).trim(); } // Multi-line docstring currentLine++; while (currentLine < lines.length) { const currentLineText = lines[currentLine]; if (currentLineText.trim().endsWith(quote)) { docstring += currentLineText.replace(quote, '').trim(); break; } docstring += currentLineText.trim() + '\n'; currentLine++; } return docstring.trim(); } return null; } extractPydanticModels(content, filePath) { const models = []; const lines = content.split('\n'); // Look for Pydantic model classes const classPattern = /class\s+(\w+)\s*\(\s*BaseModel\s*\):/g; let match; while ((match = classPattern.exec(content)) !== null) { const className = match[1]; const lineNumber = this.getLineNumber(content, match.index); const model = { name: className, type: 'object', file: filePath, line: lineNumber, properties: this.extractModelProperties(lines, lineNumber) }; models.push(model); } return models; } extractModelProperties(lines, classLine) { const properties = {}; let currentLine = classLine + 1; while (currentLine < lines.length) { const line = lines[currentLine].trim(); // Stop at next class or function definition if (line.startsWith('class ') || line.startsWith('def ') || (line && !line.startsWith(' ') && !line.startsWith('\t') && line !== '')) { break; } // Parse property definition const propMatch = line.match(/(\w+):\s*([^=]+)(?:\s*=\s*(.+))?/); if (propMatch) { const name = propMatch[1]; const type = propMatch[2].trim(); const defaultValue = propMatch[3]; properties[name] = { type: this.pythonTypeToOpenAPI(type), required: !defaultValue, default: defaultValue }; } currentLine++; } return properties; } pythonTypeToOpenAPI(pythonType) { const typeMap = { 'str': 'string', 'int': 'integer', 'float': 'number', 'bool': 'boolean', 'list': 'array', 'dict': 'object', 'List': 'array', 'Dict': 'object', 'Optional': 'string', // Simplified 'Union': 'string' // Simplified }; return typeMap[pythonType] || 'string'; } extractTags(lines, decoratorLine) { // Look for tags in decorator parameters if (decoratorLine >= 0 && decoratorLine < lines.length) { const decoratorLine = lines[decoratorLine]; const tagsMatch = decoratorLine.match(/tags\s*=\s*\[([^\]]+)\]/); if (tagsMatch) { return tagsMatch[1].split(',').map(tag => tag.trim().replace(/['"]/g, '')); } } return []; } extractResponseInfo(functionInfo) { // Simple response extraction from docstring if (functionInfo.docstring) { const returnMatch = functionInfo.docstring.match(/Returns?:\s*(.+)/i); if (returnMatch) { return { description: returnMatch[1].trim() }; } } return { description: 'Successful response' }; } getLineNumber(content, index) { return content.substring(0, index).split('\n').length; } } module.exports = FastAPIParser;