@redocly/cli 1.0.0-beta.108 → 1.0.0-beta.109

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.
@@ -1,4 +1,6 @@
1
1
  import { ConfigFixture } from './../../__tests__/fixtures/config';
2
+ import { Document, ResolveError } from '@redocly/openapi-core';
3
+ import { firstDocument, secondDocument } from '../documents';
2
4
 
3
5
  export const __redoclyClient = {
4
6
  isAuthorizedWithRedocly: jest.fn().mockResolvedValue(true),
@@ -25,3 +27,39 @@ export const formatProblems = jest.fn();
25
27
  export const slash = jest.fn();
26
28
  export const findConfig = jest.fn();
27
29
  export const doesYamlFileExist = jest.fn();
30
+ export const bundleDocument = jest.fn(() => Promise.resolve({ problems: {} }));
31
+ export const detectOpenAPI = jest.fn();
32
+
33
+ export class BaseResolver {
34
+ cache = new Map<string, Promise<Document | ResolveError>>();
35
+
36
+ getFiles = jest.fn();
37
+ resolveExternalRef = jest.fn();
38
+ loadExternalRef = jest.fn;
39
+ parseDocument = jest.fn();
40
+ resolveDocument = jest
41
+ .fn()
42
+ .mockImplementationOnce(() =>
43
+ Promise.resolve({ source: { absoluteRef: 'ref' }, parsed: firstDocument })
44
+ )
45
+ .mockImplementationOnce(() =>
46
+ Promise.resolve({ source: { absoluteRef: 'ref' }, parsed: secondDocument })
47
+ );
48
+ }
49
+
50
+ export enum OasVersion {
51
+ Version2 = 'oas2',
52
+ Version3_0 = 'oas3_0',
53
+ Version3_1 = 'oas3_1',
54
+ }
55
+
56
+ export enum Oas3Operations {
57
+ get = 'get',
58
+ put = 'put',
59
+ post = 'post',
60
+ delete = 'delete',
61
+ options = 'options',
62
+ head = 'head',
63
+ patch = 'patch',
64
+ trace = 'trace',
65
+ }
@@ -0,0 +1,63 @@
1
+ export const firstDocument = {
2
+ openapi: '3.0.0',
3
+ servers: [{ url: 'http://localhost:8080' }],
4
+ info: {
5
+ description: 'example test',
6
+ version: '1.0.0',
7
+ title: 'Swagger Petstore',
8
+ termsOfService: 'http://swagger.io/terms/',
9
+ license: {
10
+ name: 'Apache 2.0',
11
+ url: 'http://www.apache.org/licenses/LICENSE-2.0.html',
12
+ },
13
+ },
14
+ paths: {
15
+ '/GETUser/{userId}': {
16
+ summary: 'get user by id',
17
+ description: 'user info',
18
+ servers: [{ url: '/user' }, { url: '/pet', description: 'pet server' }],
19
+
20
+ get: {
21
+ tags: ['pet'],
22
+ summary: 'Find pet by ID',
23
+ description: 'Returns a single pet',
24
+ operationId: 'getPetById',
25
+ servers: [{ url: '/pet' }],
26
+ },
27
+ parameters: [{ name: 'param1', in: 'header', schema: { description: 'string' } }],
28
+ },
29
+ },
30
+ components: {},
31
+ };
32
+
33
+ export const secondDocument = {
34
+ openapi: '3.0.0',
35
+ servers: [{ url: 'http://localhost:8080' }],
36
+ info: {
37
+ description: 'example test',
38
+ version: '1.0.0',
39
+ title: 'Swagger Petstore',
40
+ termsOfService: 'http://swagger.io/terms/',
41
+ license: {
42
+ name: 'Apache 2.0',
43
+ url: 'http://www.apache.org/licenses/LICENSE-2.0.html',
44
+ },
45
+ },
46
+ post: {
47
+ '/GETUser/{userId}': {
48
+ summary: 'get user',
49
+ description: 'user information',
50
+ servers: [{ url: '/user' }, { url: '/pet', description: '' }],
51
+
52
+ get: {
53
+ tags: ['pet'],
54
+ summary: 'Find pet by ID',
55
+ description: 'Returns a single pet',
56
+ operationId: 'getPetById',
57
+ servers: [{ url: '/pet' }],
58
+ },
59
+ parameters: [{ name: 'param1', in: 'header', schema: { description: 'string' } }],
60
+ },
61
+ },
62
+ components: {},
63
+ };
@@ -11,3 +11,4 @@ export const printLintTotals = jest.fn();
11
11
  export const getOutputFileName = jest.fn(() => ({ outputFile: 'test.yaml', ext: 'yaml' }));
12
12
  export const handleError = jest.fn();
13
13
  export const exitWithError = jest.fn();
14
+ export const writeYaml = jest.fn();
@@ -1,6 +1,7 @@
1
1
  import { handleJoin } from '../../commands/join';
2
- import { exitWithError } from '../../utils';
2
+ import { exitWithError, writeYaml } from '../../utils';
3
3
  import { yellow } from 'colorette';
4
+ import { detectOpenAPI } from '@redocly/openapi-core';
4
5
 
5
6
  jest.mock('../../utils');
6
7
  jest.mock('colorette');
@@ -44,4 +45,25 @@ describe('handleJoin fails', () => {
44
45
  `You use prefix-tags-with-filename, without-x-tag-groups together.\nPlease choose only one! \n\n`
45
46
  );
46
47
  });
