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.
@@ -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,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="JavaScriptLibraryMappings">
4
+ <includedPredefinedLibrary name="Node.js Core" />
5
+ </component>
6
+ </project>
@@ -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
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
Binary file
@@ -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
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": [
10
+ "vite.config.ts"
11
+ ]
12
+ }