beer-swagger 1.0.1
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/.idea/beer-swaager.iml +8 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/beer-swagger-1.0.1.tgz +0 -0
- package/generate-apis.js +1021 -0
- package/package.json +30 -0
- package/tsconfig.node.json +12 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$" />
|
|
5
|
+
<orderEntry type="inheritedJdk" />
|
|
6
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
7
|
+
</component>
|
|
8
|
+
</module>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/beer-swaager.iml" filepath="$PROJECT_DIR$/.idea/beer-swaager.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/vcs.xml
ADDED
|
Binary file
|
package/generate-apis.js
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
|
|
7
|
+
const ENV_PATH = path.join(process.cwd(), '.env');
|
|
8
|
+
const CLIENT_TYPES = new Set(['WEB', 'WeChatMiniProgram']);
|
|
9
|
+
const SKIPPED_SERVICES = new Set([
|
|
10
|
+
'billbear-common-web-gateway',
|
|
11
|
+
'billbear-common-data-panel'
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const VERBS = [
|
|
15
|
+
'get',
|
|
16
|
+
'list',
|
|
17
|
+
'query',
|
|
18
|
+
'search',
|
|
19
|
+
'find',
|
|
20
|
+
'fetch',
|
|
21
|
+
'create',
|
|
22
|
+
'save',
|
|
23
|
+
'update',
|
|
24
|
+
'set',
|
|
25
|
+
'add',
|
|
26
|
+
'remove',
|
|
27
|
+
'delete',
|
|
28
|
+
'enable',
|
|
29
|
+
'disable',
|
|
30
|
+
'upload',
|
|
31
|
+
'download',
|
|
32
|
+
'export',
|
|
33
|
+
'import',
|
|
34
|
+
'cancel',
|
|
35
|
+
'confirm',
|
|
36
|
+
'apply',
|
|
37
|
+
'check',
|
|
38
|
+
'handle',
|
|
39
|
+
'open',
|
|
40
|
+
'close'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const KEEP_METHOD_NAMES = new Set(['enable', 'disable', 'create', 'search']);
|
|
44
|
+
const SERVICE_NAME_OVERRIDES = {
|
|
45
|
+
authcenter: 'authCenter',
|
|
46
|
+
datatask: 'dataTask',
|
|
47
|
+
usercenter: 'userCenter'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Detect server response wrapper types.
|
|
51
|
+
function isResponseWrapper(name) {
|
|
52
|
+
return /^ResponseData/.test(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Convert string to lower camel case.
|
|
56
|
+
function toCamel(value) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return 'unnamed';
|
|
59
|
+
}
|
|
60
|
+
if (!/[^a-zA-Z0-9]/.test(value) && /[A-Z]/.test(value)) {
|
|
61
|
+
return value[0].toLowerCase() + value.slice(1);
|
|
62
|
+
}
|
|
63
|
+
const parts = value.split(/[^a-zA-Z0-9]+/)
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
if (!parts.length) {
|
|
66
|
+
return 'unnamed';
|
|
67
|
+
}
|
|
68
|
+
const first = parts[0].toLowerCase();
|
|
69
|
+
const rest = parts.slice(1)
|
|
70
|
+
.map((p) => p[0].toUpperCase() + p.slice(1));
|
|
71
|
+
return [first, ...rest].join('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if a name is all lowercase/number characters.
|
|
75
|
+
function isAllLowerWord(value) {
|
|
76
|
+
return /^[a-z0-9]+$/.test(value) && !/[A-Z]/.test(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Derive a method name from the last non-path-parameter segment.
|
|
80
|
+
function pathToName(pathStr) {
|
|
81
|
+
const segments = pathStr.split('/')
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.filter((part) => !part.startsWith('{'));
|
|
84
|
+
if (!segments.length) {
|
|
85
|
+
return 'unnamed';
|
|
86
|
+
}
|
|
87
|
+
const last = segments[segments.length - 1];
|
|
88
|
+
const lowerLast = last.toLowerCase();
|
|
89
|
+
if (lowerLast === 'get' || lowerLast === 'put' || lowerLast === 'delete') {
|
|
90
|
+
const suffix = lowerLast + 'By';
|
|
91
|
+
return toCamel(suffix);
|
|
92
|
+
}
|
|
93
|
+
const normalized = last.replace(/[_-]+/g, ' ');
|
|
94
|
+
return toCamel(normalized);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build suffix from path parameters for collision handling.
|
|
98
|
+
function toParamSuffix(params, nameMap) {
|
|
99
|
+
const names = params.filter((p) => p.in === 'path')
|
|
100
|
+
.map((p) => nameMap?.get(p.name) || p.name);
|
|
101
|
+
if (!names.length) {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
return 'By' + names.map((n) => toPascal(toCamel(n)))
|
|
105
|
+
.join('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add a verb for single-segment paths when needed.
|
|
109
|
+
function addVerbForSingleSegment(name, method) {
|
|
110
|
+
const lower = name.toLowerCase();
|
|
111
|
+
if (KEEP_METHOD_NAMES.has(lower)) {
|
|
112
|
+
return name;
|
|
113
|
+
}
|
|
114
|
+
if (method === 'delete') {
|
|
115
|
+
const suffix = name[0].toUpperCase() + name.slice(1);
|
|
116
|
+
return `removeBy${suffix}`;
|
|
117
|
+
}
|
|
118
|
+
const verbMap = {
|
|
119
|
+
get: 'get',
|
|
120
|
+
post: 'save',
|
|
121
|
+
put: 'save'
|
|
122
|
+
};
|
|
123
|
+
const verb = verbMap[method];
|
|
124
|
+
if (!verb) {
|
|
125
|
+
return name;
|
|
126
|
+
}
|
|
127
|
+
if (name.startsWith(verb)) {
|
|
128
|
+
return name;
|
|
129
|
+
}
|
|
130
|
+
return verb + name[0].toUpperCase() + name.slice(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fix verb casing for names like getflight -> getFlight.
|
|
134
|
+
function fixVerbCase(name) {
|
|
135
|
+
if (/^[a-z0-9]+$/.test(name)) {
|
|
136
|
+
for (const verb of VERBS) {
|
|
137
|
+
if (name.startsWith(verb) && name.length > verb.length) {
|
|
138
|
+
const tail = name.slice(verb.length);
|
|
139
|
+
return verb + tail[0].toUpperCase() + tail.slice(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return name;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Remove DTO suffix from type names.
|
|
147
|
+
function normalizeDtoName(name) {
|
|
148
|
+
return name.replace(/DTO$/, '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get request body type name for disambiguation (strip DTO/Request suffix).
|
|
152
|
+
function getBodyTypeName(requestBody, schemas) {
|
|
153
|
+
if (!requestBody || !requestBody.content) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
const { content } = requestBody;
|
|
157
|
+
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
158
|
+
if (!firstContent || !firstContent.schema) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
const { schema } = firstContent;
|
|
162
|
+
if (!schema.$ref) {
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
const rawRefName = schema.$ref.split('/')
|
|
166
|
+
.pop();
|
|
167
|
+
const normalized = normalizeDtoName(rawRefName);
|
|
168
|
+
const stripped = normalized.replace(/Request$/, '');
|
|
169
|
+
return stripped || normalized;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Map a Swagger schema to a runtime return type.
|
|
173
|
+
function mapSchemaToType(schema, schemas, useTypes) {
|
|
174
|
+
if (!schema) {
|
|
175
|
+
return '{}';
|
|
176
|
+
}
|
|
177
|
+
const {
|
|
178
|
+
$ref,
|
|
179
|
+
type,
|
|
180
|
+
format,
|
|
181
|
+
items
|
|
182
|
+
} = schema;
|
|
183
|
+
if ($ref) {
|
|
184
|
+
const rawRefName = $ref.split('/')
|
|
185
|
+
.pop();
|
|
186
|
+
if (isResponseWrapper(rawRefName)) {
|
|
187
|
+
const responseSchema = schemas[rawRefName];
|
|
188
|
+
if (responseSchema && responseSchema.properties && responseSchema.properties.data) {
|
|
189
|
+
return mapSchemaToType(responseSchema.properties.data, schemas, useTypes);
|
|
190
|
+
}
|
|
191
|
+
return '{}';
|
|
192
|
+
}
|
|
193
|
+
const refName = normalizeDtoName(rawRefName);
|
|
194
|
+
const refSchema = schemas[rawRefName] || schemas[refName];
|
|
195
|
+
if (refSchema && refSchema.properties && refSchema.properties.data) {
|
|
196
|
+
return mapSchemaToType(refSchema.properties.data, schemas, useTypes);
|
|
197
|
+
}
|
|
198
|
+
return useTypes ? `Types.${refName}` : '{}';
|
|
199
|
+
}
|
|
200
|
+
if (type === 'string') {
|
|
201
|
+
return 'string';
|
|
202
|
+
}
|
|
203
|
+
if (type === 'integer') {
|
|
204
|
+
if (format === 'int64' || format === 'long') {
|
|
205
|
+
return 'string';
|
|
206
|
+
}
|
|
207
|
+
return 'number';
|
|
208
|
+
}
|
|
209
|
+
if (type === 'number') {
|
|
210
|
+
return 'number';
|
|
211
|
+
}
|
|
212
|
+
if (type === 'boolean') {
|
|
213
|
+
return 'boolean';
|
|
214
|
+
}
|
|
215
|
+
if (type === 'array') {
|
|
216
|
+
const itemType = mapSchemaToType(items, schemas, useTypes);
|
|
217
|
+
if (itemType === '{}' || itemType === '[]') {
|
|
218
|
+
return '[]';
|
|
219
|
+
}
|
|
220
|
+
return `${itemType}[]`;
|
|
221
|
+
}
|
|
222
|
+
if (type === 'object') {
|
|
223
|
+
return '{}';
|
|
224
|
+
}
|
|
225
|
+
return '{}';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Map parameter schema to a type for method signatures.
|
|
229
|
+
function mapParamSchemaToType(schema, schemas, useTypes) {
|
|
230
|
+
if (!schema) {
|
|
231
|
+
return '{}';
|
|
232
|
+
}
|
|
233
|
+
const {
|
|
234
|
+
$ref,
|
|
235
|
+
type,
|
|
236
|
+
format
|
|
237
|
+
} = schema;
|
|
238
|
+
if ($ref) {
|
|
239
|
+
const refName = normalizeDtoName($ref.split('/')
|
|
240
|
+
.pop());
|
|
241
|
+
return useTypes ? `Types.${refName}` : '{}';
|
|
242
|
+
}
|
|
243
|
+
if (type === 'string') {
|
|
244
|
+
return 'string';
|
|
245
|
+
}
|
|
246
|
+
if (type === 'integer') {
|
|
247
|
+
if (format === 'int64' || format === 'long') {
|
|
248
|
+
return 'string';
|
|
249
|
+
}
|
|
250
|
+
return 'number';
|
|
251
|
+
}
|
|
252
|
+
if (type === 'number') {
|
|
253
|
+
return 'number';
|
|
254
|
+
}
|
|
255
|
+
if (type === 'boolean') {
|
|
256
|
+
return 'boolean';
|
|
257
|
+
}
|
|
258
|
+
if (type === 'array') {
|
|
259
|
+
return '[]';
|
|
260
|
+
}
|
|
261
|
+
if (type === 'object') {
|
|
262
|
+
return '{}';
|
|
263
|
+
}
|
|
264
|
+
return '{}';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Resolve default response type from Swagger responses.
|
|
268
|
+
function getDefaultReturnType(op, schemas, useTypes) {
|
|
269
|
+
const { responses = {} } = op;
|
|
270
|
+
const res = responses['200'] || responses['201'] || responses.default || {};
|
|
271
|
+
const { content = {} } = res;
|
|
272
|
+
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
273
|
+
if (!firstContent) {
|
|
274
|
+
return '{}';
|
|
275
|
+
}
|
|
276
|
+
return mapSchemaToType(firstContent.schema, schemas, useTypes);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Collect query/path parameters.
|
|
280
|
+
function getParams(op) {
|
|
281
|
+
const { parameters = [] } = op;
|
|
282
|
+
const params = Array.isArray(parameters) ? parameters : [];
|
|
283
|
+
return params.filter((p) => p.in === 'query' || p.in === 'path');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Inject path params into the URL template.
|
|
287
|
+
function withPathParams(pathStr, params, nameMap) {
|
|
288
|
+
let out = pathStr;
|
|
289
|
+
for (const p of params) {
|
|
290
|
+
if (p.in === 'path') {
|
|
291
|
+
const mappedName = nameMap?.get(p.name) || p.name;
|
|
292
|
+
out = out.replace(`{${p.name}}`, `\${${mappedName}}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatObjectKey(name) {
|
|
299
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : `'${name}'`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildParamNameMap(params) {
|
|
303
|
+
const used = new Set();
|
|
304
|
+
const map = new Map();
|
|
305
|
+
for (const param of params) {
|
|
306
|
+
const base = toCamel(param.name) || 'param';
|
|
307
|
+
let candidate = base;
|
|
308
|
+
let index = 1;
|
|
309
|
+
while (used.has(candidate)) {
|
|
310
|
+
candidate = `${base}${index}`;
|
|
311
|
+
index += 1;
|
|
312
|
+
}
|
|
313
|
+
used.add(candidate);
|
|
314
|
+
map.set(param.name, candidate);
|
|
315
|
+
}
|
|
316
|
+
return map;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Normalize class name from tag.
|
|
320
|
+
function toClassName(tag) {
|
|
321
|
+
const cleaned = tag.replace(/[^a-zA-Z0-9_]/g, '');
|
|
322
|
+
return cleaned.replace(/Facade$/, '');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Build instance property name for a facade.
|
|
326
|
+
function toPropName(className) {
|
|
327
|
+
if (/^[A-Z0-9]+$/.test(className) && className.length > 1) {
|
|
328
|
+
return className.toLowerCase();
|
|
329
|
+
}
|
|
330
|
+
return toCamel(className);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Strip common suffixes for name disambiguation.
|
|
334
|
+
function toFacadePrefix(className) {
|
|
335
|
+
return className
|
|
336
|
+
.replace(/ClientFacade$/, '')
|
|
337
|
+
.replace(/Facade$/, '')
|
|
338
|
+
.replace(/Client$/, '');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Skip unsupported facades.
|
|
342
|
+
function isSkippedFacade(tag) {
|
|
343
|
+
return tag === 'core-error-controller';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Convert service name to file base name.
|
|
347
|
+
function toFileBase(serviceName) {
|
|
348
|
+
if (serviceName.startsWith('billbear-coral-')) {
|
|
349
|
+
return serviceName.replace('billbear-coral-', '');
|
|
350
|
+
}
|
|
351
|
+
if (serviceName.startsWith('billbear-common-')) {
|
|
352
|
+
return serviceName.replace('billbear-common-', '');
|
|
353
|
+
}
|
|
354
|
+
if (serviceName.startsWith('billbear-')) {
|
|
355
|
+
return serviceName.replace('billbear-', '');
|
|
356
|
+
}
|
|
357
|
+
return serviceName;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Map service name to a stable camel base.
|
|
361
|
+
function toServiceBaseName(serviceName) {
|
|
362
|
+
const fileBase = toFileBase(serviceName);
|
|
363
|
+
const parts = fileBase.split(/[-_]/g)
|
|
364
|
+
.filter(Boolean);
|
|
365
|
+
const mapped = parts.map((part) => {
|
|
366
|
+
const key = part.toLowerCase();
|
|
367
|
+
return SERVICE_NAME_OVERRIDES[key] || part;
|
|
368
|
+
});
|
|
369
|
+
return toCamel(mapped.join('-'));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Uppercase the first letter.
|
|
373
|
+
function toPascal(value) {
|
|
374
|
+
if (!value) {
|
|
375
|
+
return 'Unnamed';
|
|
376
|
+
}
|
|
377
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Generate API file content for a service.
|
|
381
|
+
function generateForSpec(spec, serviceName, fileBase, clientType) {
|
|
382
|
+
const {
|
|
383
|
+
components = {},
|
|
384
|
+
tags: specTags = [],
|
|
385
|
+
paths = {}
|
|
386
|
+
} = spec;
|
|
387
|
+
const defaultPathPrefix = typeof spec.pathPrefix === 'string' ? spec.pathPrefix : '';
|
|
388
|
+
const { schemas = {} } = components;
|
|
389
|
+
const tagDescriptions = {};
|
|
390
|
+
if (Array.isArray(specTags)) {
|
|
391
|
+
for (const tag of specTags) {
|
|
392
|
+
if (tag) {
|
|
393
|
+
const {
|
|
394
|
+
name,
|
|
395
|
+
description
|
|
396
|
+
} = tag;
|
|
397
|
+
if (name && description) {
|
|
398
|
+
tagDescriptions[name] = description.replace(/\n/g, ' ')
|
|
399
|
+
.trim();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const methodsOrder = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
|
|
405
|
+
const byTag = new Map();
|
|
406
|
+
|
|
407
|
+
for (const [p, ops] of Object.entries(paths)) {
|
|
408
|
+
for (const method of methodsOrder) {
|
|
409
|
+
const op = ops[method];
|
|
410
|
+
if (!op) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const {
|
|
414
|
+
tags: opTags,
|
|
415
|
+
operationId,
|
|
416
|
+
summary,
|
|
417
|
+
requestBody
|
|
418
|
+
} = op;
|
|
419
|
+
const tags = opTags && opTags.length ? opTags : ['DefaultFacade'];
|
|
420
|
+
const opId = operationId || summary || `${method}_${p}`;
|
|
421
|
+
let name = pathToName(p);
|
|
422
|
+
if (name === 'unnamed') {
|
|
423
|
+
name = toCamel(opId);
|
|
424
|
+
}
|
|
425
|
+
if (!KEEP_METHOD_NAMES.has(name.toLowerCase()) && isAllLowerWord(name)) {
|
|
426
|
+
const inferred = pathToName(p);
|
|
427
|
+
const pathSegments = p.split('/')
|
|
428
|
+
.filter(Boolean)
|
|
429
|
+
.filter((part) => !part.startsWith('{'));
|
|
430
|
+
name = pathSegments.length === 1 ? addVerbForSingleSegment(inferred, method) : inferred;
|
|
431
|
+
}
|
|
432
|
+
if (!KEEP_METHOD_NAMES.has(name.toLowerCase())) {
|
|
433
|
+
name = fixVerbCase(name);
|
|
434
|
+
}
|
|
435
|
+
const hasBody = !!requestBody;
|
|
436
|
+
const defaultReturnType = getDefaultReturnType(op, schemas, true);
|
|
437
|
+
const params = getParams(op);
|
|
438
|
+
for (const tag of tags) {
|
|
439
|
+
if (isSkippedFacade(tag)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (!tag.endsWith('Facade')) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (!byTag.has(tag)) {
|
|
446
|
+
byTag.set(tag, []);
|
|
447
|
+
}
|
|
448
|
+
byTag.get(tag)
|
|
449
|
+
.push({
|
|
450
|
+
path: p,
|
|
451
|
+
method,
|
|
452
|
+
name,
|
|
453
|
+
hasBody,
|
|
454
|
+
params,
|
|
455
|
+
op,
|
|
456
|
+
defaultReturnType
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const classDefs = [];
|
|
463
|
+
const apiProps = [];
|
|
464
|
+
const apiInit = [];
|
|
465
|
+
|
|
466
|
+
for (const [tag, entries] of byTag.entries()) {
|
|
467
|
+
const seen = new Set();
|
|
468
|
+
const methods = [];
|
|
469
|
+
const className = toClassName(tag);
|
|
470
|
+
const facadePrefix = toFacadePrefix(className);
|
|
471
|
+
const sortedEntries = [...entries].sort((a, b) => {
|
|
472
|
+
const aScore = (a.hasBody ? 1 : 0) + (a.params.length ? 1 : 0);
|
|
473
|
+
const bScore = (b.hasBody ? 1 : 0) + (b.params.length ? 1 : 0);
|
|
474
|
+
return aScore - bScore;
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
for (const entry of sortedEntries) {
|
|
478
|
+
const {
|
|
479
|
+
name: entryName,
|
|
480
|
+
params: entryParams,
|
|
481
|
+
method: entryMethod,
|
|
482
|
+
op: entryOp,
|
|
483
|
+
path: entryPath,
|
|
484
|
+
hasBody: entryHasBody,
|
|
485
|
+
defaultReturnType: entryDefaultReturnType
|
|
486
|
+
} = entry;
|
|
487
|
+
const paramNameMap = buildParamNameMap(entryParams);
|
|
488
|
+
let name = entryName;
|
|
489
|
+
if (!KEEP_METHOD_NAMES.has(name.toLowerCase()) && (name === 'get' || name === 'delete') && facadePrefix) {
|
|
490
|
+
name = `${name}${facadePrefix}`;
|
|
491
|
+
}
|
|
492
|
+
if (seen.has(name)) {
|
|
493
|
+
const bodyTypeName = entryHasBody ? getBodyTypeName(entryOp.requestBody, schemas) : '';
|
|
494
|
+
if (bodyTypeName) {
|
|
495
|
+
name = `${entryName}By${toPascal(bodyTypeName)}`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (seen.has(name)) {
|
|
499
|
+
const paramSuffix = toParamSuffix(entryParams, paramNameMap);
|
|
500
|
+
if (paramSuffix) {
|
|
501
|
+
name = `${entryName}${paramSuffix}`;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (seen.has(name)) {
|
|
505
|
+
const methodSuffix = entryMethod[0].toUpperCase() + entryMethod.slice(1);
|
|
506
|
+
name = `${entryName}${methodSuffix}`;
|
|
507
|
+
}
|
|
508
|
+
seen.add(name);
|
|
509
|
+
const defaultType = entryDefaultReturnType;
|
|
510
|
+
|
|
511
|
+
const requiredParams = entryParams.filter((p) => p.required);
|
|
512
|
+
const optionalParams = entryParams.filter((p) => !p.required);
|
|
513
|
+
const orderedParams = [...requiredParams, ...optionalParams];
|
|
514
|
+
|
|
515
|
+
const paramArgs = orderedParams.map((p) => {
|
|
516
|
+
const {
|
|
517
|
+
schema: paramSchema,
|
|
518
|
+
required,
|
|
519
|
+
name: paramName
|
|
520
|
+
} = p;
|
|
521
|
+
const mappedName = paramNameMap.get(paramName) || paramName;
|
|
522
|
+
const paramType = mapParamSchemaToType(paramSchema, schemas, true);
|
|
523
|
+
const optional = required ? '' : '?';
|
|
524
|
+
return `${mappedName}${optional}: ${paramType}`;
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
let bodyType = '{}';
|
|
528
|
+
const { requestBody: opRequestBody } = entryOp;
|
|
529
|
+
if (entryHasBody && opRequestBody && opRequestBody.content) {
|
|
530
|
+
const { content } = opRequestBody;
|
|
531
|
+
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
532
|
+
if (firstContent && firstContent.schema) {
|
|
533
|
+
bodyType = mapSchemaToType(firstContent.schema, schemas, true);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const bodyArg = entryHasBody ? [`body?: ${bodyType}`] : [];
|
|
537
|
+
const signatureParts = [...paramArgs, ...bodyArg];
|
|
538
|
+
const sigParts = signatureParts.length >= 3
|
|
539
|
+
? `\n ${signatureParts.join(',\n ')}\n `
|
|
540
|
+
: signatureParts.join(', ');
|
|
541
|
+
|
|
542
|
+
const {
|
|
543
|
+
summary: opSummary,
|
|
544
|
+
description: opDescription
|
|
545
|
+
} = entryOp;
|
|
546
|
+
const comment = (opSummary || opDescription || '').replace(/\n/g, ' ')
|
|
547
|
+
.trim();
|
|
548
|
+
const bodyDescription = opRequestBody && opRequestBody.description
|
|
549
|
+
? opRequestBody.description.replace(/\n/g, ' ')
|
|
550
|
+
.trim()
|
|
551
|
+
: '';
|
|
552
|
+
const paramDocs = [];
|
|
553
|
+
for (const p of orderedParams) {
|
|
554
|
+
const {
|
|
555
|
+
description,
|
|
556
|
+
name: paramName
|
|
557
|
+
} = p;
|
|
558
|
+
const mappedName = paramNameMap.get(paramName) || paramName;
|
|
559
|
+
const desc = (description || '').replace(/\n/g, ' ')
|
|
560
|
+
.trim();
|
|
561
|
+
const text = desc && desc !== paramName ? desc : paramName;
|
|
562
|
+
if (text) {
|
|
563
|
+
paramDocs.push(` * @param ${mappedName} ${text}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (entryHasBody) {
|
|
567
|
+
const bodyText = bodyDescription || 'body';
|
|
568
|
+
paramDocs.push(` * @param body ${bodyText}`);
|
|
569
|
+
}
|
|
570
|
+
if (comment || paramDocs.length) {
|
|
571
|
+
methods.push(' /**');
|
|
572
|
+
if (comment) {
|
|
573
|
+
methods.push(` * ${comment}`);
|
|
574
|
+
}
|
|
575
|
+
for (const line of paramDocs) methods.push(line);
|
|
576
|
+
methods.push(' */');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const queryParamNames = entryParams.filter((p) => p.in === 'query')
|
|
580
|
+
.map((p) => {
|
|
581
|
+
const mappedName = paramNameMap.get(p.name) || p.name;
|
|
582
|
+
const key = formatObjectKey(p.name);
|
|
583
|
+
return mappedName === p.name ? key : `${key}: ${mappedName}`;
|
|
584
|
+
});
|
|
585
|
+
const paramsObject = queryParamNames.length ? `{ ${queryParamNames.join(', ')} }` : 'undefined';
|
|
586
|
+
|
|
587
|
+
const pathTemplate = withPathParams(entryPath, entryParams, paramNameMap);
|
|
588
|
+
const pathLiteral = pathTemplate.includes('${') ? `\`${pathTemplate}\`` : `'${pathTemplate}'`;
|
|
589
|
+
|
|
590
|
+
if (entryMethod === 'get') {
|
|
591
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
592
|
+
methods.push(` return this.get<T>(${pathLiteral}, ${paramsObject});`);
|
|
593
|
+
methods.push(' }');
|
|
594
|
+
methods.push('');
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (entryMethod === 'delete') {
|
|
599
|
+
if (entryHasBody) {
|
|
600
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
601
|
+
methods.push(` return this.deleteBody<T>(${pathLiteral}, ${paramsObject}, body);`);
|
|
602
|
+
methods.push(' }');
|
|
603
|
+
methods.push('');
|
|
604
|
+
} else {
|
|
605
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
606
|
+
methods.push(` return this.delete<T>(${pathLiteral}, ${paramsObject});`);
|
|
607
|
+
methods.push(' }');
|
|
608
|
+
methods.push('');
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (entryMethod === 'post') {
|
|
614
|
+
if (entryHasBody) {
|
|
615
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
616
|
+
methods.push(` return this.postBody<T>(${pathLiteral}, ${paramsObject}, body);`);
|
|
617
|
+
methods.push(' }');
|
|
618
|
+
methods.push('');
|
|
619
|
+
} else {
|
|
620
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
621
|
+
methods.push(` return this.post<T>(${pathLiteral}, ${paramsObject});`);
|
|
622
|
+
methods.push(' }');
|
|
623
|
+
methods.push('');
|
|
624
|
+
}
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (entryMethod === 'put') {
|
|
629
|
+
if (entryHasBody) {
|
|
630
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
631
|
+
methods.push(` return this.putBody<T>(${pathLiteral}, ${paramsObject}, body);`);
|
|
632
|
+
methods.push(' }');
|
|
633
|
+
methods.push('');
|
|
634
|
+
} else {
|
|
635
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
636
|
+
methods.push(` return this.put<T>(${pathLiteral}, ${paramsObject});`);
|
|
637
|
+
methods.push(' }');
|
|
638
|
+
methods.push('');
|
|
639
|
+
}
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
methods.push(` public async ${name}<T = ${defaultType}>(${sigParts}): Promise<JsonResponse<T>> {`);
|
|
644
|
+
methods.push(` return this.postBody<T>(${pathLiteral}, ${paramsObject}, body);`);
|
|
645
|
+
methods.push(' }');
|
|
646
|
+
methods.push('');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
classDefs.push(`export class ${className} extends Fetch {`);
|
|
650
|
+
classDefs.push('');
|
|
651
|
+
classDefs.push(...methods);
|
|
652
|
+
classDefs.push('}');
|
|
653
|
+
classDefs.push('');
|
|
654
|
+
|
|
655
|
+
const propName = toPropName(className);
|
|
656
|
+
const tagDesc = tagDescriptions[tag];
|
|
657
|
+
if (tagDesc) {
|
|
658
|
+
apiProps.push(' /**');
|
|
659
|
+
apiProps.push(` * ${tagDesc}`);
|
|
660
|
+
apiProps.push(' */');
|
|
661
|
+
}
|
|
662
|
+
apiProps.push(` public readonly ${propName}: ${className};`);
|
|
663
|
+
apiInit.push(` this.${propName} = new ${className}(this.pathPrefix);`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const baseName = toServiceBaseName(serviceName);
|
|
667
|
+
const apiClassName = `${toPascal(baseName)}Api`;
|
|
668
|
+
const factoryName = `get${apiClassName}`;
|
|
669
|
+
const cacheName = `${baseName}ApiCache`;
|
|
670
|
+
const lines = [];
|
|
671
|
+
lines.push('/*');
|
|
672
|
+
lines.push(' * This file is auto-generated. Do not edit manually.');
|
|
673
|
+
lines.push(' * To update, run the generate-apis.js script.');
|
|
674
|
+
lines.push(' */');
|
|
675
|
+
const fetchImport = clientType === 'WeChatMiniProgram' ? './fetch' : 'beer-network/api';
|
|
676
|
+
lines.push(`import Fetch, { JsonResponse } from '${fetchImport}';`);
|
|
677
|
+
if (fileBase) {
|
|
678
|
+
lines.push(`import type * as Types from './types/${fileBase}-type';`);
|
|
679
|
+
}
|
|
680
|
+
lines.push('');
|
|
681
|
+
lines.push(...classDefs);
|
|
682
|
+
lines.push(`export class ${apiClassName} {`);
|
|
683
|
+
lines.push(' public readonly pathPrefix: string;');
|
|
684
|
+
lines.push(...apiProps);
|
|
685
|
+
lines.push(' constructor(pathPrefix = \'\') {');
|
|
686
|
+
lines.push(` this.pathPrefix = (pathPrefix ?? '') + '${defaultPathPrefix}';`);
|
|
687
|
+
lines.push(...apiInit);
|
|
688
|
+
lines.push(' }');
|
|
689
|
+
lines.push('}');
|
|
690
|
+
lines.push('');
|
|
691
|
+
lines.push(`const ${cacheName} = new Map<string, ${apiClassName}>();`);
|
|
692
|
+
lines.push(`export function ${factoryName}(pathPrefix = '') {`);
|
|
693
|
+
lines.push(` if (!${cacheName}.has(pathPrefix)) {`);
|
|
694
|
+
lines.push(` ${cacheName}.set(pathPrefix, new ${apiClassName}(pathPrefix));`);
|
|
695
|
+
lines.push(' }');
|
|
696
|
+
lines.push(` return ${cacheName}.get(pathPrefix)!;`);
|
|
697
|
+
lines.push('}');
|
|
698
|
+
lines.push('');
|
|
699
|
+
|
|
700
|
+
return lines.join('\n');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Map Swagger schema to a TS type for DTO output.
|
|
704
|
+
function mapSchemaToTsType(schema, schemas) {
|
|
705
|
+
if (!schema) {
|
|
706
|
+
return '{}';
|
|
707
|
+
}
|
|
708
|
+
const {
|
|
709
|
+
$ref,
|
|
710
|
+
enum: schemaEnum,
|
|
711
|
+
type,
|
|
712
|
+
format,
|
|
713
|
+
items,
|
|
714
|
+
additionalProperties
|
|
715
|
+
} = schema;
|
|
716
|
+
if ($ref) {
|
|
717
|
+
const refName = $ref.split('/')
|
|
718
|
+
.pop();
|
|
719
|
+
if (isResponseWrapper(refName)) {
|
|
720
|
+
const refSchema = schemas && schemas[refName];
|
|
721
|
+
if (refSchema && refSchema.properties && refSchema.properties.data) {
|
|
722
|
+
return mapSchemaToTsType(refSchema.properties.data, schemas);
|
|
723
|
+
}
|
|
724
|
+
return '{}';
|
|
725
|
+
}
|
|
726
|
+
return normalizeDtoName(refName);
|
|
727
|
+
}
|
|
728
|
+
if (schemaEnum && schemaEnum.length) {
|
|
729
|
+
return schemaEnum.map((v) => (typeof v === 'string' ? `'${v}'` : String(v)))
|
|
730
|
+
.join(' | ');
|
|
731
|
+
}
|
|
732
|
+
if (type === 'string') {
|
|
733
|
+
return 'string';
|
|
734
|
+
}
|
|
735
|
+
if (type === 'integer') {
|
|
736
|
+
if (format === 'int64' || format === 'long') {
|
|
737
|
+
return 'string';
|
|
738
|
+
}
|
|
739
|
+
return 'number';
|
|
740
|
+
}
|
|
741
|
+
if (type === 'number') {
|
|
742
|
+
return 'number';
|
|
743
|
+
}
|
|
744
|
+
if (type === 'boolean') {
|
|
745
|
+
return 'boolean';
|
|
746
|
+
}
|
|
747
|
+
if (type === 'array') {
|
|
748
|
+
const itemType = mapSchemaToTsType(items, schemas);
|
|
749
|
+
return `${itemType}[]`;
|
|
750
|
+
}
|
|
751
|
+
if (type === 'object') {
|
|
752
|
+
if (additionalProperties) {
|
|
753
|
+
return 'Record<string, {}>';
|
|
754
|
+
}
|
|
755
|
+
return '{}';
|
|
756
|
+
}
|
|
757
|
+
return '{}';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Generate DTO/type definitions file content.
|
|
761
|
+
function generateTypes(spec) {
|
|
762
|
+
const { components = {} } = spec;
|
|
763
|
+
const { schemas = {} } = components;
|
|
764
|
+
const lines = [];
|
|
765
|
+
lines.push('/*');
|
|
766
|
+
lines.push(' * This file is auto-generated. Do not edit manually.');
|
|
767
|
+
lines.push(' * To update, run the generate-apis.js script.');
|
|
768
|
+
lines.push(' */');
|
|
769
|
+
lines.push('');
|
|
770
|
+
|
|
771
|
+
const typeDefs = new Map();
|
|
772
|
+
|
|
773
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
774
|
+
if (!schema) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (isResponseWrapper(name)) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const {
|
|
781
|
+
type: schemaType,
|
|
782
|
+
properties,
|
|
783
|
+
required
|
|
784
|
+
} = schema;
|
|
785
|
+
const normalizedName = normalizeDtoName(name);
|
|
786
|
+
if (schemaType === 'object' && properties) {
|
|
787
|
+
if (!typeDefs.has(normalizedName) || typeDefs.get(normalizedName).kind !== 'object') {
|
|
788
|
+
typeDefs.set(normalizedName, {
|
|
789
|
+
kind: 'object',
|
|
790
|
+
props: {},
|
|
791
|
+
required: {},
|
|
792
|
+
desc: {}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
const def = typeDefs.get(normalizedName);
|
|
796
|
+
const requiredSet = new Set(required || []);
|
|
797
|
+
for (const [prop, propSchema] of Object.entries(properties)) {
|
|
798
|
+
def.props[prop] = mapSchemaToTsType(propSchema, schemas);
|
|
799
|
+
def.required[prop] = requiredSet.has(prop);
|
|
800
|
+
const { description } = propSchema || {};
|
|
801
|
+
const desc = description
|
|
802
|
+
? description.replace(/\n/g, ' ')
|
|
803
|
+
.trim()
|
|
804
|
+
: '';
|
|
805
|
+
if (desc) {
|
|
806
|
+
def.desc[prop] = desc;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const type = mapSchemaToTsType(schema, schemas);
|
|
813
|
+
typeDefs.set(normalizedName, {
|
|
814
|
+
kind: 'alias',
|
|
815
|
+
type
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
for (const [name, def] of typeDefs.entries()) {
|
|
820
|
+
if (def.kind === 'object') {
|
|
821
|
+
lines.push(`export interface ${name} {`);
|
|
822
|
+
for (const [prop, type] of Object.entries(def.props)) {
|
|
823
|
+
const optional = def.required[prop] ? '' : '?';
|
|
824
|
+
const desc = def.desc[prop];
|
|
825
|
+
if (desc) {
|
|
826
|
+
lines.push(' /**');
|
|
827
|
+
lines.push(` * ${desc}`);
|
|
828
|
+
lines.push(' */');
|
|
829
|
+
}
|
|
830
|
+
lines.push(` ${prop}${optional}: ${type};`);
|
|
831
|
+
}
|
|
832
|
+
lines.push('}');
|
|
833
|
+
lines.push('');
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
lines.push(`export type ${name} = ${def.type};`);
|
|
837
|
+
lines.push('');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return lines.join('\n');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Fetch JSON from a URL.
|
|
844
|
+
async function fetchJson(url) {
|
|
845
|
+
const response = await fetch(url);
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
throw new Error(`Failed to fetch ${url}: ${response.status}`);
|
|
848
|
+
}
|
|
849
|
+
return response.json();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Read key/value pairs from .env without extra dependencies.
|
|
853
|
+
function readEnvFile() {
|
|
854
|
+
if (!fs.existsSync(ENV_PATH)) {
|
|
855
|
+
return {};
|
|
856
|
+
}
|
|
857
|
+
const content = fs.readFileSync(ENV_PATH, 'utf8');
|
|
858
|
+
const lines = content.split(/\r?\n/);
|
|
859
|
+
const env = {};
|
|
860
|
+
for (const line of lines) {
|
|
861
|
+
if (!line || line.trim()
|
|
862
|
+
.startsWith('#')) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const idx = line.indexOf('=');
|
|
866
|
+
if (idx === -1) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const key = line.slice(0, idx)
|
|
870
|
+
.trim();
|
|
871
|
+
env[key] = line.slice(idx + 1)
|
|
872
|
+
.trim();
|
|
873
|
+
}
|
|
874
|
+
return env;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Persist values into .env (overwrite or append keys).
|
|
878
|
+
function writeEnvFile(nextEnv) {
|
|
879
|
+
const existing = readEnvFile();
|
|
880
|
+
const merged = { ...existing, ...nextEnv };
|
|
881
|
+
const lines = Object.entries(merged)
|
|
882
|
+
.map(([key, value]) => `${key}=${value}`);
|
|
883
|
+
fs.writeFileSync(ENV_PATH, `${lines.join('\n')}\n`, 'utf8');
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function normalizeClientType(value) {
|
|
887
|
+
if (!value) {
|
|
888
|
+
return '';
|
|
889
|
+
}
|
|
890
|
+
if (CLIENT_TYPES.has(value)) {
|
|
891
|
+
return value;
|
|
892
|
+
}
|
|
893
|
+
if (value.toUpperCase() === 'WEB') {
|
|
894
|
+
return 'WEB';
|
|
895
|
+
}
|
|
896
|
+
const normalized = value.replace(/[-_\s]/g, '')
|
|
897
|
+
.toLowerCase();
|
|
898
|
+
if (normalized === 'wechatminiprogram') {
|
|
899
|
+
return 'WeChatMiniProgram';
|
|
900
|
+
}
|
|
901
|
+
return '';
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Ask user for missing values via CLI.
|
|
905
|
+
async function promptForMissing(env) {
|
|
906
|
+
const rl = createInterface({
|
|
907
|
+
input: process.stdin,
|
|
908
|
+
output: process.stdout
|
|
909
|
+
});
|
|
910
|
+
const ask = (q) => new Promise((resolve) => {
|
|
911
|
+
rl.question(q, resolve);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
const result = { ...env };
|
|
915
|
+
if (!result.SWAGGER_BASE_URL) {
|
|
916
|
+
result.SWAGGER_BASE_URL = (await ask('BASE_URL: ')).trim();
|
|
917
|
+
}
|
|
918
|
+
if (!result.SWAGGER_OUT_DIRECTORY) {
|
|
919
|
+
result.SWAGGER_OUT_DIRECTORY = (await ask('DIRECTORY: ')).trim();
|
|
920
|
+
}
|
|
921
|
+
if (!result.CLIENT_TYPE || !CLIENT_TYPES.has(result.CLIENT_TYPE)) {
|
|
922
|
+
let selected = '';
|
|
923
|
+
while (!CLIENT_TYPES.has(selected)) {
|
|
924
|
+
selected = normalizeClientType((await ask('CLIENT_TYPE (WEB/WeChatMiniProgram): ')).trim());
|
|
925
|
+
}
|
|
926
|
+
result.CLIENT_TYPE = selected;
|
|
927
|
+
}
|
|
928
|
+
rl.close();
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Main CLI entrypoint.
|
|
933
|
+
async function main() {
|
|
934
|
+
const envFile = readEnvFile();
|
|
935
|
+
let {
|
|
936
|
+
SWAGGER_BASE_URL: baseUrl,
|
|
937
|
+
SWAGGER_OUT_DIRECTORY: directory,
|
|
938
|
+
CLIENT_TYPE: clientType
|
|
939
|
+
} = { ...process.env, ...envFile };
|
|
940
|
+
clientType = normalizeClientType(clientType);
|
|
941
|
+
|
|
942
|
+
if (!baseUrl || !directory || !clientType) {
|
|
943
|
+
const filled = await promptForMissing({
|
|
944
|
+
SWAGGER_BASE_URL: baseUrl || '',
|
|
945
|
+
SWAGGER_OUT_DIRECTORY: directory || '',
|
|
946
|
+
CLIENT_TYPE: clientType || ''
|
|
947
|
+
});
|
|
948
|
+
baseUrl = filled.SWAGGER_BASE_URL;
|
|
949
|
+
directory = filled.SWAGGER_OUT_DIRECTORY;
|
|
950
|
+
clientType = normalizeClientType(filled.CLIENT_TYPE);
|
|
951
|
+
writeEnvFile({
|
|
952
|
+
SWAGGER_BASE_URL: baseUrl,
|
|
953
|
+
SWAGGER_OUT_DIRECTORY: directory,
|
|
954
|
+
CLIENT_TYPE: clientType
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (!baseUrl) {
|
|
959
|
+
throw new Error('SWAGGER_BASE_URL is required.');
|
|
960
|
+
}
|
|
961
|
+
if (!directory) {
|
|
962
|
+
throw new Error('SWAGGER_OUT_DIRECTORY is required.');
|
|
963
|
+
}
|
|
964
|
+
if (!clientType) {
|
|
965
|
+
throw new Error('CLIENT_TYPE is required.');
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const configUrl = `${baseUrl}/v3/api-docs/swagger-config`;
|
|
969
|
+
const outputDirectory = path.isAbsolute(directory) ? directory : path.join(process.cwd(), directory);
|
|
970
|
+
const typesDirectory = path.join(outputDirectory, 'types');
|
|
971
|
+
|
|
972
|
+
const args = process.argv.slice(2);
|
|
973
|
+
const filter = args.length ? new Set(args) : null;
|
|
974
|
+
|
|
975
|
+
const config = await fetchJson(configUrl);
|
|
976
|
+
const services = config.urls ?? [];
|
|
977
|
+
if (config.url !== undefined) {
|
|
978
|
+
services.push({
|
|
979
|
+
name: 'api',
|
|
980
|
+
url: config.url
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (!fs.existsSync(outputDirectory)) {
|
|
985
|
+
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
986
|
+
}
|
|
987
|
+
if (!fs.existsSync(typesDirectory)) {
|
|
988
|
+
fs.mkdirSync(typesDirectory, { recursive: true });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
for (const service of services) {
|
|
992
|
+
const {
|
|
993
|
+
name: serviceName,
|
|
994
|
+
url
|
|
995
|
+
} = service;
|
|
996
|
+
if (SKIPPED_SERVICES.has(serviceName)) {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (filter && !filter.has(serviceName)) {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const specUrl = `${baseUrl}${url}`;
|
|
1003
|
+
const spec = await fetchJson(specUrl);
|
|
1004
|
+
const fileBase = toFileBase(serviceName);
|
|
1005
|
+
const content = generateForSpec(spec, serviceName, fileBase, clientType);
|
|
1006
|
+
const typesContent = generateTypes(spec);
|
|
1007
|
+
|
|
1008
|
+
const outPath = path.join(outputDirectory, `${fileBase}.ts`);
|
|
1009
|
+
const typesPath = path.join(typesDirectory, `${fileBase}-type.ts`);
|
|
1010
|
+
fs.writeFileSync(outPath, content, 'utf8');
|
|
1011
|
+
fs.writeFileSync(typesPath, typesContent, 'utf8');
|
|
1012
|
+
console.log(`Generated ${outPath}`);
|
|
1013
|
+
console.log(`Generated ${typesPath}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
main()
|
|
1018
|
+
.catch((err) => {
|
|
1019
|
+
console.error(err);
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "beer-swagger",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"bin": {
|
|
5
|
+
"beer-swagger": "./generate-apis.js"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"pub-m": "npm publish --access public"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "22.19.7"
|
|
14
|
+
},
|
|
15
|
+
"resolutions": {
|
|
16
|
+
"dayjs": "^1.11.10"
|
|
17
|
+
},
|
|
18
|
+
"browserslist": {
|
|
19
|
+
"production": [
|
|
20
|
+
">0.2%",
|
|
21
|
+
"not dead",
|
|
22
|
+
"not op_mini all"
|
|
23
|
+
],
|
|
24
|
+
"development": [
|
|
25
|
+
"last 1 chrome version",
|
|
26
|
+
"last 1 firefox version",
|
|
27
|
+
"last 1 safari version"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|