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.
Files changed (2) hide show
  1. package/generate-apis.js +111 -26
  2. 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 { createInterface } from 'readline';
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 { content } = requestBody;
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 { schema } = firstContent;
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 { items } = schema;
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 ? { type: param.type, format: param.format, items: param.items } : null);
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 { responses = {} } = op;
360
+ const {responses = {}} = op;
314
361
  const res = responses['200'] || responses['201'] || responses.default || {};
315
- const { content = {} } = res;
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 { parameters = [] } = op;
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 = typeof spec.pathPrefix === 'string' ? spec.pathPrefix : '';
432
- const { schemas = {} } = components;
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
- if (seen.has(name) && isDynamicPath && (entryMethod === 'post' || entryMethod === 'put')) {
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
- const saveName = hasSavePrefix ? name : `save${toPascal(name)}`;
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 { requestBody: opRequestBody } = entryOp;
653
+ const {requestBody: opRequestBody} = entryOp;
589
654
  if (entryHasBody && opRequestBody && opRequestBody.content) {
590
- const { content } = opRequestBody;
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 { components = {} } = spec;
884
- const { schemas = {} } = components;
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 { description } = propSchema || {};
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 = { ...existing, ...nextEnv };
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 = { ...env };
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
- } = { ...process.env, ...envFile };
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, { recursive: true });
1197
+ fs.mkdirSync(outputDirectory, {recursive: true});
1113
1198
  }
1114
1199
  if (!fs.existsSync(typesDirectory)) {
1115
- fs.mkdirSync(typesDirectory, { recursive: true });
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 fetchJson(specUrl);
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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beer-swagger",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
4
4
  "bin": {
5
5
  "beer-swagger": "./generate-apis.js"
6
6
  },