beer-swagger 4.0.1 → 4.0.2
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/generate-apis.js +111 -26
- package/package.json +1 -1
package/generate-apis.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import {createInterface} from 'readline';
|
|
6
6
|
|
|
7
7
|
const ENV_PATH = path.join(process.cwd(), '.env');
|
|
8
8
|
const SKIPPED_SERVICES = new Set([
|
|
@@ -166,12 +166,12 @@ function getBodyTypeName(requestBody, schemas) {
|
|
|
166
166
|
if (!requestBody || !requestBody.content) {
|
|
167
167
|
return '';
|
|
168
168
|
}
|
|
169
|
-
const {
|
|
169
|
+
const {content} = requestBody;
|
|
170
170
|
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
171
171
|
if (!firstContent || !firstContent.schema) {
|
|
172
172
|
return '';
|
|
173
173
|
}
|
|
174
|
-
const {
|
|
174
|
+
const {schema} = firstContent;
|
|
175
175
|
if (!schema.$ref) {
|
|
176
176
|
return '';
|
|
177
177
|
}
|
|
@@ -182,6 +182,49 @@ function getBodyTypeName(requestBody, schemas) {
|
|
|
182
182
|
return stripped || normalized;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// Return the path segment immediately above the operation segment.
|
|
186
|
+
function getParentPathSegment(pathStr) {
|
|
187
|
+
const segments = pathStr.split('/')
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.filter((part) => !part.startsWith('{'));
|
|
190
|
+
return segments.length > 1 ? segments[segments.length - 2] : '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Compare two names using normalized Levenshtein similarity.
|
|
194
|
+
function nameSimilarity(left, right) {
|
|
195
|
+
const a = String(left || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
196
|
+
const b = String(right || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
197
|
+
if (!a || !b) {
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
if (a === b) {
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
const previous = Array.from({length: b.length + 1}, (_, index) => index);
|
|
204
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
205
|
+
let diagonal = previous[0];
|
|
206
|
+
previous[0] = i;
|
|
207
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
208
|
+
const above = previous[j];
|
|
209
|
+
previous[j] = a[i - 1] === b[j - 1]
|
|
210
|
+
? diagonal
|
|
211
|
+
: Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + 1);
|
|
212
|
+
diagonal = above;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return 1 - previous[b.length] / Math.max(a.length, b.length);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A POST whose parent resource matches its defined request entity is a save.
|
|
219
|
+
function shouldSaveByPathEntity(pathStr, method, requestBody, schemas) {
|
|
220
|
+
if (method !== 'post') {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const parentPath = getParentPathSegment(pathStr);
|
|
224
|
+
const bodyTypeName = getBodyTypeName(requestBody, schemas);
|
|
225
|
+
return !!parentPath && !!bodyTypeName && nameSimilarity(parentPath, bodyTypeName) >= 0.8;
|
|
226
|
+
}
|
|
227
|
+
|
|
185
228
|
// Map a Swagger schema to a runtime return type.
|
|
186
229
|
function mapSchemaToType(schema, schemas, useTypes) {
|
|
187
230
|
if (!schema) {
|
|
@@ -255,7 +298,7 @@ function mapParamSchemaToType(schema, schemas, useTypes, isQuery = false) {
|
|
|
255
298
|
type,
|
|
256
299
|
format
|
|
257
300
|
} = schema;
|
|
258
|
-
const {
|
|
301
|
+
const {items} = schema;
|
|
259
302
|
if ($ref) {
|
|
260
303
|
const refName = normalizeDtoName($ref.split('/')
|
|
261
304
|
.pop());
|
|
@@ -290,7 +333,11 @@ function mapParamSchemaToType(schema, schemas, useTypes, isQuery = false) {
|
|
|
290
333
|
|
|
291
334
|
// Resolve parameter type for a parameter definition.
|
|
292
335
|
function resolveParamType(param, schemas) {
|
|
293
|
-
const schemaToUse = param.schema || (param.type ? {
|
|
336
|
+
const schemaToUse = param.schema || (param.type ? {
|
|
337
|
+
type: param.type,
|
|
338
|
+
format: param.format,
|
|
339
|
+
items: param.items
|
|
340
|
+
} : null);
|
|
294
341
|
const isQuery = param.in === 'query';
|
|
295
342
|
return mapParamSchemaToType(schemaToUse, schemas, true, isQuery);
|
|
296
343
|
}
|
|
@@ -310,9 +357,9 @@ function isEventStreamResponse(op) {
|
|
|
310
357
|
|
|
311
358
|
// Resolve default response type from Swagger responses.
|
|
312
359
|
function getDefaultReturnType(op, schemas, useTypes) {
|
|
313
|
-
const {
|
|
360
|
+
const {responses = {}} = op;
|
|
314
361
|
const res = responses['200'] || responses['201'] || responses.default || {};
|
|
315
|
-
const {
|
|
362
|
+
const {content = {}} = res;
|
|
316
363
|
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
317
364
|
if (!firstContent) {
|
|
318
365
|
return 'object';
|
|
@@ -322,7 +369,7 @@ function getDefaultReturnType(op, schemas, useTypes) {
|
|
|
322
369
|
|
|
323
370
|
// Collect query/path parameters.
|
|
324
371
|
function getParams(op) {
|
|
325
|
-
const {
|
|
372
|
+
const {parameters = []} = op;
|
|
326
373
|
const params = Array.isArray(parameters) ? parameters : [];
|
|
327
374
|
return params.filter((p) => p.in === 'query' || p.in === 'path');
|
|
328
375
|
}
|
|
@@ -421,15 +468,28 @@ function toPascal(value) {
|
|
|
421
468
|
return value[0].toUpperCase() + value.slice(1);
|
|
422
469
|
}
|
|
423
470
|
|
|
471
|
+
// Join path prefixes while keeping a single separator at their boundary.
|
|
472
|
+
function joinPathPrefixes(...prefixes) {
|
|
473
|
+
return prefixes
|
|
474
|
+
.map((prefix) => typeof prefix === 'string' ? prefix.trim() : '')
|
|
475
|
+
.filter(Boolean)
|
|
476
|
+
.reduce((result, prefix) => {
|
|
477
|
+
if (!result) {
|
|
478
|
+
return prefix;
|
|
479
|
+
}
|
|
480
|
+
return `${result.replace(/\/+$/, '')}/${prefix.replace(/^\/+/, '')}`;
|
|
481
|
+
}, '');
|
|
482
|
+
}
|
|
483
|
+
|
|
424
484
|
// Generate API file content for a service.
|
|
425
|
-
function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
485
|
+
function generateForSpec(spec, serviceName, fileBase, isDefaultFetch, responsePathPrefix = '') {
|
|
426
486
|
const {
|
|
427
487
|
components = {},
|
|
428
488
|
tags: specTags = [],
|
|
429
489
|
paths = {}
|
|
430
490
|
} = spec;
|
|
431
|
-
const defaultPathPrefix =
|
|
432
|
-
const {
|
|
491
|
+
const defaultPathPrefix = joinPathPrefixes(responsePathPrefix, spec.pathPrefix);
|
|
492
|
+
const {schemas = {}} = components;
|
|
433
493
|
const tagDescriptions = {};
|
|
434
494
|
if (Array.isArray(specTags)) {
|
|
435
495
|
for (const tag of specTags) {
|
|
@@ -544,10 +604,15 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
544
604
|
name = `${name}${facadePrefix}`;
|
|
545
605
|
}
|
|
546
606
|
const isDynamicPath = entryParams.some((p) => p.in === 'path') || entryPath.includes('{');
|
|
547
|
-
|
|
607
|
+
const saveByPathEntity = shouldSaveByPathEntity(
|
|
608
|
+
entryPath,
|
|
609
|
+
entryMethod,
|
|
610
|
+
entryHasBody ? entryOp.requestBody : null,
|
|
611
|
+
schemas
|
|
612
|
+
);
|
|
613
|
+
if (saveByPathEntity || (seen.has(name) && isDynamicPath && (entryMethod === 'post' || entryMethod === 'put'))) {
|
|
548
614
|
const hasSavePrefix = name.startsWith('save') && (name.length === 4 || /^[A-Z]/.test(name.slice(4)));
|
|
549
|
-
|
|
550
|
-
name = saveName;
|
|
615
|
+
name = hasSavePrefix ? name : `save${toPascal(name)}`;
|
|
551
616
|
}
|
|
552
617
|
const baseName = name;
|
|
553
618
|
if (seen.has(name)) {
|
|
@@ -585,9 +650,9 @@ function generateForSpec(spec, serviceName, fileBase, isDefaultFetch) {
|
|
|
585
650
|
});
|
|
586
651
|
|
|
587
652
|
let bodyType = 'object';
|
|
588
|
-
const {
|
|
653
|
+
const {requestBody: opRequestBody} = entryOp;
|
|
589
654
|
if (entryHasBody && opRequestBody && opRequestBody.content) {
|
|
590
|
-
const {
|
|
655
|
+
const {content} = opRequestBody;
|
|
591
656
|
const firstContent = content['application/json'] || content['application/*+json'] || Object.values(content)[0];
|
|
592
657
|
if (firstContent && firstContent.schema) {
|
|
593
658
|
bodyType = mapSchemaToType(firstContent.schema, schemas, true);
|
|
@@ -880,8 +945,8 @@ function isAnySchema(schema) {
|
|
|
880
945
|
|
|
881
946
|
// Generate DTO/type definitions file content.
|
|
882
947
|
function generateTypes(spec) {
|
|
883
|
-
const {
|
|
884
|
-
const {
|
|
948
|
+
const {components = {}} = spec;
|
|
949
|
+
const {schemas = {}} = components;
|
|
885
950
|
const lines = [];
|
|
886
951
|
lines.push('/*');
|
|
887
952
|
lines.push(' * This file is auto-generated. Do not edit manually.');
|
|
@@ -925,7 +990,7 @@ function generateTypes(spec) {
|
|
|
925
990
|
}
|
|
926
991
|
def.props[prop] = propType;
|
|
927
992
|
def.required[prop] = requiredSet.has(prop);
|
|
928
|
-
const {
|
|
993
|
+
const {description} = propSchema || {};
|
|
929
994
|
const desc = description
|
|
930
995
|
? description.replace(/\n/g, ' ')
|
|
931
996
|
.trim()
|
|
@@ -977,6 +1042,26 @@ async function fetchJson(url) {
|
|
|
977
1042
|
return response.json();
|
|
978
1043
|
}
|
|
979
1044
|
|
|
1045
|
+
// Fetch JSON together with response headers needed by API generation.
|
|
1046
|
+
async function fetchJsonWithHeaders(url) {
|
|
1047
|
+
console.log(`Fetching ${url}`);
|
|
1048
|
+
const response = await fetch(url);
|
|
1049
|
+
if (!response.ok) {
|
|
1050
|
+
throw new Error(`Failed to fetch ${url}: ${response.status}`);
|
|
1051
|
+
}
|
|
1052
|
+
const responseText = await response.text();
|
|
1053
|
+
if (responseText === '') {
|
|
1054
|
+
return {
|
|
1055
|
+
data: {},
|
|
1056
|
+
pathPrefix: response.headers.get('x-path-prefix') || ''
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
data: JSON.parse(responseText),
|
|
1061
|
+
pathPrefix: response.headers.get('x-path-prefix') || ''
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
980
1065
|
// Read key/value pairs from .env without extra dependencies.
|
|
981
1066
|
function readEnvFile() {
|
|
982
1067
|
if (!fs.existsSync(ENV_PATH)) {
|
|
@@ -1005,7 +1090,7 @@ function readEnvFile() {
|
|
|
1005
1090
|
// Persist values into .env (overwrite or append keys).
|
|
1006
1091
|
function writeEnvFile(nextEnv) {
|
|
1007
1092
|
const existing = readEnvFile();
|
|
1008
|
-
const merged = {
|
|
1093
|
+
const merged = {...existing, ...nextEnv};
|
|
1009
1094
|
const lines = Object.entries(merged)
|
|
1010
1095
|
.map(([key, value]) => `${key}=${value}`);
|
|
1011
1096
|
fs.writeFileSync(ENV_PATH, `${lines.join('\n')}\n`, 'utf8');
|
|
@@ -1040,7 +1125,7 @@ async function promptForMissing(env) {
|
|
|
1040
1125
|
rl.question(q, resolve);
|
|
1041
1126
|
});
|
|
1042
1127
|
|
|
1043
|
-
const result = {
|
|
1128
|
+
const result = {...env};
|
|
1044
1129
|
if (!result.SWAGGER_BASE_URL) {
|
|
1045
1130
|
result.SWAGGER_BASE_URL = (await ask('BASE_URL: ')).trim();
|
|
1046
1131
|
}
|
|
@@ -1064,7 +1149,7 @@ async function main() {
|
|
|
1064
1149
|
SWAGGER_BASE_URL: baseUrl,
|
|
1065
1150
|
SWAGGER_OUT_DIRECTORY: directory,
|
|
1066
1151
|
IS_DEFAULT_FETCH: isDefaultFetch
|
|
1067
|
-
} = {
|
|
1152
|
+
} = {...process.env, ...envFile};
|
|
1068
1153
|
isDefaultFetch = normalizeBoolean(isDefaultFetch);
|
|
1069
1154
|
|
|
1070
1155
|
if (!baseUrl || !directory || isDefaultFetch === undefined) {
|
|
@@ -1109,10 +1194,10 @@ async function main() {
|
|
|
1109
1194
|
}
|
|
1110
1195
|
|
|
1111
1196
|
if (!fs.existsSync(outputDirectory)) {
|
|
1112
|
-
fs.mkdirSync(outputDirectory, {
|
|
1197
|
+
fs.mkdirSync(outputDirectory, {recursive: true});
|
|
1113
1198
|
}
|
|
1114
1199
|
if (!fs.existsSync(typesDirectory)) {
|
|
1115
|
-
fs.mkdirSync(typesDirectory, {
|
|
1200
|
+
fs.mkdirSync(typesDirectory, {recursive: true});
|
|
1116
1201
|
}
|
|
1117
1202
|
|
|
1118
1203
|
for (const service of services) {
|
|
@@ -1127,9 +1212,9 @@ async function main() {
|
|
|
1127
1212
|
continue;
|
|
1128
1213
|
}
|
|
1129
1214
|
const specUrl = `${baseUrl}${url}`;
|
|
1130
|
-
const spec = await
|
|
1215
|
+
const {data: spec, pathPrefix} = await fetchJsonWithHeaders(specUrl);
|
|
1131
1216
|
const fileBase = toFileBase(serviceName);
|
|
1132
|
-
const content = generateForSpec(spec, serviceName, fileBase, isDefaultFetch);
|
|
1217
|
+
const content = generateForSpec(spec, serviceName, fileBase, isDefaultFetch, pathPrefix);
|
|
1133
1218
|
const typesContent = generateTypes(spec);
|
|
1134
1219
|
|
|
1135
1220
|
const outPath = path.join(outputDirectory, `${fileBase}.ts`);
|