Skip to content

Commit 97da1bd

Browse files
committed
feat: introduce basic OAS3.2 support
1 parent c3d9b58 commit 97da1bd

35 files changed

+786
-54
lines changed

docs/@v2/rules/ruleset-templates.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ The default behavior is to use a recommended ruleset.
257257
The recommended rulesets for each format are listed below.
258258
There is also a "recommended-strict" ruleset which is identical but with all `warn` settings changed to `error`.
259259

260-
### Recommended ruleset: OpenAPI 3.1
260+
### Recommended ruleset: OpenAPI 3.1/3.2
261261

262262
```yaml
263263
rules:
@@ -297,6 +297,7 @@ rules:
297297
required-string-property-missing-min-length: off
298298
response-contains-header: off
299299
scalar-property-missing-example: off
300+
no-duplicated-tag-names: warn
300301
no-required-schema-properties-undefined: warn
301302
no-schema-type-mismatch: error
302303
no-enum-type-mismatch: error

packages/cli/src/__tests__/commands/join.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('handleJoin', () => {
152152
version: 'cli-version',
153153
});
154154
expect(exitWithError).toHaveBeenCalledWith(
155-
'Only OpenAPI 3.0 and OpenAPI 3.1 are supported: undefined.'
155+
'Only OpenAPI 3.0, 3.1, and 3.2 are supported: undefined.'
156156
);
157157
});
158158

packages/cli/src/__tests__/fixtures/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const configFixture: Config = {
2020
oas2: {},
2121
oas3_0: {},
2222
oas3_1: {},
23+
oas3_2: {},
2324
async2: {},
2425
async3: {},
2526
arazzo1: {},
@@ -29,6 +30,7 @@ export const configFixture: Config = {
2930
oas2: {},
3031
oas3_0: {},
3132
oas3_1: {},
33+
oas3_2: {},
3234
async2: {},
3335
async3: {},
3436
arazzo1: {},
@@ -40,6 +42,7 @@ export const configFixture: Config = {
4042
oas2: {},
4143
oas3_0: {},
4244
oas3_1: {},
45+
oas3_2: {},
4346
async2: {},
4447
async3: {},
4548
arazzo1: {},

packages/cli/src/__tests__/utils.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ describe('checkIfRulesetExist', () => {
540540
oas2: {},
541541
oas3_0: {},
542542
oas3_1: {},
543+
oas3_2: {},
543544
async2: {},
544545
async3: {},
545546
arazzo1: {},

packages/cli/src/commands/join.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ import type {
3333
BundleResult,
3434
Oas3Definition,
3535
Oas3_1Definition,
36+
Oas3_2Definition,
3637
Oas3Parameter,
3738
Oas3PathItem,
3839
Oas3Server,
3940
Oas3Tag,
41+
Oas3_2Tag,
4042
SpecVersion,
4143
} from '@redocly/openapi-core';
4244
import type { CommandArgs } from '../wrapper.js';
@@ -46,14 +48,17 @@ const Tags = 'tags';
4648
const xTagGroups = 'x-tagGroups';
4749
let potentialConflictsTotal = 0;
4850

51+
type AnyOas3Definition = Oas3Definition | Oas3_1Definition | Oas3_2Definition;
52+
4953
type JoinDocumentContext = {
5054
api: string;
5155
apiFilename: string;
5256
apiTitle?: string;
53-
tags?: Oas3Tag[];
57+
tags?: (Oas3Tag | Oas3_2Tag)[];
5458
potentialConflicts: any;
5559
tagsPrefix: string;
5660
componentsPrefix: string | undefined;
61+
oasVersion: Extract<SpecVersion, 'oas3_0' | 'oas3_1' | 'oas3_2'> | null;
5762
};
5863

5964
export type JoinArgv = {
@@ -154,9 +159,9 @@ export async function handleJoin({
154159
try {
155160
const version = detectSpec(document.parsed);
156161
collectSpecData?.(document.parsed);
157-
if (version !== 'oas3_0' && version !== 'oas3_1') {
162+
if (version !== 'oas3_0' && version !== 'oas3_1' && version !== 'oas3_2') {
158163
return exitWithError(
159-
`Only OpenAPI 3.0 and OpenAPI 3.1 are supported: ${blue(document.source.absoluteRef)}.`
164+
`Only OpenAPI 3.0, 3.1, and 3.2 are supported: ${blue(document.source.absoluteRef)}.`
160165
);
161166
}
162167

@@ -171,7 +176,7 @@ export async function handleJoin({
171176
}
172177
}
173178

174-
const [first, ...others] = (documents ?? []) as Document<Oas3Definition | Oas3_1Definition>[];
179+
const [first, ...others] = (documents ?? []) as Document<AnyOas3Definition>[];
175180
const serversAreTheSame = others.every(({ parsed: { paths, servers } }) => {
176181
// include only documents with paths
177182
if (!paths || isEmptyObject(paths || {})) {
@@ -197,9 +202,9 @@ export async function handleJoin({
197202
}
198203

199204
for (const document of documents) {
200-
const openapi = isPlainObject<Oas3Definition | Oas3_1Definition>(document.parsed)
205+
const openapi = isPlainObject<AnyOas3Definition>(document.parsed)
201206
? document.parsed
202-
: ({} as Oas3Definition | Oas3_1Definition);
207+
: ({} as AnyOas3Definition);
203208
const { tags, info } = openapi;
204209
const api = path.relative(process.cwd(), document.source.absoluteRef);
205210
const apiFilename = getApiFilename(api);
@@ -220,14 +225,15 @@ export async function handleJoin({
220225
potentialConflicts,
221226
tagsPrefix,
222227
componentsPrefix,
228+
oasVersion,
223229
};
224230
if (tags) {
225231
populateTags(context);
226232
}
227233
collectExternalDocs(openapi, context);
228234
collectPaths(openapi, context, serversAreTheSame);
229235
collectComponents(openapi, context);
230-
collectWebhooks(oasVersion!, openapi, context);
236+
collectWebhooks(openapi, context);
231237
if (componentsPrefix) {
232238
replace$Refs(openapi, componentsPrefix);
233239
}
@@ -252,6 +258,7 @@ export async function handleJoin({
252258
potentialConflicts,
253259
tagsPrefix,
254260
componentsPrefix,
261+
oasVersion,
255262
}: JoinDocumentContext) {
256263
if (!joinedDef.hasOwnProperty(Tags)) {
257264
joinedDef[Tags] = [];
@@ -268,7 +275,9 @@ export async function handleJoin({
268275
tag.description = addComponentsPrefix(tag.description, componentsPrefix!);
269276
}
270277

271-
const tagDuplicate = joinedDef.tags.find((t: Oas3Tag) => t.name === entrypointTagName);
278+
const tagDuplicate = joinedDef.tags.find(
279+
(t: Oas3Tag | Oas3_2Tag) => t.name === entrypointTagName
280+
);
272281

273282
if (tagDuplicate && withoutXTagGroups) {
274283
// If tag already exist and `without-x-tag-groups` option,
@@ -281,7 +290,11 @@ export async function handleJoin({
281290
);
282291
} else if (!tagDuplicate) {
283292
// Instead add tag to joinedDef if there no duplicate;
284-
tag['x-displayName'] = tag['x-displayName'] || tag.name;
293+
if (oasVersion === 'oas3_0' || oasVersion === 'oas3_1') {
294+
(tag as Oas3Tag)['x-displayName'] = (tag as Oas3Tag)['x-displayName'] || tag.name;
295+
} else if (oasVersion === 'oas3_2') {
296+
(tag as Oas3_2Tag).summary = (tag as Oas3_2Tag).summary || tag.name;
297+
}
285298
tag.name = entrypointTagName;
286299
joinedDef.tags.push(tag);
287300

@@ -290,7 +303,7 @@ export async function handleJoin({
290303
}
291304
}
292305

293-
if (!withoutXTagGroups) {
306+
if (!withoutXTagGroups && oasVersion !== 'oas3_2') {
294307
const groupName = apiTitle || apiFilename;
295308
createXTagGroups(groupName);
296309
if (!tagDuplicate) {
@@ -337,10 +350,7 @@ export async function handleJoin({
337350
}
338351
}
339352

340-
function collectExternalDocs(
341-
openapi: Oas3Definition | Oas3_1Definition,
342-
{ api }: JoinDocumentContext
343-
) {
353+
function collectExternalDocs(openapi: AnyOas3Definition, { api }: JoinDocumentContext) {
344354
const { externalDocs } = openapi;
345355
if (externalDocs) {
346356
if (joinedDef.hasOwnProperty('externalDocs')) {
@@ -352,14 +362,15 @@ export async function handleJoin({
352362
}
353363

354364
function collectPaths(
355-
openapi: Oas3Definition | Oas3_1Definition,
365+
openapi: AnyOas3Definition,
356366
{
357367
apiFilename,
358368
apiTitle,
359369
api,
360370
potentialConflicts,
361371
tagsPrefix,
362372
componentsPrefix,
373+
oasVersion,
363374
}: JoinDocumentContext,
364375
serversAreTheSame: boolean
365376
) {
@@ -521,6 +532,7 @@ export async function handleJoin({
521532
potentialConflicts,
522533
tagsPrefix,
523534
componentsPrefix,
535+
oasVersion,
524536
});
525537
} else {
526538
joinedDef.paths[path][operation]['tags'] = [addPrefix('other', tagsPrefix || apiFilename)];
@@ -532,6 +544,7 @@ export async function handleJoin({
532544
potentialConflicts,
533545
tagsPrefix: tagsPrefix || apiFilename,
534546
componentsPrefix,
547+
oasVersion,
535548
});
536549
}
537550
if (!security && openapi.hasOwnProperty('security')) {
@@ -557,7 +570,7 @@ export async function handleJoin({
557570
}
558571

559572
function collectComponents(
560-
openapi: Oas3Definition | Oas3_1Definition,
573+
openapi: AnyOas3Definition,
561574
{ api, potentialConflicts, componentsPrefix }: JoinDocumentContext
562575
) {
563576
const { components } = openapi;
@@ -583,19 +596,19 @@ export async function handleJoin({
583596
}
584597

585598
function collectWebhooks(
586-
oasVersion: SpecVersion,
587-
openapi: Oas3Definition | Oas3_1Definition,
599+
openapi: AnyOas3Definition,
588600
{
589601
apiFilename,
590602
apiTitle,
591603
api,
592604
potentialConflicts,
593605
tagsPrefix,
594606
componentsPrefix,
607+
oasVersion,
595608
}: JoinDocumentContext
596609
) {
597610
const webhooks = oasVersion === 'oas3_1' ? 'webhooks' : 'x-webhooks';
598-
const openapiWebhooks = (openapi as Exact<Oas3Definition | Oas3_1Definition>)[webhooks];
611+
const openapiWebhooks = (openapi as Exact<AnyOas3Definition>)[webhooks];
599612
if (openapiWebhooks) {
600613
if (!joinedDef.hasOwnProperty(webhooks)) {
601614
joinedDef[webhooks] = {};
@@ -626,6 +639,7 @@ export async function handleJoin({
626639
potentialConflicts,
627640
tagsPrefix,
628641
componentsPrefix,
642+
oasVersion,
629643
});
630644
}
631645
}

packages/cli/src/commands/split/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { COMPONENTS, OPENAPI3_METHOD_NAMES, OPENAPI3_COMPONENT_NAMES } from './t
2727
import type {
2828
Oas3Definition,
2929
Oas3_1Definition,
30+
Oas3_2Definition,
3031
Oas2Definition,
3132
Oas3Schema,
3233
Oas3_1Schema,
@@ -41,6 +42,8 @@ import type { ComponentsFiles, Definition, Oas3Component, RefObject } from './ty
4142
import type { CommandArgs } from '../../wrapper.js';
4243
import type { VerifyConfigOptions } from '../../types.js';
4344

45+
type AnyOas3Definition = Oas3Definition | Oas3_1Definition | Oas3_2Definition;
46+
4447
export type SplitArgv = {
4548
api: string;
4649
outDir: string;
@@ -52,7 +55,7 @@ export async function handleSplit({ argv, collectSpecData }: CommandArgs<SplitAr
5255
const { api, outDir, separator } = argv;
5356
validateDefinitionFileName(api);
5457
const ext = getAndValidateFileExtension(api);
55-
const openapi = readYaml(api) as Oas3Definition | Oas3_1Definition;
58+
const openapi = readYaml(api) as AnyOas3Definition;
5659
collectSpecData?.(openapi);
5760
splitDefinition(openapi, outDir, separator, ext);
5861
logger.info(
@@ -63,7 +66,7 @@ export async function handleSplit({ argv, collectSpecData }: CommandArgs<SplitAr
6366
}
6467

6568
function splitDefinition(
66-
openapi: Oas3Definition | Oas3_1Definition,
69+
openapi: AnyOas3Definition,
6770
openapiDir: string,
6871
pathSeparator: string,
6972
ext: string
@@ -82,7 +85,8 @@ function splitDefinition(
8285
ext
8386
);
8487
const webhooks =
85-
(openapi as Oas3_1Definition).webhooks || (openapi as Oas3Definition)['x-webhooks'];
88+
(openapi as Oas3_1Definition | Oas3_2Definition).webhooks ||
89+
(openapi as Oas3Definition)['x-webhooks'];
8690
// use webhook_ prefix for code samples to prevent potential name-clashes with paths samples
8791
iteratePathItems(
8892
webhooks,
@@ -119,7 +123,7 @@ function validateDefinitionFileName(fileName: string) {
119123
const file = loadFile(fileName);
120124
if ((file as Oas2Definition).swagger)
121125
exitWithError('OpenAPI 2 is not supported by this command.');
122-
if (!(file as Oas3Definition | Oas3_1Definition).openapi)
126+
if (!(file as AnyOas3Definition).openapi)
123127
exitWithError(
124128
'File does not conform to the OpenAPI Specification. OpenAPI version is not specified.'
125129
);
@@ -242,7 +246,7 @@ function doesFileDiffer(filename: string, componentData: any) {
242246
}
243247

244248
function removeEmptyComponents(
245-
openapi: Oas3Definition | Oas3_1Definition,
249+
openapi: AnyOas3Definition,
246250
componentType: Oas3ComponentName<Oas3Schema | Oas3_1Schema>
247251
) {
248252
if (openapi.components && isEmptyObject(openapi.components[componentType])) {
@@ -339,7 +343,7 @@ function iteratePathItems(
339343
}
340344

341345
function iterateComponents(
342-
openapi: Oas3Definition | Oas3_1Definition,
346+
openapi: AnyOas3Definition,
343347
openapiDir: string,
344348
componentsFiles: ComponentsFiles,
345349
ext: string

packages/cli/src/commands/split/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import type { Oas2Definition, Oas3_1Definition, Oas3Definition } from '@redocly/openapi-core';
1+
import type {
2+
Oas2Definition,
3+
Oas3Definition,
4+
Oas3_1Definition,
5+
Oas3_2Definition,
6+
} from '@redocly/openapi-core';
27

3-
export type Definition = Oas3_1Definition | Oas3Definition | Oas2Definition;
8+
export type Definition = Oas2Definition | Oas3Definition | Oas3_1Definition | Oas3_2Definition;
49
export interface ComponentsFiles {
510
[schemas: string]: any;
611
}

0 commit comments

Comments
 (0)