@redocly/cli 1.0.0 → 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/CHANGELOG.md +8 -0
- package/lib/commands/build-docs/index.js +2 -4
- package/lib/commands/build-docs/utils.d.ts +1 -1
- package/lib/commands/build-docs/utils.js +3 -3
- package/package.json +2 -2
- package/src/__mocks__/@redocly/openapi-core.ts +80 -0
- package/src/__mocks__/documents.ts +63 -0
- package/src/__mocks__/fs.ts +6 -0
- package/src/__mocks__/perf_hooks.ts +3 -0
- package/src/__mocks__/redoc.ts +2 -0
- package/src/__mocks__/utils.ts +19 -0
- package/src/__tests__/commands/build-docs.test.ts +62 -0
- package/src/__tests__/commands/bundle.test.ts +150 -0
- package/src/__tests__/commands/join.test.ts +122 -0
- package/src/__tests__/commands/lint.test.ts +190 -0
- package/src/__tests__/commands/push-region.test.ts +58 -0
- package/src/__tests__/commands/push.test.ts +492 -0
- package/src/__tests__/fetch-with-timeout.test.ts +35 -0
- package/src/__tests__/fixtures/config.ts +21 -0
- package/src/__tests__/fixtures/openapi.json +0 -0
- package/src/__tests__/fixtures/openapi.yaml +0 -0
- package/src/__tests__/fixtures/redocly.yaml +0 -0
- package/src/__tests__/utils.test.ts +564 -0
- package/src/__tests__/wrapper.test.ts +57 -0
- package/src/assert-node-version.ts +8 -0
- package/src/commands/build-docs/index.ts +50 -0
- package/src/commands/build-docs/template.hbs +23 -0
- package/src/commands/build-docs/types.ts +24 -0
- package/src/commands/build-docs/utils.ts +110 -0
- package/src/commands/bundle.ts +177 -0
- package/src/commands/join.ts +811 -0
- package/src/commands/lint.ts +151 -0
- package/src/commands/login.ts +27 -0
- package/src/commands/preview-docs/index.ts +190 -0
- package/src/commands/preview-docs/preview-server/default.hbs +24 -0
- package/src/commands/preview-docs/preview-server/hot.js +42 -0
- package/src/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
- package/src/commands/preview-docs/preview-server/preview-server.ts +156 -0
- package/src/commands/preview-docs/preview-server/server.ts +91 -0
- package/src/commands/push.ts +441 -0
- package/src/commands/split/__tests__/fixtures/samples.json +61 -0
- package/src/commands/split/__tests__/fixtures/spec.json +70 -0
- package/src/commands/split/__tests__/fixtures/webhooks.json +85 -0
- package/src/commands/split/__tests__/index.test.ts +137 -0
- package/src/commands/split/index.ts +385 -0
- package/src/commands/split/types.ts +85 -0
- package/src/commands/stats.ts +119 -0
- package/src/custom.d.ts +1 -0
- package/src/fetch-with-timeout.ts +21 -0
- package/src/index.ts +484 -0
- package/src/js-utils.ts +17 -0
- package/src/types.ts +40 -0
- package/src/update-version-notifier.ts +106 -0
- package/src/utils.ts +590 -0
- package/src/wrapper.ts +42 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { red, blue, yellow, green } from 'colorette';
|
|
3
|
+
import { performance } from 'perf_hooks';
|
|
4
|
+
const isEqual = require('lodash.isequal');
|
|
5
|
+
import {
|
|
6
|
+
Config,
|
|
7
|
+
Oas3Definition,
|
|
8
|
+
OasVersion,
|
|
9
|
+
BaseResolver,
|
|
10
|
+
Document,
|
|
11
|
+
StyleguideConfig,
|
|
12
|
+
Oas3Tag,
|
|
13
|
+
formatProblems,
|
|
14
|
+
getTotals,
|
|
15
|
+
lintDocument,
|
|
16
|
+
detectOpenAPI,
|
|
17
|
+
bundleDocument,
|
|
18
|
+
Referenced,
|
|
19
|
+
isRef,
|
|
20
|
+
} from '@redocly/openapi-core';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
getFallbackApisOrExit,
|
|
24
|
+
printExecutionTime,
|
|
25
|
+
handleError,
|
|
26
|
+
printLintTotals,
|
|
27
|
+
writeYaml,
|
|
28
|
+
exitWithError,
|
|
29
|
+
sortTopLevelKeysForOas,
|
|
30
|
+
} from '../utils';
|
|
31
|
+
import { isObject, isString, keysOf } from '../js-utils';
|
|
32
|
+
import { Oas3Parameter, Oas3PathItem, Oas3Server } from '@redocly/openapi-core/lib/typings/openapi';
|
|
33
|
+
import { OPENAPI3_METHOD } from './split/types';
|
|
34
|
+
import { BundleResult } from '@redocly/openapi-core/lib/bundle';
|
|
35
|
+
|
|
36
|
+
const COMPONENTS = 'components';
|
|
37
|
+
const Tags = 'tags';
|
|
38
|
+
const xTagGroups = 'x-tagGroups';
|
|
39
|
+
let potentialConflictsTotal = 0;
|
|
40
|
+
|
|
41
|
+
type JoinDocumentContext = {
|
|
42
|
+
api: string;
|
|
43
|
+
apiFilename: string;
|
|
44
|
+
tags: Oas3Tag[];
|
|
45
|
+
potentialConflicts: any;
|
|
46
|
+
tagsPrefix: string;
|
|
47
|
+
componentsPrefix: string | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type JoinOptions = {
|
|
51
|
+
apis: string[];
|
|
52
|
+
lint?: boolean;
|
|
53
|
+
decorate?: boolean;
|
|
54
|
+
preprocess?: boolean;
|
|
55
|
+
'prefix-tags-with-info-prop'?: string;
|
|
56
|
+
'prefix-tags-with-filename'?: boolean;
|
|
57
|
+
'prefix-components-with-info-prop'?: string;
|
|
58
|
+
'without-x-tag-groups'?: boolean;
|
|
59
|
+
output?: string;
|
|
60
|
+
config?: string;
|
|
61
|
+
extends?: undefined;
|
|
62
|
+
'lint-config'?: undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export async function handleJoin(argv: JoinOptions, config: Config, packageVersion: string) {
|
|
66
|
+
const startedAt = performance.now();
|
|
67
|
+
if (argv.apis.length < 2) {
|
|
68
|
+
return exitWithError(`At least 2 apis should be provided. \n\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
'prefix-components-with-info-prop': prefixComponentsWithInfoProp,
|
|
73
|
+
'prefix-tags-with-filename': prefixTagsWithFilename,
|
|
74
|
+
'prefix-tags-with-info-prop': prefixTagsWithInfoProp,
|
|
75
|
+
'without-x-tag-groups': withoutXTagGroups,
|
|
76
|
+
output: specFilename = 'openapi.yaml',
|
|
77
|
+
} = argv;
|
|
78
|
+
|
|
79
|
+
const usedTagsOptions = [
|
|
80
|
+
prefixTagsWithFilename && 'prefix-tags-with-filename',
|
|
81
|
+
prefixTagsWithInfoProp && 'prefix-tags-with-info-prop',
|
|
82
|
+
withoutXTagGroups && 'without-x-tag-groups',
|
|
83
|
+
].filter(Boolean);
|
|
84
|
+
|
|
85
|
+
if (usedTagsOptions.length > 1) {
|
|
86
|
+
return exitWithError(
|
|
87
|
+
`You use ${yellow(usedTagsOptions.join(', '))} together.\nPlease choose only one! \n\n`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const apis = await getFallbackApisOrExit(argv.apis, config);
|
|
92
|
+
const externalRefResolver = new BaseResolver(config.resolve);
|
|
93
|
+
const documents = await Promise.all(
|
|
94
|
+
apis.map(
|
|
95
|
+
({ path }) => externalRefResolver.resolveDocument(null, path, true) as Promise<Document>
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!argv.decorate) {
|
|
100
|
+
const decorators = new Set([
|
|
101
|
+
...Object.keys(config.styleguide.decorators.oas3_0),
|
|
102
|
+
...Object.keys(config.styleguide.decorators.oas3_1),
|
|
103
|
+
...Object.keys(config.styleguide.decorators.oas2),
|
|
104
|
+
]);
|
|
105
|
+
config.styleguide.skipDecorators(Array.from(decorators));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!argv.preprocess) {
|
|
109
|
+
const preprocessors = new Set([
|
|
110
|
+
...Object.keys(config.styleguide.preprocessors.oas3_0),
|
|
111
|
+
...Object.keys(config.styleguide.preprocessors.oas3_1),
|
|
112
|
+
...Object.keys(config.styleguide.preprocessors.oas2),
|
|
113
|
+
]);
|
|
114
|
+
config.styleguide.skipPreprocessors(Array.from(preprocessors));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const bundleResults = await Promise.all(
|
|
118
|
+
documents.map((document) =>
|
|
119
|
+
bundleDocument({
|
|
120
|
+
document,
|
|
121
|
+
config: config.styleguide,
|
|
122
|
+
externalRefResolver,
|
|
123
|
+
}).catch((e) => {
|
|
124
|
+
exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}`);
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
for (const { problems, bundle: document } of bundleResults as BundleResult[]) {
|
|
130
|
+
const fileTotals = getTotals(problems);
|
|
131
|
+
if (fileTotals.errors) {
|
|
132
|
+
formatProblems(problems, {
|
|
133
|
+
totals: fileTotals,
|
|
134
|
+
version: document.parsed.version,
|
|
135
|
+
});
|
|
136
|
+
exitWithError(
|
|
137
|
+
`❌ Errors encountered while bundling ${blue(
|
|
138
|
+
document.source.absoluteRef
|
|
139
|
+
)}: join will not proceed.\n`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const document of documents) {
|
|
145
|
+
try {
|
|
146
|
+
const version = detectOpenAPI(document.parsed);
|
|
147
|
+
if (version !== OasVersion.Version3_0) {
|
|
148
|
+
return exitWithError(
|
|
149
|
+
`Only OpenAPI 3 is supported: ${blue(document.source.absoluteRef)} \n\n`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (argv.lint) {
|
|
158
|
+
for (const document of documents) {
|
|
159
|
+
await validateApi(document, config.styleguide, externalRefResolver, packageVersion);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const joinedDef: any = {};
|
|
164
|
+
const potentialConflicts = {
|
|
165
|
+
tags: {},
|
|
166
|
+
paths: {},
|
|
167
|
+
components: {},
|
|
168
|
+
xWebhooks: {},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
addInfoSectionAndSpecVersion(documents, prefixComponentsWithInfoProp);
|
|
172
|
+
|
|
173
|
+
for (const document of documents) {
|
|
174
|
+
const openapi = document.parsed;
|
|
175
|
+
const { tags, info } = openapi;
|
|
176
|
+
const api = path.relative(process.cwd(), document.source.absoluteRef);
|
|
177
|
+
const apiFilename = getApiFilename(api);
|
|
178
|
+
const tagsPrefix = prefixTagsWithFilename
|
|
179
|
+
? apiFilename
|
|
180
|
+
: getInfoPrefix(info, prefixTagsWithInfoProp, 'tags');
|
|
181
|
+
const componentsPrefix = getInfoPrefix(info, prefixComponentsWithInfoProp, COMPONENTS);
|
|
182
|
+
|
|
183
|
+
if (openapi.hasOwnProperty('x-tagGroups')) {
|
|
184
|
+
process.stderr.write(yellow(`warning: x-tagGroups at ${blue(api)} will be skipped \n`));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const context = {
|
|
188
|
+
api,
|
|
189
|
+
apiFilename,
|
|
190
|
+
tags,
|
|
191
|
+
potentialConflicts,
|
|
192
|
+
tagsPrefix,
|
|
193
|
+
componentsPrefix,
|
|
194
|
+
};
|
|
195
|
+
if (tags) {
|
|
196
|
+
populateTags(context);
|
|
197
|
+
}
|
|
198
|
+
collectServers(openapi);
|
|
199
|
+
collectInfoDescriptions(openapi, context);
|
|
200
|
+
collectExternalDocs(openapi, context);
|
|
201
|
+
collectPaths(openapi, context);
|
|
202
|
+
collectComponents(openapi, context);
|
|
203
|
+
collectXWebhooks(openapi, context);
|
|
204
|
+
if (componentsPrefix) {
|
|
205
|
+
replace$Refs(openapi, componentsPrefix);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
iteratePotentialConflicts(potentialConflicts, withoutXTagGroups);
|
|
210
|
+
const noRefs = true;
|
|
211
|
+
|
|
212
|
+
if (potentialConflictsTotal) {
|
|
213
|
+
return exitWithError(`Please fix conflicts before running ${yellow('join')}.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
writeYaml(sortTopLevelKeysForOas(joinedDef), specFilename, noRefs);
|
|
217
|
+
printExecutionTime('join', startedAt, specFilename);
|
|
218
|
+
|
|
219
|
+
function populateTags({
|
|
220
|
+
api,
|
|
221
|
+
apiFilename,
|
|
222
|
+
tags,
|
|
223
|
+
potentialConflicts,
|
|
224
|
+
tagsPrefix,
|
|
225
|
+
componentsPrefix,
|
|
226
|
+
}: JoinDocumentContext) {
|
|
227
|
+
if (!joinedDef.hasOwnProperty(Tags)) {
|
|
228
|
+
joinedDef[Tags] = [];
|
|
229
|
+
}
|
|
230
|
+
if (!potentialConflicts.tags.hasOwnProperty('all')) {
|
|
231
|
+
potentialConflicts.tags['all'] = {};
|
|
232
|
+
}
|
|
233
|
+
if (withoutXTagGroups && !potentialConflicts.tags.hasOwnProperty('description')) {
|
|
234
|
+
potentialConflicts.tags['description'] = {};
|
|
235
|
+
}
|
|
236
|
+
for (const tag of tags) {
|
|
237
|
+
const entrypointTagName = addPrefix(tag.name, tagsPrefix);
|
|
238
|
+
if (tag.description) {
|
|
239
|
+
tag.description = addComponentsPrefix(tag.description, componentsPrefix!);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const tagDuplicate = joinedDef.tags.find((t: Oas3Tag) => t.name === entrypointTagName);
|
|
243
|
+
|
|
244
|
+
if (tagDuplicate && withoutXTagGroups) {
|
|
245
|
+
// If tag already exist and `without-x-tag-groups` option,
|
|
246
|
+
// check if description are different for potential conflicts warning.
|
|
247
|
+
const isTagDescriptionNotEqual =
|
|
248
|
+
tag.hasOwnProperty('description') && tagDuplicate.description !== tag.description;
|
|
249
|
+
|
|
250
|
+
potentialConflicts.tags.description[entrypointTagName].push(
|
|
251
|
+
...(isTagDescriptionNotEqual ? [api] : [])
|
|
252
|
+
);
|
|
253
|
+
} else if (!tagDuplicate) {
|
|
254
|
+
// Instead add tag to joinedDef if there no duplicate;
|
|
255
|
+
tag['x-displayName'] = tag['x-displayName'] || tag.name;
|
|
256
|
+
tag.name = entrypointTagName;
|
|
257
|
+
joinedDef.tags.push(tag);
|
|
258
|
+
|
|
259
|
+
if (withoutXTagGroups) {
|
|
260
|
+
potentialConflicts.tags.description[entrypointTagName] = [api];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!withoutXTagGroups) {
|
|
265
|
+
createXTagGroups(apiFilename);
|
|
266
|
+
if (!tagDuplicate) {
|
|
267
|
+
populateXTagGroups(entrypointTagName, getIndexGroup(apiFilename));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const doesEntrypointExist =
|
|
272
|
+
!potentialConflicts.tags.all[entrypointTagName] ||
|
|
273
|
+
(potentialConflicts.tags.all[entrypointTagName] &&
|
|
274
|
+
!potentialConflicts.tags.all[entrypointTagName].includes(api));
|
|
275
|
+
potentialConflicts.tags.all[entrypointTagName] = [
|
|
276
|
+
...(potentialConflicts.tags.all[entrypointTagName] || []),
|
|
277
|
+
...(!withoutXTagGroups && doesEntrypointExist ? [api] : []),
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getIndexGroup(apiFilename: string): number {
|
|
283
|
+
return joinedDef[xTagGroups].findIndex((item: any) => item.name === apiFilename);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createXTagGroups(apiFilename: string) {
|
|
287
|
+
if (!joinedDef.hasOwnProperty(xTagGroups)) {
|
|
288
|
+
joinedDef[xTagGroups] = [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!joinedDef[xTagGroups].some((g: any) => g.name === apiFilename)) {
|
|
292
|
+
joinedDef[xTagGroups].push({ name: apiFilename, tags: [] });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const indexGroup = getIndexGroup(apiFilename);
|
|
296
|
+
|
|
297
|
+
if (!joinedDef[xTagGroups][indexGroup].hasOwnProperty(Tags)) {
|
|
298
|
+
joinedDef[xTagGroups][indexGroup][Tags] = [];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function populateXTagGroups(entrypointTagName: string, indexGroup: number) {
|
|
303
|
+
if (
|
|
304
|
+
!joinedDef[xTagGroups][indexGroup][Tags].find((t: Oas3Tag) => t.name === entrypointTagName)
|
|
305
|
+
) {
|
|
306
|
+
joinedDef[xTagGroups][indexGroup][Tags].push(entrypointTagName);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function collectServers(openapi: Oas3Definition) {
|
|
311
|
+
const { servers } = openapi;
|
|
312
|
+
if (servers) {
|
|
313
|
+
if (!joinedDef.hasOwnProperty('servers')) {
|
|
314
|
+
joinedDef['servers'] = [];
|
|
315
|
+
}
|
|
316
|
+
for (const server of servers) {
|
|
317
|
+
if (!joinedDef.servers.some((s: any) => s.url === server.url)) {
|
|
318
|
+
joinedDef.servers.push(server);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function collectInfoDescriptions(
|
|
325
|
+
openapi: Oas3Definition,
|
|
326
|
+
{ apiFilename, componentsPrefix }: JoinDocumentContext
|
|
327
|
+
) {
|
|
328
|
+
const { info } = openapi;
|
|
329
|
+
if (info?.description) {
|
|
330
|
+
const groupIndex = joinedDef[xTagGroups] ? getIndexGroup(apiFilename) : -1;
|
|
331
|
+
if (
|
|
332
|
+
joinedDef.hasOwnProperty(xTagGroups) &&
|
|
333
|
+
groupIndex !== -1 &&
|
|
334
|
+
joinedDef[xTagGroups][groupIndex]['tags'] &&
|
|
335
|
+
joinedDef[xTagGroups][groupIndex]['tags'].length
|
|
336
|
+
) {
|
|
337
|
+
joinedDef[xTagGroups][groupIndex]['description'] = addComponentsPrefix(
|
|
338
|
+
info.description,
|
|
339
|
+
componentsPrefix!
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function collectExternalDocs(openapi: Oas3Definition, { api }: JoinDocumentContext) {
|
|
346
|
+
const { externalDocs } = openapi;
|
|
347
|
+
if (externalDocs) {
|
|
348
|
+
if (joinedDef.hasOwnProperty('externalDocs')) {
|
|
349
|
+
process.stderr.write(
|
|
350
|
+
yellow(`warning: skip externalDocs from ${blue(path.basename(api))} \n`)
|
|
351
|
+
);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
joinedDef['externalDocs'] = externalDocs;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function collectPaths(
|
|
359
|
+
openapi: Oas3Definition,
|
|
360
|
+
{ apiFilename, api, potentialConflicts, tagsPrefix, componentsPrefix }: JoinDocumentContext
|
|
361
|
+
) {
|
|
362
|
+
const { paths } = openapi;
|
|
363
|
+
const operationsSet = new Set(keysOf<typeof OPENAPI3_METHOD>(OPENAPI3_METHOD));
|
|
364
|
+
if (paths) {
|
|
365
|
+
if (!joinedDef.hasOwnProperty('paths')) {
|
|
366
|
+
joinedDef['paths'] = {};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const path of keysOf(paths)) {
|
|
370
|
+
if (!joinedDef.paths.hasOwnProperty(path)) {
|
|
371
|
+
joinedDef.paths[path] = {};
|
|
372
|
+
}
|
|
373
|
+
if (!potentialConflicts.paths.hasOwnProperty(path)) {
|
|
374
|
+
potentialConflicts.paths[path] = {};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const pathItem = paths[path] as Oas3PathItem;
|
|
378
|
+
|
|
379
|
+
for (const field of keysOf(pathItem)) {
|
|
380
|
+
if (operationsSet.has(field as OPENAPI3_METHOD)) {
|
|
381
|
+
collectPathOperation(pathItem, path, field as OPENAPI3_METHOD);
|
|
382
|
+
}
|
|
383
|
+
if (field === 'servers') {
|
|
384
|
+
collectPathServers(pathItem, path);
|
|
385
|
+
}
|
|
386
|
+
if (field === 'parameters') {
|
|
387
|
+
collectPathParameters(pathItem, path);
|
|
388
|
+
}
|
|
389
|
+
if (typeof pathItem[field] === 'string') {
|
|
390
|
+
collectPathStringFields(pathItem, path, field);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function collectPathStringFields(
|
|
397
|
+
pathItem: Oas3PathItem,
|
|
398
|
+
path: string | number,
|
|
399
|
+
field: keyof Oas3PathItem
|
|
400
|
+
) {
|
|
401
|
+
const fieldValue = pathItem[field];
|
|
402
|
+
if (
|
|
403
|
+
joinedDef.paths[path].hasOwnProperty(field) &&
|
|
404
|
+
joinedDef.paths[path][field] !== fieldValue
|
|
405
|
+
) {
|
|
406
|
+
process.stderr.write(yellow(`warning: different ${field} values in ${path}\n`));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
joinedDef.paths[path][field] = fieldValue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function collectPathServers(pathItem: Oas3PathItem, path: string | number) {
|
|
413
|
+
if (!pathItem.servers) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!joinedDef.paths[path].hasOwnProperty('servers')) {
|
|
418
|
+
joinedDef.paths[path].servers = [];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const server of pathItem.servers) {
|
|
422
|
+
let isFoundServer = false;
|
|
423
|
+
for (const pathServer of joinedDef.paths[path].servers) {
|
|
424
|
+
if (pathServer.url === server.url) {
|
|
425
|
+
if (!isServersEqual(pathServer, server)) {
|
|
426
|
+
exitWithError(`Different server values for (${server.url}) in ${path}`);
|
|
427
|
+
}
|
|
428
|
+
isFoundServer = true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!isFoundServer) {
|
|
433
|
+
joinedDef.paths[path].servers.push(server);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function collectPathParameters(pathItem: Oas3PathItem, path: string | number) {
|
|
439
|
+
if (!pathItem.parameters) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!joinedDef.paths[path].hasOwnProperty('parameters')) {
|
|
443
|
+
joinedDef.paths[path].parameters = [];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const parameter of pathItem.parameters as Referenced<Oas3Parameter>[]) {
|
|
447
|
+
let isFoundParameter = false;
|
|
448
|
+
|
|
449
|
+
for (const pathParameter of joinedDef.paths[path]
|
|
450
|
+
.parameters as Referenced<Oas3Parameter>[]) {
|
|
451
|
+
// Compare $ref only if both are reference objects
|
|
452
|
+
if (isRef(pathParameter) && isRef(parameter)) {
|
|
453
|
+
if (pathParameter['$ref'] === parameter['$ref']) {
|
|
454
|
+
isFoundParameter = true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Compare properties only if both are reference objects
|
|
458
|
+
if (!isRef(pathParameter) && !isRef(parameter)) {
|
|
459
|
+
if (pathParameter.name === parameter.name && pathParameter.in === parameter.in) {
|
|
460
|
+
if (!isEqual(pathParameter.schema, parameter.schema)) {
|
|
461
|
+
exitWithError(`Different parameter schemas for (${parameter.name}) in ${path}`);
|
|
462
|
+
}
|
|
463
|
+
isFoundParameter = true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!isFoundParameter) {
|
|
469
|
+
joinedDef.paths[path].parameters.push(parameter);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function collectPathOperation(
|
|
475
|
+
pathItem: Oas3PathItem,
|
|
476
|
+
path: string | number,
|
|
477
|
+
operation: OPENAPI3_METHOD
|
|
478
|
+
) {
|
|
479
|
+
const pathOperation = pathItem[operation];
|
|
480
|
+
|
|
481
|
+
if (!pathOperation) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
joinedDef.paths[path][operation] = pathOperation;
|
|
486
|
+
potentialConflicts.paths[path][operation] = [
|
|
487
|
+
...(potentialConflicts.paths[path][operation] || []),
|
|
488
|
+
api,
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
const { operationId } = pathOperation;
|
|
492
|
+
|
|
493
|
+
if (operationId) {
|
|
494
|
+
if (!potentialConflicts.paths.hasOwnProperty('operationIds')) {
|
|
495
|
+
potentialConflicts.paths['operationIds'] = {};
|
|
496
|
+
}
|
|
497
|
+
potentialConflicts.paths.operationIds[operationId] = [
|
|
498
|
+
...(potentialConflicts.paths.operationIds[operationId] || []),
|
|
499
|
+
api,
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const { tags, security } = joinedDef.paths[path][operation];
|
|
504
|
+
|
|
505
|
+
if (tags) {
|
|
506
|
+
joinedDef.paths[path][operation].tags = tags.map((tag: string) =>
|
|
507
|
+
addPrefix(tag, tagsPrefix)
|
|
508
|
+
);
|
|
509
|
+
populateTags({
|
|
510
|
+
api,
|
|
511
|
+
apiFilename,
|
|
512
|
+
tags: formatTags(tags),
|
|
513
|
+
potentialConflicts,
|
|
514
|
+
tagsPrefix,
|
|
515
|
+
componentsPrefix,
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
joinedDef.paths[path][operation]['tags'] = [addPrefix('other', tagsPrefix || apiFilename)];
|
|
519
|
+
populateTags({
|
|
520
|
+
api,
|
|
521
|
+
apiFilename,
|
|
522
|
+
tags: formatTags(['other']),
|
|
523
|
+
potentialConflicts,
|
|
524
|
+
tagsPrefix: tagsPrefix || apiFilename,
|
|
525
|
+
componentsPrefix,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (!security && openapi.hasOwnProperty('security')) {
|
|
529
|
+
joinedDef.paths[path][operation]['security'] = addSecurityPrefix(
|
|
530
|
+
openapi.security,
|
|
531
|
+
componentsPrefix!
|
|
532
|
+
);
|
|
533
|
+
} else if (pathOperation.security) {
|
|
534
|
+
joinedDef.paths[path][operation].security = addSecurityPrefix(
|
|
535
|
+
pathOperation.security,
|
|
536
|
+
componentsPrefix!
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function isServersEqual(serverOne: Oas3Server, serverTwo: Oas3Server) {
|
|
543
|
+
if (serverOne.description === serverTwo.description) {
|
|
544
|
+
return isEqual(serverOne.variables, serverTwo.variables);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function collectComponents(
|
|
551
|
+
openapi: Oas3Definition,
|
|
552
|
+
{ api, potentialConflicts, componentsPrefix }: JoinDocumentContext
|
|
553
|
+
) {
|
|
554
|
+
const { components } = openapi;
|
|
555
|
+
if (components) {
|
|
556
|
+
if (!joinedDef.hasOwnProperty(COMPONENTS)) {
|
|
557
|
+
joinedDef[COMPONENTS] = {};
|
|
558
|
+
}
|
|
559
|
+
for (const [component, componentObj] of Object.entries(components)) {
|
|
560
|
+
if (!potentialConflicts[COMPONENTS].hasOwnProperty(component)) {
|
|
561
|
+
potentialConflicts[COMPONENTS][component] = {};
|
|
562
|
+
joinedDef[COMPONENTS][component] = {};
|
|
563
|
+
}
|
|
564
|
+
for (const item of Object.keys(componentObj)) {
|
|
565
|
+
const componentPrefix = addPrefix(item, componentsPrefix!);
|
|
566
|
+
potentialConflicts.components[component][componentPrefix] = [
|
|
567
|
+
...(potentialConflicts.components[component][item] || []),
|
|
568
|
+
{ [api]: componentObj[item] },
|
|
569
|
+
];
|
|
570
|
+
joinedDef.components[component][componentPrefix] = componentObj[item];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function collectXWebhooks(
|
|
577
|
+
openapi: Oas3Definition,
|
|
578
|
+
{ apiFilename, api, potentialConflicts, tagsPrefix, componentsPrefix }: JoinDocumentContext
|
|
579
|
+
) {
|
|
580
|
+
const xWebhooks = 'x-webhooks';
|
|
581
|
+
const openapiXWebhooks = openapi[xWebhooks];
|
|
582
|
+
if (openapiXWebhooks) {
|
|
583
|
+
if (!joinedDef.hasOwnProperty(xWebhooks)) {
|
|
584
|
+
joinedDef[xWebhooks] = {};
|
|
585
|
+
}
|
|
586
|
+
for (const webhook of Object.keys(openapiXWebhooks)) {
|
|
587
|
+
joinedDef[xWebhooks][webhook] = openapiXWebhooks[webhook];
|
|
588
|
+
|
|
589
|
+
if (!potentialConflicts.xWebhooks.hasOwnProperty(webhook)) {
|
|
590
|
+
potentialConflicts.xWebhooks[webhook] = {};
|
|
591
|
+
}
|
|
592
|
+
for (const operation of Object.keys(openapiXWebhooks[webhook])) {
|
|
593
|
+
potentialConflicts.xWebhooks[webhook][operation] = [
|
|
594
|
+
...(potentialConflicts.xWebhooks[webhook][operation] || []),
|
|
595
|
+
api,
|
|
596
|
+
];
|
|
597
|
+
}
|
|
598
|
+
for (const operationKey of Object.keys(joinedDef[xWebhooks][webhook])) {
|
|
599
|
+
const { tags } = joinedDef[xWebhooks][webhook][operationKey];
|
|
600
|
+
if (tags) {
|
|
601
|
+
joinedDef[xWebhooks][webhook][operationKey].tags = tags.map((tag: string) =>
|
|
602
|
+
addPrefix(tag, tagsPrefix)
|
|
603
|
+
);
|
|
604
|
+
populateTags({
|
|
605
|
+
api,
|
|
606
|
+
apiFilename,
|
|
607
|
+
tags: formatTags(tags),
|
|
608
|
+
potentialConflicts,
|
|
609
|
+
tagsPrefix,
|
|
610
|
+
componentsPrefix,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function addInfoSectionAndSpecVersion(
|
|
619
|
+
documents: any,
|
|
620
|
+
prefixComponentsWithInfoProp: string | undefined
|
|
621
|
+
) {
|
|
622
|
+
const firstApi = documents[0];
|
|
623
|
+
const openapi = firstApi.parsed;
|
|
624
|
+
const componentsPrefix = getInfoPrefix(openapi.info, prefixComponentsWithInfoProp, COMPONENTS);
|
|
625
|
+
if (!openapi.openapi) exitWithError('Version of specification is not found in. \n');
|
|
626
|
+
if (!openapi.info) exitWithError('Info section is not found in specification. \n');
|
|
627
|
+
if (openapi.info?.description) {
|
|
628
|
+
openapi.info.description = addComponentsPrefix(openapi.info.description, componentsPrefix);
|
|
629
|
+
}
|
|
630
|
+
joinedDef.openapi = openapi.openapi;
|
|
631
|
+
joinedDef.info = openapi.info;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function doesComponentsDiffer(curr: object, next: object) {
|
|
636
|
+
return !isEqual(Object.values(curr)[0], Object.values(next)[0]);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function validateComponentsDifference(files: any) {
|
|
640
|
+
let isDiffer = false;
|
|
641
|
+
for (let i = 0, len = files.length; i < len; i++) {
|
|
642
|
+
const next = files[i + 1];
|
|
643
|
+
if (next && doesComponentsDiffer(files[i], next)) {
|
|
644
|
+
isDiffer = true;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return isDiffer;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function iteratePotentialConflicts(potentialConflicts: any, withoutXTagGroups?: boolean) {
|
|
651
|
+
for (const group of Object.keys(potentialConflicts)) {
|
|
652
|
+
for (const [key, value] of Object.entries(potentialConflicts[group])) {
|
|
653
|
+
const conflicts = filterConflicts(value as object);
|
|
654
|
+
if (conflicts.length) {
|
|
655
|
+
if (group === COMPONENTS) {
|
|
656
|
+
for (const [_, conflict] of Object.entries(conflicts)) {
|
|
657
|
+
if (validateComponentsDifference(conflict[1])) {
|
|
658
|
+
conflict[1] = conflict[1].map((c: string) => Object.keys(c)[0]);
|
|
659
|
+
showConflicts(green(group) + ' => ' + key, [conflict]);
|
|
660
|
+
potentialConflictsTotal += 1;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
if (withoutXTagGroups && group === 'tags') {
|
|
665
|
+
duplicateTagDescriptionWarning(conflicts);
|
|
666
|
+
} else {
|
|
667
|
+
potentialConflictsTotal += conflicts.length;
|
|
668
|
+
showConflicts(green(group) + ' => ' + key, conflicts);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (group === 'tags' && !withoutXTagGroups) {
|
|
673
|
+
prefixTagSuggestion(conflicts.length);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function duplicateTagDescriptionWarning(conflicts: [string, any][]) {
|
|
681
|
+
const tagsKeys = conflicts.map(([tagName]) => `\`${tagName}\``);
|
|
682
|
+
const joinString = yellow(', ');
|
|
683
|
+
process.stderr.write(
|
|
684
|
+
yellow(
|
|
685
|
+
`\nwarning: ${tagsKeys.length} conflict(s) on the ${red(
|
|
686
|
+
tagsKeys.join(joinString)
|
|
687
|
+
)} tags description.\n`
|
|
688
|
+
)
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function prefixTagSuggestion(conflictsLength: number) {
|
|
693
|
+
process.stderr.write(
|
|
694
|
+
green(
|
|
695
|
+
`\n${conflictsLength} conflict(s) on tags.\nSuggestion: please use ${blue(
|
|
696
|
+
'prefix-tags-with-filename'
|
|
697
|
+
)}, ${blue('prefix-tags-with-info-prop')} or ${blue(
|
|
698
|
+
'without-x-tag-groups'
|
|
699
|
+
)} to prevent naming conflicts.\n\n`
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function showConflicts(key: string, conflicts: any) {
|
|
705
|
+
for (const [path, files] of conflicts) {
|
|
706
|
+
process.stderr.write(yellow(`Conflict on ${key} : ${red(path)} in files: ${blue(files)} \n`));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function filterConflicts(entities: object) {
|
|
711
|
+
return Object.entries(entities).filter(([_, files]) => files.length > 1);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function getApiFilename(filePath: string) {
|
|
715
|
+
return path.basename(filePath, path.extname(filePath));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function addPrefix(tag: string, tagsPrefix: string) {
|
|
719
|
+
return tagsPrefix ? tagsPrefix + '_' + tag : tag;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function formatTags(tags: string[]) {
|
|
723
|
+
return tags.map((tag: string) => ({ name: tag }));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function addComponentsPrefix(description: string, componentsPrefix: string) {
|
|
727
|
+
return description.replace(/"(#\/components\/.*?)"/g, (match) => {
|
|
728
|
+
const componentName = path.basename(match);
|
|
729
|
+
return match.replace(componentName, addPrefix(componentName, componentsPrefix));
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function addSecurityPrefix(security: any, componentsPrefix: string) {
|
|
734
|
+
return componentsPrefix
|
|
735
|
+
? security?.map((s: any) => {
|
|
736
|
+
const key = Object.keys(s)[0];
|
|
737
|
+
return { [componentsPrefix + '_' + key]: s[key] };
|
|
738
|
+
})
|
|
739
|
+
: security;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function getInfoPrefix(info: any, prefixArg: string | undefined, type: string) {
|
|
743
|
+
if (!prefixArg) return '';
|
|
744
|
+
if (!info) exitWithError('Info section is not found in specification. \n');
|
|
745
|
+
if (!info[prefixArg])
|
|
746
|
+
exitWithError(
|
|
747
|
+
`${yellow(`prefix-${type}-with-info-prop`)} argument value is not found in info section. \n`
|
|
748
|
+
);
|
|
749
|
+
if (!isString(info[prefixArg]))
|
|
750
|
+
exitWithError(
|
|
751
|
+
`${yellow(`prefix-${type}-with-info-prop`)} argument value should be string. \n\n`
|
|
752
|
+
);
|
|
753
|
+
if (info[prefixArg].length > 50)
|
|
754
|
+
exitWithError(
|
|
755
|
+
`${yellow(
|
|
756
|
+
`prefix-${type}-with-info-prop`
|
|
757
|
+
)} argument value length should not exceed 50 characters. \n\n`
|
|
758
|
+
);
|
|
759
|
+
return info[prefixArg];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function validateApi(
|
|
763
|
+
document: Document,
|
|
764
|
+
config: StyleguideConfig,
|
|
765
|
+
externalRefResolver: BaseResolver,
|
|
766
|
+
packageVersion: string
|
|
767
|
+
) {
|
|
768
|
+
try {
|
|
769
|
+
const results = await lintDocument({ document, config, externalRefResolver });
|
|
770
|
+
const fileTotals = getTotals(results);
|
|
771
|
+
formatProblems(results, { format: 'stylish', totals: fileTotals, version: packageVersion });
|
|
772
|
+
printLintTotals(fileTotals, 2);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
handleError(err, document.parsed);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function crawl(object: any, visitor: any) {
|
|
779
|
+
if (!isObject(object)) return;
|
|
780
|
+
for (const key of Object.keys(object)) {
|
|
781
|
+
visitor(object, key);
|
|
782
|
+
crawl(object[key], visitor);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function replace$Refs(obj: any, componentsPrefix: string) {
|
|
787
|
+
crawl(obj, (node: any) => {
|
|
788
|
+
if (node.$ref && isString(node.$ref) && node.$ref.startsWith(`#/${COMPONENTS}/`)) {
|
|
789
|
+
const name = path.basename(node.$ref);
|
|
790
|
+
node.$ref = node.$ref.replace(name, componentsPrefix + '_' + name);
|
|
791
|
+
} else if (
|
|
792
|
+
node.discriminator &&
|
|
793
|
+
node.discriminator.mapping &&
|
|
794
|
+
isObject(node.discriminator.mapping)
|
|
795
|
+
) {
|
|
796
|
+
const { mapping } = node.discriminator;
|
|
797
|
+
for (const name of Object.keys(mapping)) {
|
|
798
|
+
if (isString(mapping[name]) && mapping[name].startsWith(`#/${COMPONENTS}/`)) {
|
|
799
|
+
mapping[name] = mapping[name]
|
|
800
|
+
.split('/')
|
|
801
|
+
.map((name: string, i: number, arr: []) => {
|
|
802
|
+
return arr.length - 1 === i && !name.includes(componentsPrefix)
|
|
803
|
+
? componentsPrefix + '_' + name
|
|
804
|
+
: name;
|
|
805
|
+
})
|
|
806
|
+
.join('/');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|