@g1cloud/api-gen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +22 -0
- package/CLAUDE.md +63 -0
- package/README.md +379 -0
- package/dist/analyzer/controllerAnalyzer.d.ts +20 -0
- package/dist/analyzer/controllerAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/controllerAnalyzer.js +101 -0
- package/dist/analyzer/controllerAnalyzer.js.map +1 -0
- package/dist/analyzer/parameterAnalyzer.d.ts +19 -0
- package/dist/analyzer/parameterAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/parameterAnalyzer.js +207 -0
- package/dist/analyzer/parameterAnalyzer.js.map +1 -0
- package/dist/analyzer/responseAnalyzer.d.ts +12 -0
- package/dist/analyzer/responseAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/responseAnalyzer.js +116 -0
- package/dist/analyzer/responseAnalyzer.js.map +1 -0
- package/dist/analyzer/schemaGenerator.d.ts +6 -0
- package/dist/analyzer/schemaGenerator.d.ts.map +1 -0
- package/dist/analyzer/schemaGenerator.js +347 -0
- package/dist/analyzer/schemaGenerator.js.map +1 -0
- package/dist/analyzer/securityAnalyzer.d.ts +6 -0
- package/dist/analyzer/securityAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/securityAnalyzer.js +177 -0
- package/dist/analyzer/securityAnalyzer.js.map +1 -0
- package/dist/generator/openapiGenerator.d.ts +14 -0
- package/dist/generator/openapiGenerator.d.ts.map +1 -0
- package/dist/generator/openapiGenerator.js +340 -0
- package/dist/generator/openapiGenerator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +61 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +199 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +257 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp-server.mjs +45586 -0
- package/dist/parser/astAnalyzer.d.ts +87 -0
- package/dist/parser/astAnalyzer.d.ts.map +1 -0
- package/dist/parser/astAnalyzer.js +321 -0
- package/dist/parser/astAnalyzer.js.map +1 -0
- package/dist/parser/javaParser.d.ts +10 -0
- package/dist/parser/javaParser.d.ts.map +1 -0
- package/dist/parser/javaParser.js +805 -0
- package/dist/parser/javaParser.js.map +1 -0
- package/dist/types/index.d.ts +217 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/examples/CreateUserRequest.java +80 -0
- package/examples/DepartmentDTO.java +45 -0
- package/examples/Filter.java +39 -0
- package/examples/PaginatedList.java +71 -0
- package/examples/ProductController.java +136 -0
- package/examples/ProductDTO.java +129 -0
- package/examples/RoleDTO.java +47 -0
- package/examples/SearchParam.java +55 -0
- package/examples/Sort.java +70 -0
- package/examples/UpdateUserRequest.java +74 -0
- package/examples/UserController.java +98 -0
- package/examples/UserDTO.java +119 -0
- package/package.json +51 -0
- package/prompt/01_Initial.md +358 -0
- package/prompt/02_/354/266/224/352/260/200.md +31 -0
- package/src/analyzer/controllerAnalyzer.ts +125 -0
- package/src/analyzer/parameterAnalyzer.ts +259 -0
- package/src/analyzer/responseAnalyzer.ts +142 -0
- package/src/analyzer/schemaGenerator.ts +412 -0
- package/src/analyzer/securityAnalyzer.ts +200 -0
- package/src/generator/openapiGenerator.ts +378 -0
- package/src/index.ts +212 -0
- package/src/lib.ts +240 -0
- package/src/mcp-server.ts +310 -0
- package/src/parser/astAnalyzer.ts +373 -0
- package/src/parser/javaParser.ts +901 -0
- package/src/types/index.ts +238 -0
- package/test-boolean.yaml +607 -0
- package/test-filter.yaml +576 -0
- package/test-inner.ts +59 -0
- package/test-output.yaml +650 -0
- package/test-paginated.yaml +585 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +30 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import {
|
|
4
|
+
CLIOptions,
|
|
5
|
+
OpenAPISpec,
|
|
6
|
+
ControllerInfo,
|
|
7
|
+
EndpointInfo,
|
|
8
|
+
ProcessingContext,
|
|
9
|
+
PathItem,
|
|
10
|
+
OperationObject,
|
|
11
|
+
ParameterObject,
|
|
12
|
+
RequestBodyObject,
|
|
13
|
+
ResponseObject,
|
|
14
|
+
SchemaInfo,
|
|
15
|
+
TagInfo,
|
|
16
|
+
} from '../types';
|
|
17
|
+
import { generateSchemas } from '../analyzer/schemaGenerator';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate OpenAPI YAML from analyzed controllers
|
|
21
|
+
*/
|
|
22
|
+
export function generateOpenAPISpec(
|
|
23
|
+
controllers: ControllerInfo[],
|
|
24
|
+
context: ProcessingContext,
|
|
25
|
+
options: CLIOptions
|
|
26
|
+
): OpenAPISpec {
|
|
27
|
+
// Generate schemas for all referenced types
|
|
28
|
+
const schemas = generateSchemas(context);
|
|
29
|
+
|
|
30
|
+
// Build tags from controllers
|
|
31
|
+
const tags: TagInfo[] = controllers.map(controller => ({
|
|
32
|
+
name: controller.className,
|
|
33
|
+
description: controller.javaClass.javadoc || `Endpoints for ${controller.className}`,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Build paths
|
|
37
|
+
const paths: Record<string, PathItem> = {};
|
|
38
|
+
|
|
39
|
+
for (const controller of controllers) {
|
|
40
|
+
for (const endpoint of controller.endpoints) {
|
|
41
|
+
const path = endpoint.path;
|
|
42
|
+
|
|
43
|
+
if (!paths[path]) {
|
|
44
|
+
paths[path] = {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
paths[path][endpoint.method] = buildOperation(endpoint, controller.className);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Convert schema Map to object
|
|
52
|
+
const schemaObject: Record<string, SchemaInfo> = {};
|
|
53
|
+
for (const [name, schema] of schemas) {
|
|
54
|
+
schemaObject[name] = schema;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build the spec
|
|
58
|
+
const spec: OpenAPISpec = {
|
|
59
|
+
openapi: '3.0.0',
|
|
60
|
+
info: {
|
|
61
|
+
title: options.title,
|
|
62
|
+
version: options.version,
|
|
63
|
+
},
|
|
64
|
+
tags,
|
|
65
|
+
paths,
|
|
66
|
+
components: {
|
|
67
|
+
schemas: schemaObject,
|
|
68
|
+
securitySchemes: {
|
|
69
|
+
bearerAuth: {
|
|
70
|
+
type: 'http',
|
|
71
|
+
scheme: 'bearer',
|
|
72
|
+
bearerFormat: 'JWT',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Add servers if base path is specified
|
|
79
|
+
if (options.basePath) {
|
|
80
|
+
spec.servers = [{ url: `http://localhost:8080${options.basePath}` }];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return spec;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build operation object for an endpoint
|
|
88
|
+
*/
|
|
89
|
+
function buildOperation(endpoint: EndpointInfo, controllerName?: string): OperationObject {
|
|
90
|
+
const operation: OperationObject = {
|
|
91
|
+
summary: endpoint.summary,
|
|
92
|
+
operationId: endpoint.operationId,
|
|
93
|
+
responses: {},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Add tag for controller grouping
|
|
97
|
+
if (controllerName) {
|
|
98
|
+
operation.tags = [controllerName];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Build description with javadoc, @return, and security info
|
|
102
|
+
const descriptionParts: string[] = [];
|
|
103
|
+
|
|
104
|
+
if (endpoint.description) {
|
|
105
|
+
descriptionParts.push(endpoint.description);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add @return description from javadoc
|
|
109
|
+
if (endpoint.returnDescription) {
|
|
110
|
+
descriptionParts.push(`\n\n**Returns:** ${endpoint.returnDescription}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add security info to description
|
|
114
|
+
if (endpoint.security.roles.length > 0) {
|
|
115
|
+
descriptionParts.push(`\n\n**Required Roles:** ${endpoint.security.roles.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (endpoint.security.securityExpression) {
|
|
119
|
+
descriptionParts.push(`\n\n**Security Expression:** \`${endpoint.security.securityExpression}\``);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (descriptionParts.length > 0) {
|
|
123
|
+
operation.description = descriptionParts.join('');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add parameters
|
|
127
|
+
if (endpoint.parameters.length > 0) {
|
|
128
|
+
operation.parameters = endpoint.parameters.map(buildParameter);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add request body
|
|
132
|
+
if (endpoint.requestBody) {
|
|
133
|
+
operation.requestBody = buildRequestBody(endpoint.requestBody);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add responses
|
|
137
|
+
for (const response of endpoint.responses) {
|
|
138
|
+
operation.responses[response.statusCode] = buildResponse(response);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add security
|
|
142
|
+
if (endpoint.security.roles.length > 0 || endpoint.security.authorities.length > 0) {
|
|
143
|
+
operation.security = [{ bearerAuth: [] }];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Add x-required-roles
|
|
147
|
+
if (endpoint.security.roles.length > 0) {
|
|
148
|
+
operation['x-required-roles'] = endpoint.security.roles;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add x-security-expression for complex expressions
|
|
152
|
+
if (endpoint.security.hasComplexExpression && endpoint.security.securityExpression) {
|
|
153
|
+
operation['x-security-expression'] = endpoint.security.securityExpression;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return operation;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build parameter object
|
|
161
|
+
*/
|
|
162
|
+
function buildParameter(param: import('../types').ParameterInfo): ParameterObject {
|
|
163
|
+
const parameter: ParameterObject = {
|
|
164
|
+
name: param.name,
|
|
165
|
+
in: param.in,
|
|
166
|
+
schema: param.schema || { type: param.type },
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (param.required) {
|
|
170
|
+
parameter.required = true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (param.description) {
|
|
174
|
+
parameter.description = param.description;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return parameter;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Build request body object
|
|
182
|
+
*/
|
|
183
|
+
function buildRequestBody(requestBody: import('../types').RequestBodyInfo): RequestBodyObject {
|
|
184
|
+
const body: RequestBodyObject = {
|
|
185
|
+
required: requestBody.required,
|
|
186
|
+
content: {
|
|
187
|
+
[requestBody.contentType]: {
|
|
188
|
+
schema: requestBody.schema,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Add description from @param javadoc if available
|
|
194
|
+
if (requestBody.description) {
|
|
195
|
+
body.description = requestBody.description;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return body;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build response object
|
|
203
|
+
*/
|
|
204
|
+
function buildResponse(response: import('../types').ResponseInfo): ResponseObject {
|
|
205
|
+
const responseObj: ResponseObject = {
|
|
206
|
+
description: response.description,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Add headers
|
|
210
|
+
if (response.headers) {
|
|
211
|
+
responseObj.headers = {};
|
|
212
|
+
for (const [name, header] of Object.entries(response.headers)) {
|
|
213
|
+
responseObj.headers[name] = {
|
|
214
|
+
schema: header.schema,
|
|
215
|
+
description: header.description,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Add content
|
|
221
|
+
if (response.contentType && response.schema) {
|
|
222
|
+
responseObj.content = {
|
|
223
|
+
[response.contentType]: {
|
|
224
|
+
schema: response.schema,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return responseObj;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Recursively extract all $ref types from a schema
|
|
234
|
+
*/
|
|
235
|
+
function extractRefsFromSchema(schema: any, refs: Set<string>): void {
|
|
236
|
+
if (!schema || typeof schema !== 'object') {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Direct $ref
|
|
241
|
+
if (schema.$ref && typeof schema.$ref === 'string') {
|
|
242
|
+
const typeName = schema.$ref.replace('#/components/schemas/', '');
|
|
243
|
+
refs.add(typeName);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Array items
|
|
247
|
+
if (schema.items) {
|
|
248
|
+
extractRefsFromSchema(schema.items, refs);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Object properties
|
|
252
|
+
if (schema.properties) {
|
|
253
|
+
for (const prop of Object.values(schema.properties)) {
|
|
254
|
+
extractRefsFromSchema(prop, refs);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// additionalProperties
|
|
259
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
260
|
+
extractRefsFromSchema(schema.additionalProperties, refs);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// allOf, anyOf, oneOf
|
|
264
|
+
for (const key of ['allOf', 'anyOf', 'oneOf']) {
|
|
265
|
+
if (Array.isArray(schema[key])) {
|
|
266
|
+
for (const subSchema of schema[key]) {
|
|
267
|
+
extractRefsFromSchema(subSchema, refs);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generate OpenAPI spec for a single controller
|
|
275
|
+
*/
|
|
276
|
+
export function generateOpenAPISpecForController(
|
|
277
|
+
controller: ControllerInfo,
|
|
278
|
+
context: ProcessingContext,
|
|
279
|
+
options: CLIOptions
|
|
280
|
+
): OpenAPISpec {
|
|
281
|
+
// Create a temporary context with only this controller to generate relevant schemas
|
|
282
|
+
const singleControllerContext: ProcessingContext = {
|
|
283
|
+
javaClasses: context.javaClasses,
|
|
284
|
+
controllers: [controller],
|
|
285
|
+
dtoSchemas: new Map(),
|
|
286
|
+
referencedTypes: new Set(),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Collect referenced types from this controller's endpoints
|
|
290
|
+
for (const endpoint of controller.endpoints) {
|
|
291
|
+
// Extract refs from request body schema (including nested refs like array items)
|
|
292
|
+
if (endpoint.requestBody?.schema) {
|
|
293
|
+
extractRefsFromSchema(endpoint.requestBody.schema, singleControllerContext.referencedTypes);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Extract refs from response schemas
|
|
297
|
+
for (const response of endpoint.responses) {
|
|
298
|
+
if (response.schema) {
|
|
299
|
+
extractRefsFromSchema(response.schema, singleControllerContext.referencedTypes);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Extract refs from parameter schemas
|
|
304
|
+
for (const param of endpoint.parameters) {
|
|
305
|
+
if (param.schema) {
|
|
306
|
+
extractRefsFromSchema(param.schema, singleControllerContext.referencedTypes);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Generate schemas for referenced types
|
|
312
|
+
const schemas = generateSchemas(singleControllerContext);
|
|
313
|
+
|
|
314
|
+
// Build paths for this controller only
|
|
315
|
+
const paths: Record<string, PathItem> = {};
|
|
316
|
+
for (const endpoint of controller.endpoints) {
|
|
317
|
+
const path = endpoint.path;
|
|
318
|
+
if (!paths[path]) {
|
|
319
|
+
paths[path] = {};
|
|
320
|
+
}
|
|
321
|
+
paths[path][endpoint.method] = buildOperation(endpoint, controller.className);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Build tag for this controller
|
|
325
|
+
const tags: TagInfo[] = [{
|
|
326
|
+
name: controller.className,
|
|
327
|
+
description: controller.javaClass.javadoc || `Endpoints for ${controller.className}`,
|
|
328
|
+
}];
|
|
329
|
+
|
|
330
|
+
// Convert schema Map to object
|
|
331
|
+
const schemaObject: Record<string, SchemaInfo> = {};
|
|
332
|
+
for (const [name, schema] of schemas) {
|
|
333
|
+
schemaObject[name] = schema;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Build the spec with controller name as title
|
|
337
|
+
const spec: OpenAPISpec = {
|
|
338
|
+
openapi: '3.0.0',
|
|
339
|
+
info: {
|
|
340
|
+
title: controller.className,
|
|
341
|
+
version: options.version,
|
|
342
|
+
},
|
|
343
|
+
tags,
|
|
344
|
+
paths,
|
|
345
|
+
components: {
|
|
346
|
+
schemas: schemaObject,
|
|
347
|
+
securitySchemes: {
|
|
348
|
+
bearerAuth: {
|
|
349
|
+
type: 'http',
|
|
350
|
+
scheme: 'bearer',
|
|
351
|
+
bearerFormat: 'JWT',
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Add servers if base path is specified
|
|
358
|
+
if (options.basePath) {
|
|
359
|
+
spec.servers = [{ url: `http://localhost:8080${options.basePath}` }];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return spec;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Write OpenAPI spec to YAML file
|
|
367
|
+
*/
|
|
368
|
+
export function writeOpenAPISpec(spec: OpenAPISpec, outputPath: string): void {
|
|
369
|
+
const yamlContent = yaml.dump(spec, {
|
|
370
|
+
indent: 2,
|
|
371
|
+
lineWidth: -1,
|
|
372
|
+
noRefs: true,
|
|
373
|
+
sortKeys: false,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
fs.writeFileSync(outputPath, yamlContent, 'utf-8');
|
|
377
|
+
console.log(` Written: ${outputPath}`);
|
|
378
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { CLIOptions, ProcessingContext } from './types';
|
|
7
|
+
import { parseJavaSource } from './parser/javaParser';
|
|
8
|
+
import { analyzeControllers } from './analyzer/controllerAnalyzer';
|
|
9
|
+
import { generateOpenAPISpec, generateOpenAPISpecForController, writeOpenAPISpec } from './generator/openapiGenerator';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
// Helper to collect multiple values for an option
|
|
14
|
+
function collect(value: string, previous: string[]): string[] {
|
|
15
|
+
return previous.concat([value]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('spring-to-openapi')
|
|
20
|
+
.description('Generate OpenAPI v3 YAML from Spring Boot Java source code')
|
|
21
|
+
.version('1.0.0')
|
|
22
|
+
.requiredOption('-s, --source <path...>', 'Java source code directory path(s) (can be specified multiple times)', collect, [])
|
|
23
|
+
.option('-a, --api-source <path>', 'Directory to search for RestControllers (defaults to first --source)')
|
|
24
|
+
.option('-o, --output <path>', 'Output YAML file path (generates single combined file)')
|
|
25
|
+
.option('-d, --out-dir <path>', 'Output directory path (generates separate YAML per controller)')
|
|
26
|
+
.option('-t, --title <title>', 'API title', 'API Documentation')
|
|
27
|
+
.option('--api-version <version>', 'API version', '1.0.0')
|
|
28
|
+
.option('-b, --base-path <path>', 'API base path')
|
|
29
|
+
.action(async (options: any) => {
|
|
30
|
+
try {
|
|
31
|
+
await run(options);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function run(options: any): Promise<void> {
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
// Validate output options
|
|
42
|
+
if (!options.output && !options.outDir) {
|
|
43
|
+
options.output = 'openapi.yaml'; // default to single file
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.output && options.outDir) {
|
|
47
|
+
console.error('Error: Cannot use both --output and --out-dir. Choose one output mode.');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Normalize source to array
|
|
52
|
+
const sources: string[] = Array.isArray(options.source) ? options.source : [options.source];
|
|
53
|
+
|
|
54
|
+
if (sources.length === 0) {
|
|
55
|
+
console.error('Error: At least one --source path is required.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Normalize options
|
|
60
|
+
const cliOptions: CLIOptions = {
|
|
61
|
+
source: sources,
|
|
62
|
+
apiSource: options.apiSource, // Optional: specific directory for RestController search
|
|
63
|
+
output: options.output,
|
|
64
|
+
outDir: options.outDir,
|
|
65
|
+
title: options.title,
|
|
66
|
+
version: options.apiVersion,
|
|
67
|
+
basePath: options.basePath,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const isDirectoryMode = !!cliOptions.outDir;
|
|
71
|
+
|
|
72
|
+
console.log('='.repeat(60));
|
|
73
|
+
console.log('Spring Boot to OpenAPI Generator');
|
|
74
|
+
console.log('='.repeat(60));
|
|
75
|
+
if (cliOptions.source.length === 1) {
|
|
76
|
+
console.log(`\nSource directory: ${cliOptions.source[0]}`);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`\nSource directories:`);
|
|
79
|
+
for (const src of cliOptions.source) {
|
|
80
|
+
console.log(` - ${src}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (cliOptions.apiSource) {
|
|
84
|
+
console.log(`API source directory: ${cliOptions.apiSource} (RestController search)`);
|
|
85
|
+
}
|
|
86
|
+
if (isDirectoryMode) {
|
|
87
|
+
console.log(`Output directory: ${cliOptions.outDir} (per-controller mode)`);
|
|
88
|
+
} else {
|
|
89
|
+
console.log(`Output file: ${cliOptions.output}`);
|
|
90
|
+
}
|
|
91
|
+
console.log(`API title: ${cliOptions.title}`);
|
|
92
|
+
console.log(`API version: ${cliOptions.version}`);
|
|
93
|
+
if (cliOptions.basePath) {
|
|
94
|
+
console.log(`Base path: ${cliOptions.basePath}`);
|
|
95
|
+
}
|
|
96
|
+
console.log();
|
|
97
|
+
|
|
98
|
+
// Resolve paths
|
|
99
|
+
const sourcePaths = cliOptions.source.map(s => path.resolve(s));
|
|
100
|
+
const apiSourcePath = cliOptions.apiSource ? path.resolve(cliOptions.apiSource) : undefined;
|
|
101
|
+
|
|
102
|
+
// Step 1: Parse Java files from all source directories
|
|
103
|
+
console.log('Step 1: Parsing Java source files...');
|
|
104
|
+
const javaClasses = new Map<string, any>();
|
|
105
|
+
|
|
106
|
+
for (const sourcePath of sourcePaths) {
|
|
107
|
+
const classes = await parseJavaSource(sourcePath);
|
|
108
|
+
console.log(` Found ${classes.size} class(es) in ${sourcePath}`);
|
|
109
|
+
// Merge classes (later sources override earlier ones for same class name)
|
|
110
|
+
for (const [name, cls] of classes) {
|
|
111
|
+
javaClasses.set(name, cls);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log(` Total: ${javaClasses.size} Java class(es)`);
|
|
115
|
+
|
|
116
|
+
if (javaClasses.size === 0) {
|
|
117
|
+
console.warn('Warning: No Java files found in the specified directory.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Step 2: Initialize processing context
|
|
122
|
+
const context: ProcessingContext = {
|
|
123
|
+
javaClasses,
|
|
124
|
+
controllers: [],
|
|
125
|
+
dtoSchemas: new Map(),
|
|
126
|
+
referencedTypes: new Set(),
|
|
127
|
+
apiSourcePath, // Pass apiSourcePath to filter RestControllers
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Step 3: Analyze controllers
|
|
131
|
+
console.log('\nStep 2: Analyzing REST controllers...');
|
|
132
|
+
const controllers = analyzeControllers(context);
|
|
133
|
+
context.controllers = controllers;
|
|
134
|
+
|
|
135
|
+
const totalEndpoints = controllers.reduce((sum, c) => sum + c.endpoints.length, 0);
|
|
136
|
+
console.log(`\n Found ${controllers.length} controller(s) with ${totalEndpoints} endpoint(s)`);
|
|
137
|
+
|
|
138
|
+
if (controllers.length === 0) {
|
|
139
|
+
console.warn('Warning: No REST controllers found in the source files.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Step 4: Generate and write OpenAPI spec(s)
|
|
144
|
+
console.log('\nStep 3: Generating OpenAPI specification...');
|
|
145
|
+
|
|
146
|
+
if (isDirectoryMode) {
|
|
147
|
+
// Directory mode: generate separate YAML file per controller
|
|
148
|
+
const outDirPath = path.resolve(cliOptions.outDir!);
|
|
149
|
+
|
|
150
|
+
// Create output directory if it doesn't exist
|
|
151
|
+
if (!fs.existsSync(outDirPath)) {
|
|
152
|
+
fs.mkdirSync(outDirPath, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('\nStep 4: Writing YAML files (per controller)...');
|
|
156
|
+
let totalSchemas = 0;
|
|
157
|
+
|
|
158
|
+
for (const controller of controllers) {
|
|
159
|
+
const spec = generateOpenAPISpecForController(controller, context, cliOptions);
|
|
160
|
+
const fileName = `${controller.className}.yaml`;
|
|
161
|
+
const outputPath = path.join(outDirPath, fileName);
|
|
162
|
+
writeOpenAPISpec(spec, outputPath);
|
|
163
|
+
totalSchemas += Object.keys(spec.components.schemas).length;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const endTime = Date.now();
|
|
167
|
+
console.log(`\nCompleted in ${endTime - startTime}ms`);
|
|
168
|
+
console.log('='.repeat(60));
|
|
169
|
+
|
|
170
|
+
// Print summary
|
|
171
|
+
console.log('\nSummary:');
|
|
172
|
+
console.log(` Controllers: ${controllers.length}`);
|
|
173
|
+
console.log(` Endpoints: ${totalEndpoints}`);
|
|
174
|
+
console.log(` Files generated: ${controllers.length}`);
|
|
175
|
+
|
|
176
|
+
// Print generated files
|
|
177
|
+
console.log('\nGenerated files:');
|
|
178
|
+
for (const controller of controllers) {
|
|
179
|
+
const endpointCount = controller.endpoints.length;
|
|
180
|
+
console.log(` ${controller.className}.yaml (${endpointCount} endpoint${endpointCount !== 1 ? 's' : ''})`);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Single file mode: generate combined YAML
|
|
184
|
+
const spec = generateOpenAPISpec(controllers, context, cliOptions);
|
|
185
|
+
const outputPath = path.resolve(cliOptions.output!);
|
|
186
|
+
|
|
187
|
+
console.log('\nStep 4: Writing YAML output...');
|
|
188
|
+
writeOpenAPISpec(spec, outputPath);
|
|
189
|
+
|
|
190
|
+
const endTime = Date.now();
|
|
191
|
+
console.log(`\nCompleted in ${endTime - startTime}ms`);
|
|
192
|
+
console.log('='.repeat(60));
|
|
193
|
+
|
|
194
|
+
// Print summary
|
|
195
|
+
console.log('\nSummary:');
|
|
196
|
+
console.log(` Controllers: ${controllers.length}`);
|
|
197
|
+
console.log(` Endpoints: ${totalEndpoints}`);
|
|
198
|
+
console.log(` Schemas: ${Object.keys(spec.components.schemas).length}`);
|
|
199
|
+
|
|
200
|
+
// Print endpoints by path
|
|
201
|
+
console.log('\nEndpoints:');
|
|
202
|
+
const paths = Object.keys(spec.paths).sort();
|
|
203
|
+
for (const p of paths) {
|
|
204
|
+
const methods = Object.keys(spec.paths[p]).sort();
|
|
205
|
+
for (const method of methods) {
|
|
206
|
+
console.log(` ${method.toUpperCase().padEnd(7)} ${p}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
program.parse();
|