Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smooth-dancers-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/cli": patch
---

Ordered top-level keys in AsyncAPI documents during bundling for improved consistency and readability.
4 changes: 2 additions & 2 deletions packages/cli/src/__tests__/commands/join.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { exitWithError } from '../../utils/error.js';
import {
getAndValidateFileExtension,
getFallbackApisOrExit,
sortTopLevelKeysForOas,
sortTopLevelKeys,
writeToFileByExtension,
} from '../../utils/miscellaneous.js';
import { configFixture } from '../fixtures/config.js';
Expand All @@ -38,7 +38,7 @@ describe('handleJoin', () => {
vi.mocked(getFallbackApisOrExit).mockImplementation(
async (entrypoints) => entrypoints?.map((path: string) => ({ path })) ?? []
);
vi.mocked(sortTopLevelKeysForOas).mockImplementation((document) => document);
vi.mocked(sortTopLevelKeys).mockImplementation((document) => document);
writeToFileByExtensionSpy = vi
.mocked(writeToFileByExtension)
.mockImplementation(() => undefined);
Expand Down
62 changes: 58 additions & 4 deletions packages/cli/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
checkIfRulesetExist,
handleError,
CircularJSONNotSupportedError,
sortTopLevelKeysForOas,
sortTopLevelKeys,
cleanColors,
getAndValidateFileExtension,
writeToFileByExtension,
Expand Down Expand Up @@ -382,7 +382,7 @@ describe('langToExt', () => {
});
});

