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.
385 lines (311 loc) • 11.1 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
class SpringParser {
constructor(options = {}) {
this.options = options;
}
async parseFile(filePath) {
const content = await fs.readFile(filePath, 'utf8');
const results = {
endpoints: [],
schemas: []
};
try {
// Parse Spring Boot annotations and methods
results.endpoints = this.extractSpringEndpoints(content, filePath);
results.schemas = this.extractJavaModels(content, filePath);
} catch (error) {
console.warn(`⚠️ Failed to parse Spring file ${filePath}:`, error.message);
}
return results;
}
extractSpringEndpoints(content, filePath) {
const endpoints = [];
const lines = content.split('\n');
// Find controller classes
const controllerMatch = content.match(/|/);
if (!controllerMatch) return endpoints;
// Extract base path from class-level RequestMapping
const classBasePath = this.extractClassBasePath(content);
// Find method-level mappings
const methodPatterns = {
'@GetMapping': 'GET',
'@PostMapping': 'POST',
'@PutMapping': 'PUT',
'@DeleteMapping': 'DELETE',
'@PatchMapping': 'PATCH',
'@RequestMapping': null // Will be determined from method attribute
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
for (const [annotation, httpMethod] of Object.entries(methodPatterns)) {
if (line.includes(annotation)) {
const endpoint = this.parseEndpointAnnotation(
lines, i, annotation, httpMethod, classBasePath, filePath
);
if (endpoint) {
endpoints.push(endpoint);
}
}
}
}
return endpoints;
}
extractClassBasePath(content) {
const classMapping = content.match(/\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/);
return classMapping ? classMapping[1] : '';
}
parseEndpointAnnotation(lines, annotationLine, annotation, httpMethod, basePath, filePath) {
const line = lines[annotationLine].trim();
// Extract path from annotation
let path = '';
const pathMatch = line.match(/\(\s*(?:value\s*=\s*)?["']([^"']+)["']/);
if (pathMatch) {
path = pathMatch[1];
}
// Handle @RequestMapping with method attribute
if (annotation === '@RequestMapping') {
const methodMatch = line.match(/method\s*=\s*RequestMethod\.(\w+)/);
if (methodMatch) {
httpMethod = methodMatch[1];
} else {
httpMethod = 'GET'; // Default
}
}
// Combine base path and method path
const fullPath = this.combinePaths(basePath, path);
// Find the method definition
const methodInfo = this.extractMethodInfo(lines, annotationLine + 1);
if (!methodInfo.name) return null;
return {
method: httpMethod,
path: fullPath,
file: filePath,
line: annotationLine + 1,
description: this.extractJavaDocComment(lines, annotationLine - 1),
parameters: this.extractMethodParameters(methodInfo),
produces: this.extractProduces(line),
consumes: this.extractConsumes(line),
methodName: methodInfo.name,
returnType: methodInfo.returnType
};
}
extractMethodInfo(lines, startLine) {
let methodLine = startLine;
// Find the method definition
while (methodLine < lines.length) {
const line = lines[methodLine].trim();
// Skip empty lines and annotations
if (!line || line.startsWith('@')) {
methodLine++;
continue;
}
// Look for method signature
const methodMatch = line.match(/(?:public|private|protected)?\s*(\w+(?:<[^>]+>)?)\s+(\w+)\s*\(([^)]*)\)/);
if (methodMatch) {
const returnType = methodMatch[1];
const name = methodMatch[2];
const params = methodMatch[3];
return {
returnType,
name,
parameters: this.parseJavaParameters(params),
startLine: methodLine
};
}
methodLine++;
// Don't search too far
if (methodLine - startLine > 5) break;
}
return { name: null, returnType: null, parameters: [] };
}
parseJavaParameters(paramString) {
if (!paramString.trim()) return [];
const params = [];
const paramList = paramString.split(',');
for (const param of paramList) {
const trimmed = param.trim();
if (trimmed) {
const paramInfo = this.parseJavaParameter(trimmed);
if (paramInfo) {
params.push(paramInfo);
}
}
}
return params;
}
parseJavaParameter(paramStr) {
// Handle annotations like @RequestParam, @PathVariable, @RequestBody
const annotations = [];
let cleanParam = paramStr;
// Extract annotations
const annotationMatches = paramStr.match(/@\w+(?:\([^)]*\))?/g);
if (annotationMatches) {
annotations.push(...annotationMatches);
cleanParam = paramStr.replace(/@\w+(?:\([^)]*\))?/g, '').trim();
}
// Parse type and name
const typeNameMatch = cleanParam.match(/(\w+(?:<[^>]+>)?)\s+(\w+)/);
if (!typeNameMatch) return null;
const type = typeNameMatch[1];
const name = typeNameMatch[2];
return {
name,
type,
annotations,
in: this.determineParameterLocation(annotations),
required: this.isParameterRequired(annotations)
};
}
determineParameterLocation(annotations) {
for (const annotation of annotations) {
if (annotation.includes('@PathVariable')) return 'path';
if (annotation.includes('@RequestParam')) return 'query';
if (annotation.includes('@RequestBody')) return 'body';
if (annotation.includes('@RequestHeader')) return 'header';
}
return 'query'; // Default
}
isParameterRequired(annotations) {
for (const annotation of annotations) {
if (annotation.includes('required = false')) return false;
}
return true; // Default to required
}
extractMethodParameters(methodInfo) {
return methodInfo.parameters.map(param => ({
name: param.name,
type: this.javaTypeToOpenAPI(param.type),
in: param.in,
required: param.required,
description: `${param.type} parameter`
}));
}
extractProduces(line) {
const producesMatch = line.match(/produces\s*=\s*["']([^"']+)["']/);
return producesMatch ? [producesMatch[1]] : ['application/json'];
}
extractConsumes(line) {
const consumesMatch = line.match(/consumes\s*=\s*["']([^"']+)["']/);
return consumesMatch ? [consumesMatch[1]] : ['application/json'];
}
extractJavaDocComment(lines, startLine) {
// Look backwards for JavaDoc comments
let currentLine = startLine;
const commentLines = [];
while (currentLine >= 0) {
const line = lines[currentLine].trim();
if (line === '*/') {
// End of JavaDoc, start collecting
currentLine--;
continue;
}
if (line.startsWith('/**')) {
// Start of JavaDoc, we're done
break;
}
if (line.startsWith('*')) {
commentLines.unshift(line.substring(1).trim());
}
currentLine--;
// Don't search too far back
if (startLine - currentLine > 10) break;
}
return commentLines.length > 0 ? commentLines.join('\n') : null;
}
extractJavaModels(content, filePath) {
const models = [];
const lines = content.split('\n');
// Look for entity classes, DTOs, etc.
const classPattern = /(?:||class\s+\w+.*DTO|class\s+\w+.*Entity|class\s+\w+.*Model)/g;
let match;
while ((match = classPattern.exec(content)) !== null) {
const lineNumber = this.getLineNumber(content, match.index);
const classInfo = this.extractJavaClassInfo(lines, lineNumber);
if (classInfo.name) {
const model = {
name: classInfo.name,
type: 'object',
file: filePath,
line: lineNumber,
properties: this.extractClassProperties(lines, lineNumber)
};
models.push(model);
}
}
return models;
}
extractJavaClassInfo(lines, startLine) {
for (let i = startLine; i < Math.min(startLine + 5, lines.length); i++) {
const line = lines[i].trim();
const classMatch = line.match(/class\s+(\w+)/);
if (classMatch) {
return { name: classMatch[1], line: i };
}
}
return { name: null };
}
extractClassProperties(lines, classLine) {
const properties = {};
let currentLine = classLine + 1;
while (currentLine < lines.length) {
const line = lines[currentLine].trim();
// Stop at next class definition or end of class
if (line.startsWith('class ') || line === '}') {
break;
}
// Look for field declarations
const fieldMatch = line.match(/(?:private|public|protected)?\s*(\w+(?:<[^>]+>)?)\s+(\w+);/);
if (fieldMatch) {
const type = fieldMatch[1];
const name = fieldMatch[2];
properties[name] = {
type: this.javaTypeToOpenAPI(type),
description: `${type} field`
};
}
currentLine++;
}
return properties;
}
javaTypeToOpenAPI(javaType) {
const typeMap = {
'String': 'string',
'Integer': 'integer',
'int': 'integer',
'Long': 'integer',
'long': 'integer',
'Double': 'number',
'double': 'number',
'Float': 'number',
'float': 'number',
'Boolean': 'boolean',
'boolean': 'boolean',
'List': 'array',
'ArrayList': 'array',
'Set': 'array',
'Map': 'object',
'HashMap': 'object',
'Date': 'string',
'LocalDateTime': 'string',
'LocalDate': 'string'
};
// Handle generic types
const genericMatch = javaType.match(/(\w+)<.+>/);
if (genericMatch) {
return typeMap[genericMatch[1]] || 'object';
}
return typeMap[javaType] || 'object';
}
combinePaths(basePath, methodPath) {
if (!basePath) return methodPath || '/';
if (!methodPath) return basePath;
const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
const method = methodPath.startsWith('/') ? methodPath : '/' + methodPath;
return base + method;
}
getLineNumber(content, index) {
return content.substring(0, index).split('\n').length;
}
}
module.exports = SpringParser;