48
+
49
+ it('should call exitWithError because Only OpenAPI 3 is supported', async () => {
50
+ await handleJoin(
51
+ {
52
+ apis: ['first.yaml', 'second.yaml'],
53
+ },
54
+ 'cli-version'
55
+ );
56
+ expect(exitWithError).toHaveBeenCalledWith('Only OpenAPI 3 is supported: undefined \n\n');
57
+ });
58
+
59
+ it('should call writeYaml function', async () => {
60
+ (detectOpenAPI as jest.Mock).mockReturnValue('oas3_0');
61
+ await handleJoin(
62
+ {
63
+ apis: ['first.yaml', 'second.yaml'],
64
+ },
65
+ 'cli-version'
66
+ );
67
+ expect(writeYaml).toHaveBeenCalled();
68
+ });
47
69
  });
@@ -26,7 +26,9 @@ import {
26
26
  writeYaml,
27
27
  exitWithError,
28
28
  } from '../utils';
29
- import { isObject, isString } from '../js-utils';
29
+ import { isObject, isString, keysOf } from '../js-utils';
30
+ import { Oas3Parameter, Oas3PathItem, Oas3Server } from '@redocly/openapi-core/lib/typings/openapi';
31
+ import { OPENAPI3_METHOD } from './split/types';
30
32
 
31
33
  const COMPONENTS = 'components';
32
34
  const Tags = 'tags';