describe('sorTopLevelKeysForOas', () => {
describe('sortTopLevelKeys', () => {
it('should sort oas3 top level keys', () => {
const openApi: openapiCore.Oas3Definition = {
openapi: '3.0.0',
Expand Down Expand Up @@ -411,7 +411,7 @@ describe('sorTopLevelKeysForOas', () => {
'x-webhooks',
'components',
];
const result = sortTopLevelKeysForOas(openApi);
const result = sortTopLevelKeys(openApi);

Object.keys(result).forEach((key, index) => {
expect(key).toEqual(orderedKeys[index]);
Expand Down Expand Up @@ -453,7 +453,61 @@ describe('sorTopLevelKeysForOas', () => {
'responses',
'securityDefinitions',
];
const result = sortTopLevelKeysForOas(openApi);
const result = sortTopLevelKeys(openApi);

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

it('should sort asyncapi2 top level keys', () => {
const asyncApi: openapiCore.Async2Definition = {
asyncapi: '2.0.0',
servers: {},
channels: {},
components: {},
tags: [],
info: { title: 'Test', version: '1.0.0' },
externalDocs: {},
defaultContentType: 'application/json',
};
const orderedKeys = [
'asyncapi',
'info',
'externalDocs',
'tags',
'defaultContentType',
'servers',
'channels',
'components',
];
const result = sortTopLevelKeys(asyncApi);

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

it('should sort asyncapi3 top level keys', () => {
const asyncApi: openapiCore.Async3Definition = {
asyncapi: '3.0.0',
channels: {},
info: { title: 'Test', version: '1.0.0' },
servers: {},
components: {},
operations: {},
defaultContentType: 'application/json',
};
const orderedKeys = [
'asyncapi',
'info',
'defaultContentType',
'servers',
'channels',
'operations',
'components',
];
const result = sortTopLevelKeys(asyncApi);

Object.keys(result).forEach((key, index) => {
expect(key).toEqual(orderedKeys[index]);
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/src/commands/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
type Oas2Definition,
type Oas3Definition,
type RuleSeverity,
type Async2Definition,
type Async3Definition,
} from '@redocly/openapi-core';
import { blue, gray, green, yellow } from 'colorette';
import { writeFileSync } from 'fs';
Expand All @@ -21,7 +23,7 @@ import {
handleError,
printUnusedWarnings,
saveBundle,
sortTopLevelKeysForOas,
sortTopLevelKeys,
formatPath,
} from '../utils/miscellaneous.js';
import { type CommandArgs } from '../wrapper.js';
Expand Down Expand Up @@ -91,14 +93,18 @@ export async function handleBundle({
if (fileTotals.errors === 0 || argv.force) {
if (!outputFile) {
const bundled = dumpBundle(
sortTopLevelKeysForOas(result.parsed as Oas3Definition | Oas2Definition),
sortTopLevelKeys(
result.parsed as Oas3Definition | Oas2Definition | Async2Definition | Async3Definition
),
argv.ext || 'yaml',
argv.dereferenced
);
logger.output(bundled);
} else {
const bundled = dumpBundle(
sortTopLevelKeysForOas(result.parsed as Oas3Definition | Oas2Definition),
sortTopLevelKeys(
result.parsed as Oas3Definition | Oas2Definition | Async2Definition | Async3Definition
),
ext,
argv.dereferenced
);
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/join/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { exitWithError } from '../../utils/error.js';
import {
getFallbackApisOrExit,
printExecutionTime,
sortTopLevelKeysForOas,
sortTopLevelKeys,
getAndValidateFileExtension,
writeToFileByExtension,
} from '../../utils/miscellaneous.js';
Expand Down Expand Up @@ -222,7 +222,7 @@ export async function handleJoin({
return exitWithError(`Please fix conflicts before running ${yellow('join')}.`);
}

writeToFileByExtension(sortTopLevelKeysForOas(joinedDef), specFilename, noRefs);
writeToFileByExtension(sortTopLevelKeys(joinedDef), specFilename, noRefs);

printExecutionTime('join', startedAt, specFilename);
}
121 changes: 73 additions & 48 deletions packages/cli/src/utils/miscellaneous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
type Oas3Definition,
type Oas2Definition,
type Exact,
type Async3Definition,
type Async2Definition,
} from '@redocly/openapi-core';
import { blue, gray, green, red, yellow } from 'colorette';
import { hasMagic, glob } from 'glob';
Expand Down Expand Up @@ -446,58 +448,81 @@ export async function loadConfigAndHandleErrors(
}
}

export function sortTopLevelKeysForOas(
document: Oas3Definition | Oas2Definition
): Oas3Definition | Oas2Definition {
if ('swagger' in document) {
return sortOas2Keys(document);
function isAsync2Definition(doc: Async2Definition | Async3Definition): doc is Async2Definition {
return doc.asyncapi?.startsWith('2');
}

const oas2OrderedKeys = [
'swagger',
'info',
'host',
'basePath',
'schemes',
'consumes',
'produces',
'security',
'tags',
'externalDocs',
'paths',
'definitions',
'parameters',
'responses',
'securityDefinitions',
];

const oas3OrderedKeys = [
'openapi',
'info',
'jsonSchemaDialect',
'servers',
'security',
'tags',
'externalDocs',
'paths',
'webhooks',
'x-webhooks',
'components',
];

const asyncApi2OrderedKeys = [
'asyncapi',
'id',
'info',
'externalDocs',
'tags',
'defaultContentType',
'servers',
'channels',
'components',
];

const asyncApi3OrderedKeys = [
'asyncapi',
'info',
'defaultContentType',
'servers',
'channels',
'operations',
'components',
];

export function sortTopLevelKeys(
document: Oas3Definition | Oas2Definition | Async2Definition | Async3Definition
): Oas3Definition | Oas2Definition | Async2Definition | Async3Definition {
if ('asyncapi' in document) {
return isAsync2Definition(document)
? sortDocumentKeys(document, asyncApi2OrderedKeys)
: sortDocumentKeys(document, asyncApi3OrderedKeys);
}
return sortOas3Keys(document as Oas3Definition);
}

function sortOas2Keys(document: Oas2Definition): Oas2Definition {
const orderedKeys = [
'swagger',
'info',
'host',
'basePath',
'schemes',
'consumes',
'produces',
'security',
'tags',
'externalDocs',
'paths',
'definitions',
'parameters',
'responses',
'securityDefinitions',
];
const result: any = {};
for (const key of orderedKeys as (keyof Oas2Definition)[]) {
if (document.hasOwnProperty(key)) {
result[key] = document[key];
}
if ('swagger' in document) {
return sortDocumentKeys(document, oas2OrderedKeys);
}
// merge any other top-level keys (e.g. vendor extensions)
return Object.assign(result, document);
return sortDocumentKeys(document, oas3OrderedKeys);
}
function sortOas3Keys(document: Oas3Definition): Oas3Definition {
const orderedKeys = [
'openapi',
'info',
'jsonSchemaDialect',
'servers',
'security',
'tags',
'externalDocs',
'paths',
'webhooks',
'x-webhooks',
'components',
];

function sortDocumentKeys<T extends object>(document: T, orderedKeys: string[]): T {
const result: any = {};
for (const key of orderedKeys as (keyof Oas3Definition)[]) {
for (const key of orderedKeys as (keyof T)[]) {
if (document.hasOwnProperty(key)) {
result[key] = document[key];
}
Expand Down
30 changes: 15 additions & 15 deletions tests/e2e/bundle/async3/snapshot.txt
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
asyncapi: 3.0.0
info:
title: Account Service
version: 1.0.0
description: This service is in charge of processing user signups
components:
messages:
UserSignedUp:
UserSignedUp:
payload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
asyncapi: 3.0.0
channels:
userSignedup:
address: user/signedup
Expand Down Expand Up @@ -50,6 +36,20 @@ operations:
type: string
format: email
description: Email of the user
components:
messages:
UserSignedUp:
UserSignedUp:
payload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user

bundling simple.yml using configuration for api 'main'...
📦 Created a bundle for simple.yml at stdout <test>ms.
2 changes: 1 addition & 1 deletion tests/e2e/bundle/sibling-refs-asyncapi/snapshot.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
asyncapi: 3.0.0
info:
title: Account Service
version: 1.0.0
Expand All @@ -15,7 +16,6 @@ components:
properties:
name:
type: string
asyncapi: 3.0.0

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