Skip to content

Commit 0d3b0f4

Browse files
fix: correct sort order for async API keys and update related tests (#2741)
1 parent 47bb8cf commit 0d3b0f4

8 files changed

Lines changed: 165 additions & 75 deletions

File tree

.changeset/smooth-dancers-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@redocly/cli": patch
3+
---
4+
5+
Ordered top-level keys in AsyncAPI documents during bundling for improved consistency and readability.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { exitWithError } from '../../utils/error.js';
1414
import {
1515
getAndValidateFileExtension,
1616
getFallbackApisOrExit,
17-
sortTopLevelKeysForOas,
17+
sortTopLevelKeys,
1818
writeToFileByExtension,
1919
} from '../../utils/miscellaneous.js';
2020
import { configFixture } from '../fixtures/config.js';
@@ -38,7 +38,7 @@ describe('handleJoin', () => {
3838
vi.mocked(getFallbackApisOrExit).mockImplementation(
3939
async (entrypoints) => entrypoints?.map((path: string) => ({ path })) ?? []
4040
);
41-
vi.mocked(sortTopLevelKeysForOas).mockImplementation((document) => document);
41+
vi.mocked(sortTopLevelKeys).mockImplementation((document) => document);
4242
writeToFileByExtensionSpy = vi
4343
.mocked(writeToFileByExtension)
4444
.mockImplementation(() => undefined);

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

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
checkIfRulesetExist,
2626
handleError,
2727
CircularJSONNotSupportedError,
28-
sortTopLevelKeysForOas,
28+
sortTopLevelKeys,
2929
cleanColors,
3030
getAndValidateFileExtension,
3131
writeToFileByExtension,
@@ -382,7 +382,7 @@ describe('langToExt', () => {
382382
});
383383
});
384384

385-
describe('sorTopLevelKeysForOas', () => {
385+
describe('sortTopLevelKeys', () => {
386386
it('should sort oas3 top level keys', () => {
387387
const openApi: openapiCore.Oas3Definition = {
388388
openapi: '3.0.0',
@@ -411,7 +411,7 @@ describe('sorTopLevelKeysForOas', () => {
411411
'x-webhooks',
412412
'components',
413413
];
414-
const result = sortTopLevelKeysForOas(openApi);
414+
const result = sortTopLevelKeys(openApi);
415415

416416
Object.keys(result).forEach((key, index) => {
417417
expect(key).toEqual(orderedKeys[index]);
@@ -453,7 +453,61 @@ describe('sorTopLevelKeysForOas', () => {
453453
'responses',
454454
'securityDefinitions',
455455
];
456-
const result = sortTopLevelKeysForOas(openApi);
456+
const result = sortTopLevelKeys(openApi);
457+
458+
Object.keys(result).forEach((key, index) => {
459+
expect(key).toEqual(orderedKeys[index]);
460+
});
461+
});
462+
463+
it('should sort asyncapi2 top level keys', () => {
464+
const asyncApi: openapiCore.Async2Definition = {
465+
asyncapi: '2.0.0',
466+
servers: {},
467+
channels: {},
468+
components: {},
469+
tags: [],
470+
info: { title: 'Test', version: '1.0.0' },
471+
externalDocs: {},
472+
defaultContentType: 'application/json',
473+
};
474+
const orderedKeys = [
475+
'asyncapi',
476+
'info',
477+
'externalDocs',
478+
'tags',
479+
'defaultContentType',
480+
'servers',
481+
'channels',
482+
'components',
483+
];
484+
const result = sortTopLevelKeys(asyncApi);
485+
486+
Object.keys(result).forEach((key, index) => {
487+
expect(key).toEqual(orderedKeys[index]);
488+
});
489+
});
490+
491+
it('should sort asyncapi3 top level keys', () => {
492+
const asyncApi: openapiCore.Async3Definition = {
493+
asyncapi: '3.0.0',
494+
channels: {},
495+
info: { title: 'Test', version: '1.0.0' },
496+
servers: {},
497+
components: {},
498+
operations: {},
499+
defaultContentType: 'application/json',
500+
};
501+
const orderedKeys = [
502+
'asyncapi',
503+
'info',
504+
'defaultContentType',
505+
'servers',
506+
'channels',
507+
'operations',
508+
'components',
509+
];
510+
const result = sortTopLevelKeys(asyncApi);
457511

458512
Object.keys(result).forEach((key, index) => {
459513
expect(key).toEqual(orderedKeys[index]);

packages/cli/src/commands/bundle.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
type Oas2Definition,
77
type Oas3Definition,
88
type RuleSeverity,
9+
type Async2Definition,
10+
type Async3Definition,
911
} from '@redocly/openapi-core';
1012
import { blue, gray, green, yellow } from 'colorette';
1113
import { writeFileSync } from 'fs';
@@ -21,7 +23,7 @@ import {
2123
handleError,
2224
printUnusedWarnings,
2325
saveBundle,
24-
sortTopLevelKeysForOas,
26+
sortTopLevelKeys,
2527
formatPath,
2628
} from '../utils/miscellaneous.js';
2729
import { type CommandArgs } from '../wrapper.js';
@@ -91,14 +93,18 @@ export async function handleBundle({
9193
if (fileTotals.errors === 0 || argv.force) {
9294
if (!outputFile) {
9395
const bundled = dumpBundle(
94-
sortTopLevelKeysForOas(result.parsed as Oas3Definition | Oas2Definition),
96+
sortTopLevelKeys(
97+
result.parsed as Oas3Definition | Oas2Definition | Async2Definition | Async3Definition
98+
),
9599
argv.ext || 'yaml',
96100
argv.dereferenced
97101
);
98102
logger.output(bundled);
99103
} else {
100104
const bundled = dumpBundle(
101-
sortTopLevelKeysForOas(result.parsed as Oas3Definition | Oas2Definition),
105+
sortTopLevelKeys(
106+
result.parsed as Oas3Definition | Oas2Definition | Async2Definition | Async3Definition
107+
),
102108
ext,
103109
argv.dereferenced
104110
);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { exitWithError } from '../../utils/error.js';
2121
import {
2222
getFallbackApisOrExit,
2323
printExecutionTime,
24-
sortTopLevelKeysForOas,
24+
sortTopLevelKeys,
2525
getAndValidateFileExtension,
2626
writeToFileByExtension,
2727
} from '../../utils/miscellaneous.js';
@@ -222,7 +222,7 @@ export async function handleJoin({
222222
return exitWithError(`Please fix conflicts before running ${yellow('join')}.`);
223223
}
224224

225-
writeToFileByExtension(sortTopLevelKeysForOas(joinedDef), specFilename, noRefs);
225+
writeToFileByExtension(sortTopLevelKeys(joinedDef), specFilename, noRefs);
226226

227227
printExecutionTime('join', startedAt, specFilename);
228228
}

packages/cli/src/utils/miscellaneous.ts

Lines changed: 73 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
type Oas3Definition,
1717
type Oas2Definition,
1818
type Exact,
19+
type Async3Definition,
20+
type Async2Definition,
1921
} from '@redocly/openapi-core';
2022
import { blue, gray, green, red, yellow } from 'colorette';
2123
import { hasMagic, glob } from 'glob';
@@ -446,58 +448,81 @@ export async function loadConfigAndHandleErrors(
446448
}
447449
}
448450

449-
export function sortTopLevelKeysForOas(
450-
document: Oas3Definition | Oas2Definition
451-
): Oas3Definition | Oas2Definition {
452-
if ('swagger' in document) {
453-
return sortOas2Keys(document);
451+
function isAsync2Definition(doc: Async2Definition | Async3Definition): doc is Async2Definition {
452+
return doc.asyncapi?.startsWith('2');
453+
}
454+
455+
const oas2OrderedKeys = [
456+
'swagger',
457+
'info',
458+
'host',
459+
'basePath',
460+
'schemes',
461+
'consumes',
462+
'produces',
463+
'security',
464+
'tags',
465+
'externalDocs',
466+
'paths',
467+
'definitions',
468+
'parameters',
469+
'responses',
470+
'securityDefinitions',
471+
];
472+
473+
const oas3OrderedKeys = [
474+
'openapi',
475+
'info',
476+
'jsonSchemaDialect',
477+
'servers',
478+
'security',
479+
'tags',
480+
'externalDocs',
481+
'paths',
482+
'webhooks',
483+
'x-webhooks',
484+
'components',
485+
];
486+
487+
const asyncApi2OrderedKeys = [
488+
'asyncapi',
489+
'id',
490+
'info',
491+
'externalDocs',
492+
'tags',
493+
'defaultContentType',
494+
'servers',
495+
'channels',
496+
'components',
497+
];
498+
499+
const asyncApi3OrderedKeys = [
500+
'asyncapi',
501+
'info',
502+
'defaultContentType',
503+
'servers',
504+
'channels',
505+
'operations',
506+
'components',
507+
];
508+
509+
export function sortTopLevelKeys(
510+
document: Oas3Definition | Oas2Definition | Async2Definition | Async3Definition
511+
): Oas3Definition | Oas2Definition | Async2Definition | Async3Definition {
512+
if ('asyncapi' in document) {
513+
return isAsync2Definition(document)
514+
? sortDocumentKeys(document, asyncApi2OrderedKeys)
515+
: sortDocumentKeys(document, asyncApi3OrderedKeys);
454516
}
455-
return sortOas3Keys(document as Oas3Definition);
456-
}
457-
458-
function sortOas2Keys(document: Oas2Definition): Oas2Definition {
459-
const orderedKeys = [
460-
'swagger',
461-
'info',
462-
'host',
463-
'basePath',
464-
'schemes',
465-
'consumes',
466-
'produces',
467-
'security',
468-
'tags',
469-
'externalDocs',
470-
'paths',
471-
'definitions',
472-
'parameters',
473-
'responses',
474-
'securityDefinitions',
475-
];
476-
const result: any = {};
477-
for (const key of orderedKeys as (keyof Oas2Definition)[]) {
478-
if (document.hasOwnProperty(key)) {
479-
result[key] = document[key];
480-
}
517+
if ('swagger' in document) {
518+
return sortDocumentKeys(document, oas2OrderedKeys);
481519
}
482-
// merge any other top-level keys (e.g. vendor extensions)
483-
return Object.assign(result, document);
520+
return sortDocumentKeys(document, oas3OrderedKeys);
484521
}
485-
function sortOas3Keys(document: Oas3Definition): Oas3Definition {
486-
const orderedKeys = [
487-
'openapi',
488-
'info',
489-
'jsonSchemaDialect',
490-
'servers',
491-
'security',
492-
'tags',
493-
'externalDocs',
494-
'paths',
495-
'webhooks',
496-
'x-webhooks',
497-
'components',
498-
];
522+
523+
function sortDocumentKeys<T extends object>(document: T, orderedKeys: string[]): T {
499524
const result: any = {};
500-
for (const key of orderedKeys as (keyof Oas3Definition)[]) {
525+
for (const key of orderedKeys as (keyof T)[]) {
501526
if (document.hasOwnProperty(key)) {
502527
result[key] = document[key];
503528
}

tests/e2e/bundle/async3/snapshot.txt

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,8 @@
1+
asyncapi: 3.0.0
12
info:
23
title: Account Service
34
version: 1.0.0
45
description: This service is in charge of processing user signups
5-
components:
6-
messages:
7-
UserSignedUp:
8-
UserSignedUp:
9-
payload:
10-
type: object
11-
properties:
12-
displayName:
13-
type: string
14-
description: Name of the user
15-
email:
16-
type: string
17-
format: email
18-
description: Email of the user
19-
asyncapi: 3.0.0
206
channels:
217
userSignedup:
228
address: user/signedup
@@ -50,6 +36,20 @@ operations:
5036
type: string
5137
format: email
5238
description: Email of the user
39+
components:
40+
messages:
41+
UserSignedUp:
42+
UserSignedUp:
43+
payload:
44+
type: object
45+
properties:
46+
displayName:
47+
type: string
48+
description: Name of the user
49+
email:
50+
type: string
51+
format: email
52+
description: Email of the user
5353

5454
bundling simple.yml using configuration for api 'main'...
5555
📦 Created a bundle for simple.yml at stdout <test>ms.

tests/e2e/bundle/sibling-refs-asyncapi/snapshot.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
asyncapi: 3.0.0
12
info:
23
title: Account Service
34
version: 1.0.0
@@ -15,7 +16,6 @@ components:
1516
properties:
1617
name:
1718
type: string
18-
asyncapi: 3.0.0
1919

2020
bundling asyncapi.yaml using configuration for api 'main'...
2121
📦 Created a bundle for asyncapi.yaml at stdout <test>ms.

0 commit comments

Comments
 (0)