@@ -332,73 +334,180 @@ export async function handleJoin(argv: JoinArgv, packageVersion: string) {
332
334
  { apiFilename, api, potentialConflicts, tagsPrefix, componentsPrefix }: JoinDocumentContext
333
335
  ) {
334
336
  const { paths } = openapi;
337
+ const operationsSet = new Set(keysOf<typeof OPENAPI3_METHOD>(OPENAPI3_METHOD));
335
338
  if (paths) {
336
339
  if (!joinedDef.hasOwnProperty('paths')) {
337
340
  joinedDef['paths'] = {};
338
341
  }
339
- for (const path of Object.keys(paths)) {
342
+
343
+ for (const path of keysOf(paths)) {
340
344
  if (!joinedDef.paths.hasOwnProperty(path)) {
341
345
  joinedDef.paths[path] = {};
342
346
  }
343
347
  if (!potentialConflicts.paths.hasOwnProperty(path)) {
344
348
  potentialConflicts.paths[path] = {};
345
349
  }
346
- for (const [operation, pathOperation] of Object.entries(paths[path])) {
347
- joinedDef.paths[path][operation] = pathOperation;
348
- potentialConflicts.paths[path][operation] = [
349
- ...(potentialConflicts.paths[path][operation] || []),
350
- api,
351
- ];
352
- const { operationId } = pathOperation;
353
- if (operationId) {
354
- if (!potentialConflicts.paths.hasOwnProperty('operationIds')) {
355
- potentialConflicts.paths['operationIds'] = {};
356
- }
357
- potentialConflicts.paths.operationIds[operationId] = [
358
- ...(potentialConflicts.paths.operationIds[operationId] || []),
359
- api,
360
- ];
350
+
351
+ const pathItem = paths[path] as Oas3PathItem;
352
+
353
+ for (const field of keysOf(pathItem)) {
354
+ if (operationsSet.has(field as OPENAPI3_METHOD)) {
355
+ collectPathOperation(pathItem, path, field as OPENAPI3_METHOD);
361
356
  }
362
- const { tags, security } = joinedDef.paths[path][operation];
363
- if (tags) {
364
- joinedDef.paths[path][operation].tags = tags.map((tag: string) =>
365
- addPrefix(tag, tagsPrefix)
366
- );
367
- populateTags({
368
- api,
369
- apiFilename,
370
- tags: formatTags(tags),
371
- potentialConflicts,
372
- tagsPrefix,
373
- componentsPrefix,
374
- });
375
- } else {
376
- joinedDef.paths[path][operation]['tags'] = [
377
- addPrefix('other', tagsPrefix || apiFilename),
378
- ];
379
- populateTags({
380
- api,
381
- apiFilename,
382
- tags: formatTags(['other']),
383
- potentialConflicts,
384
- tagsPrefix: tagsPrefix || apiFilename,
385
- componentsPrefix,
386
- });
357
+ if (field === 'servers') {
358
+ collectPathServers(pathItem, path);
387
359
  }
388
- if (!security && openapi.hasOwnProperty('security')) {
389
- joinedDef.paths[path][operation]['security'] = addSecurityPrefix(
390
- openapi.security,
391
- componentsPrefix!
392
- );
393
- } else if (pathOperation.security) {
394
- joinedDef.paths[path][operation].security = addSecurityPrefix(
395
- pathOperation.security,
396
- componentsPrefix!
397
- );
360
+ if (field === 'parameters') {
361
+ collectPathParameters(pathItem, path);
362
+ }
363
+ if (typeof pathItem[field] === 'string') {
364
+ collectPathStringFields(pathItem, path, field);
398
365
  }
399
366
  }
400
367
  }
401
368
  }
369
+
370
+ function collectPathStringFields(
371
+ pathItem: Oas3PathItem,
372
+ path: string | number,
373
+ field: keyof Oas3PathItem
374
+ ) {
375
+ const fieldValue = pathItem[field];
376
+ if (
377
+ joinedDef.paths[path].hasOwnProperty(field) &&
378
+ joinedDef.paths[path][field] !== fieldValue
379
+ ) {
380
+ process.stderr.write(yellow(`warning: different ${field} values in ${path}\n`));
381
+ return;
382
+ }
383
+ joinedDef.paths[path][field] = fieldValue;
384
+ }
385
+
386
+ function collectPathServers(pathItem: Oas3PathItem, path: string | number) {
387
+ if (!pathItem.servers) {
388
+ return;
389
+ }
390
+
391
+ if (!joinedDef.paths[path].hasOwnProperty('servers')) {
392
+ joinedDef.paths[path].servers = [];
393
+ }
394
+
395
+ for (const server of pathItem.servers) {
396
+ let isFoundServer = false;
397
+ for (const pathServer of joinedDef.paths[path].servers) {
398
+ if (pathServer.url === server.url) {
399
+ if (!isServersEqual(pathServer, server)) {
400
+ exitWithError(`Different server values for (${server.url}) in ${path}`);
401
+ }
402
+ isFoundServer = true;
403
+ }
404
+ }
405
+
406
+ if (!isFoundServer) {
407
+ joinedDef.paths[path].servers.push(server);
408
+ }
409
+ }
410
+ }
411
+
412
+ function collectPathParameters(pathItem: Oas3PathItem, path: string | number) {
413
+ if (!pathItem.parameters) {
414
+ return;
415
+ }
416
+ if (!joinedDef.paths[path].hasOwnProperty('parameters')) {
417
+ joinedDef.paths[path].parameters = [];
418
+ }
419
+
420
+ for (const parameter of pathItem.parameters as Oas3Parameter[]) {
421
+ let isFoundParameter = false;
422
+ for (const pathParameter of joinedDef.paths[path].parameters) {
423
+ if (pathParameter.name === parameter.name && pathParameter.in === parameter.in) {
424
+ if (!isEqual(pathParameter.schema, parameter.schema)) {
425
+ exitWithError(`Different parameter schemas for (${parameter.name}) in ${path}`);
426
+ }
427
+ isFoundParameter = true;
428
+ }
429
+ }
430
+
431
+ if (!isFoundParameter) {
432
+ joinedDef.paths[path].parameters.push(parameter);
433
+ }
434
+ }
435
+ }
436
+
437
+ function collectPathOperation(
438
+ pathItem: Oas3PathItem,
439
+ path: string | number,
440
+ operation: OPENAPI3_METHOD
441
+ ) {
442
+ const pathOperation = pathItem[operation];
443
+
444
+ if (!pathOperation) {
445
+ return;
446
+ }
447
+
448
+ joinedDef.paths[path][operation] = pathOperation;
449
+ potentialConflicts.paths[path][operation] = [
450
+ ...(potentialConflicts.paths[path][operation] || []),
451
+ api,
452
+ ];
453
+
454
+ const { operationId } = pathOperation;
455
+
456
+ if (operationId) {
457
+ if (!potentialConflicts.paths.hasOwnProperty('operationIds')) {
458
+ potentialConflicts.paths['operationIds'] = {};
459
+ }
460
+ potentialConflicts.paths.operationIds[operationId] = [
461
+ ...(potentialConflicts.paths.operationIds[operationId] || []),
462
+ api,
463
+ ];
464
+ }
465
+
466
+ const { tags, security } = joinedDef.paths[path][operation];
467
+
468
+ if (tags) {
469
+ joinedDef.paths[path][operation].tags = tags.map((tag: string) =>
470
+ addPrefix(tag, tagsPrefix)
471
+ );
472
+ populateTags({
473
+ api,
474
+ apiFilename,
475
+ tags: formatTags(tags),
476
+ potentialConflicts,
477
+ tagsPrefix,
478
+ componentsPrefix,
479
+ });
480
+ } else {
481
+ joinedDef.paths[path][operation]['tags'] = [addPrefix('other', tagsPrefix || apiFilename)];
482
+ populateTags({
483
+ api,
484
+ apiFilename,
485
+ tags: formatTags(['other']),
486
+ potentialConflicts,
487
+ tagsPrefix: tagsPrefix || apiFilename,
488
+ componentsPrefix,
489
+ });
490
+ }
491
+ if (!security && openapi.hasOwnProperty('security')) {
492
+ joinedDef.paths[path][operation]['security'] = addSecurityPrefix(
493
+ openapi.security,
494
+ componentsPrefix!
495
+ );
496
+ } else if (pathOperation.security) {
497
+ joinedDef.paths[path][operation].security = addSecurityPrefix(
498
+ pathOperation.security,
499
+ componentsPrefix!
500
+ );
501
+ }
502
+ }
503
+ }
504
+
505
+ function isServersEqual(serverOne: Oas3Server, serverTwo: Oas3Server) {
506
+ if (serverOne.description === serverTwo.description) {
507
+ return isEqual(serverOne.variables, serverTwo.variables);
508
+ }
509
+
510
+ return false;
402
511
  }
403
512
 
404
513
  function collectComponents(
@@ -127,6 +127,11 @@ function lintConfigCallback(argv: LintOptions, version: string) {
127
127
  return;
128
128
  }
129
129
 
130
+ if (argv.format === 'json') {
131
+ // we can't print config lint results as it will break json output
132
+ return;
133
+ }
134
+
130
135
  return async (config: RawConfig) => {
131
136
  const { 'max-problems': maxProblems, format } = argv;
132
137
  const configPath = findConfig(argv.config) || '';
@@ -38,26 +38,26 @@ export const WEBHOOKS = 'webhooks';
38
38
  export const xWEBHOOKS = 'x-webhooks';
39
39
  export const componentsPath = `#/${COMPONENTS}/`;
40
40
 
41
- enum OPENAPI3_METHOD {
42
- Get = 'get',
43
- Put = 'put',
44
- Post = 'post',
45
- Delete = 'delete',
46
- Options = 'options',
47
- Head = 'head',
48
- Patch = 'patch',
49
- Trace = 'trace',
41
+ export enum OPENAPI3_METHOD {
42
+ get = 'get',
43
+ put = 'put',
44
+ post = 'post',
45
+ delete = 'delete',
46
+ options = 'options',
47
+ head = 'head',
48
+ patch = 'patch',
49
+ trace = 'trace',
50
50
  }
51
51
 
52
52
  export const OPENAPI3_METHOD_NAMES: OPENAPI3_METHOD[] = [
53
- OPENAPI3_METHOD.Get,
54
- OPENAPI3_METHOD.Put,
55
- OPENAPI3_METHOD.Post,
56
- OPENAPI3_METHOD.Delete,
57
- OPENAPI3_METHOD.Options,
58
- OPENAPI3_METHOD.Head,
59
- OPENAPI3_METHOD.Patch,
60
- OPENAPI3_METHOD.Trace,
53
+ OPENAPI3_METHOD.get,
54
+ OPENAPI3_METHOD.put,
55
+ OPENAPI3_METHOD.post,
56
+ OPENAPI3_METHOD.delete,
57
+ OPENAPI3_METHOD.options,
58
+ OPENAPI3_METHOD.head,
59
+ OPENAPI3_METHOD.patch,
60
+ OPENAPI3_METHOD.trace,
61
61
  ];
62
62
 
63
63
  export enum OPENAPI3_COMPONENT {
@@ -90,7 +90,7 @@ export async function handleStats(argv: { config?: string; api?: string; format:
90
90
 
91
91
  const resolvedRefMap = await resolveDocument({
92
92
  rootDocument: document,
93
- rootType: types.DefinitionRoot,
93
+ rootType: types.Root,
94
94
  externalRefResolver,
95
95
  });
96
96
 
@@ -107,7 +107,7 @@ export async function handleStats(argv: { config?: string; api?: string; format:
107
107
 
108
108
  walkDocument({
109
109
  document,
110
- rootType: types.DefinitionRoot,
110
+ rootType: types.Root,
111
111
  normalizedVisitors: statsVisitor,
112
112
  resolvedRefMap,
113
113
  ctx,
package/src/index.ts CHANGED
@@ -152,6 +152,7 @@ yargs
152
152
  'json',
153
153
  'checkstyle',
154
154
  'codeclimate',
155
+ 'summary',
155
156
  ] as ReadonlyArray<OutputFormat>,
156
157
  default: 'codeframe' as OutputFormat,
157
158
  },
package/src/js-utils.ts CHANGED
@@ -10,3 +10,8 @@ export function isEmptyObject(obj: any) {
10
10
  export function isString(str: string) {
11
11
  return Object.prototype.toString.call(str) === '[object String]';
12
12
  }
13
+
14
+ export function keysOf<T>(obj: T) {
15
+ if (!obj) return [];
16
+ return Object.keys(obj) as (keyof T)[];
17
+ }
package/src/utils.ts CHANGED
@@ -107,6 +107,7 @@ export function dumpBundle(obj: any, format: BundleOutputFormat, dereference?: b
107
107
  } else {
108
108
  return stringifyYaml(obj, {
109
109
  noRefs: !dereference,
110
+ lineWidth: -1,
110
111
  });
111
112
  }
112
113
  }