Skip to content
Open
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
38 changes: 31 additions & 7 deletions docs/@v2/commands/split.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,41 @@
## Introduction

The `split` command takes an API description file and creates a [multi-file structure](https://redocly.com/docs/resources/multi-file-definitions/) out of it by extracting referenced parts into standalone, separate files.
Code samples, components, and paths are split from the root API description into separate files and folders.
The structure of the unbundled directory corresponds to the structure created by our [openapi-starter](https://github.com/Redocly/openapi-starter) tool.
The advantage of this approach is making smaller files that are easier to manage and a structure that makes reviewing simpler.

Use the [`bundle`](./bundle.md) command and supply the main file as the entrypoint to get your OpenAPI description in one file.
Many OpenAPI tools prefer a single file, but `split` and `bundle` allow you to manage your files easily for development, and then prepare a single file for other tools to consume.

{% admonition type="warning" name="OpenAPI 3.x only" %}
The `split` command supports OpenAPI 3.x descriptions only.
{% admonition type="warning" name="Supported specifications" %}
The `split` command supports OpenAPI 3.x, AsyncAPI 2.x, and AsyncAPI 3.x descriptions. OpenAPI 2.x (Swagger) is not supported.
{% /admonition %}

The parts that get split depend on the type of API description:

**OpenAPI 3.x**

Components, paths, and webhooks are split from the root API description into separate files and folders.
The structure of the unbundled directory corresponds to the structure created by the [openapi-starter](https://github.com/Redocly/openapi-starter) tool.

- `paths/` - each path item is written to a separate file
- `webhooks/` - each webhook is written to a separate file (OpenAPI 3.1+)
- `components/` - schemas, responses, parameters, examples, headers, requestBodies, links, callbacks, and securitySchemes are each split into subdirectories

**AsyncAPI 2.x**

Channels and components are split from the root API description into separate files and folders.

- `channels/` - each channel is written to a separate file
- `components/` - schemas, messages, securitySchemes, parameters, correlationIds, messageTraits, operationTraits, serverBindings, channelBindings, operationBindings, and messageBindings are each split into subdirectories

**AsyncAPI 3.x**

Channels, operations, and components are split from the root API description into separate files and folders.

- `channels/` - each channel is written to a separate file
- `operations/` - each operation is written to a separate file
- `components/` - schemas, messages, securitySchemes, servers, serverVariables, parameters, replies, replyAddresses, correlationIds, messageTraits, operationTraits, tags, externalDocs, serverBindings, channelBindings, operationBindings, and messageBindings are each split into subdirectories

Use the [`bundle`](./bundle.md) command and supply the main file as the entrypoint to get your API description back in one file.
Many API tools prefer a single file, but `split` and `bundle` allow you to manage your files easily for development, and then prepare a single file for other tools to consume.

## Usage

```bash
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/__tests__/commands/join.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
} from '@redocly/openapi-core';
import { yellow } from 'colorette';

import { replace$Refs } from '../../commands/join/helpers/replace-$-refs.js';
import { handleJoin } from '../../commands/join/index.js';
import { replace$Refs } from '../../commands/join/utils/replace-$-refs.js';
import { exitWithError } from '../../utils/error.js';
import {
getAndValidateFileExtension,
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/join/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
writeToFileByExtension,
} from '../../utils/miscellaneous.js';
import type { CommandArgs } from '../../wrapper.js';
import { COMPONENTS } from '../split/types.js';
import { COMPONENTS } from '../split/constants.js';
import type { JoinArgv, AnyOas3Definition } from './types.js';
import {
replace$Refs,
getInfoPrefix,
Expand All @@ -38,8 +39,7 @@ import {
collectComponents,
collectWebhooks,
addInfoSectionAndSpecVersion,
} from './helpers/index.js';
import type { JoinArgv, AnyOas3Definition } from './types.js';
} from './utils/index.js';

export async function handleJoin({
argv,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { exitWithError } from '../../../utils/error.js';
import { COMPONENTS } from '../../split/types.js';
import { COMPONENTS } from '../../split/constants.js';
import { addComponentsPrefix } from './add-components-prefix.js';
import { getInfoPrefix } from './get-info-prefix.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { COMPONENTS } from '../../split/types.js';
import { COMPONENTS } from '../../split/constants.js';
import type { AnyOas3Definition, JoinDocumentContext } from '../types.js';
import { addPrefix } from './add-prefix.js';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
} from '@redocly/openapi-core';

import { exitWithError } from '../../../utils/error.js';
import { type Oas3Method, OPENAPI3_METHOD_NAMES } from '../../split/types.js';
import { OPENAPI3_METHOD_NAMES } from '../../split/oas/constants.js';
import { type Oas3Method } from '../../split/types.js';
import type { AnyOas3Definition, JoinDocumentContext } from '../types.js';
import { addPrefix } from './add-prefix.js';
import { addSecurityPrefix } from './add-security-prefix.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { dequal } from '@redocly/openapi-core';
import { green } from 'colorette';

import { COMPONENTS } from '../../split/types.js';
import { COMPONENTS } from '../../split/constants.js';
import { duplicateTagDescriptionWarning } from './duplicate-tag-description-warning.js';
import { filterConflicts } from './filter-conflicts.js';
import { prefixTagSuggestion } from './prefix-tag-suggestion.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isRef, isPlainObject } from '@redocly/openapi-core';
import * as path from 'node:path';

import { crawl, startsWithComponents } from '../../split/index.js';
import { crawl } from '../../split/utils/crawl.js';
import { startsWithComponents } from '../../split/utils/starts-with-components.js';

export function replace$Refs(obj: unknown, componentsPrefix: string) {
crawl(obj, (node: Record<string, unknown>) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/split/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import * as process from 'node:process';

import { configFixture } from '../../../__tests__/fixtures/config.js';
import * as utils from '../../../utils/miscellaneous.js';
import { iteratePathItems, handleSplit } from '../index.js';
import { handleSplit } from '../index.js';
import { type ComponentsFiles } from '../types.js';
import { iteratePathItems } from '../utils/iterate-path-items.js';
import samplesJson from './fixtures/samples.json';
import specJson from './fixtures/spec.json';
import webhooksJson from './fixtures/webhooks.json';
Expand Down
73 changes: 73 additions & 0 deletions packages/cli/src/commands/split/asyncapi/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export const ASYNCAPI_ACTION_NAMES = ['send', 'receive'] as const;
export const ASYNCAPI2_COMPONENT_NAMES = [
'schemas',
'messages',
'parameters',
'correlationIds',
'messageTraits',
'operationTraits',
'securitySchemes',
'servers',
'serverVariables',
'channels',
'serverBindings',
'channelBindings',
'operationBindings',
'messageBindings',
] as const;

export const ASYNCAPI2_SPLITTABLE_COMPONENT_NAMES = [
'schemas',
'messages',
'securitySchemes',
'parameters',
'correlationIds',
'messageTraits',
'operationTraits',
'serverBindings',
'channelBindings',
'operationBindings',
'messageBindings',
] as const;

export const ASYNCAPI3_COMPONENT_NAMES = [
'schemas',
'messages',
'parameters',
'replies',
'replyAddresses',
'correlationIds',
'messageTraits',
'operationTraits',
'tags',
'externalDocs',
'securitySchemes',
'servers',
'serverVariables',
'channels',
'operations',
'serverBindings',
'channelBindings',
'operationBindings',
'messageBindings',
] as const;

export const ASYNCAPI3_SPLITTABLE_COMPONENT_NAMES = [
'schemas',
'messages',
'securitySchemes',
'servers',
'serverVariables',
'parameters',
'replies',
'replyAddresses',
'correlationIds',
'messageTraits',
'operationTraits',
'tags',
'externalDocs',
'serverBindings',
'channelBindings',
'operationBindings',
'messageBindings',
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
ASYNCAPI2_SPLITTABLE_COMPONENT_NAMES,
ASYNCAPI3_SPLITTABLE_COMPONENT_NAMES,
} from './constants.js';

export function findAsyncApiComponentTypes(
components: Record<string, unknown>,
specVersion: 'async2' | 'async3'
) {
const componentNames =
specVersion === 'async2'
? ASYNCAPI2_SPLITTABLE_COMPONENT_NAMES
: ASYNCAPI3_SPLITTABLE_COMPONENT_NAMES;

return componentNames.filter((item) => item in components);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { isTruthy } from '@redocly/openapi-core';
import * as path from 'node:path';

import { COMPONENTS } from '../constants.js';
import { type ComponentsFiles, type AnyAsyncApiDefinition } from '../types.js';
import { getFileNamePath } from '../utils/get-file-name-path.js';
import { findAsyncApiComponentTypes } from './find-asyncapi-component-types.js';

export function gatherAsyncApiComponentFiles({
asyncapi,
asyncapiDir,
componentsFiles,
ext,
specVersion,
}: {
asyncapi: AnyAsyncApiDefinition;
asyncapiDir: string;
componentsFiles: ComponentsFiles;
ext: string;
specVersion: 'async2' | 'async3';
}) {
const { components } = asyncapi;
if (!components) return;
const componentsDir = path.join(asyncapiDir, COMPONENTS);
const componentTypes = findAsyncApiComponentTypes(components, specVersion);
for (const componentType of componentTypes) {
const componentDirPath = path.join(componentsDir, componentType);
for (const componentName of Object.keys(components[componentType] || {})) {
const filename = getFileNamePath(componentDirPath, componentName, ext);
let inherits: string[] = [];
if (componentType === 'schemas') {
inherits = (
(components[componentType]?.[componentName] as { allOf?: Array<{ $ref?: string }> })
?.allOf || []
)
.map(({ $ref }) => $ref)
.filter(isTruthy);
}
componentsFiles[componentType] = componentsFiles[componentType] || {};
componentsFiles[componentType][componentName] = { inherits, filename };
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { slash, isRef } from '@redocly/openapi-core';
import * as fs from 'node:fs';
import * as path from 'node:path';

import { pathToFilename, writeToFileByExtension } from '../../../utils/miscellaneous.js';
import { type ChannelsFiles, type ComponentsFiles } from '../types.js';
import { replace$Refs } from '../utils/replace-$-refs.js';
import {
traverseDirectoryDeep,
traverseDirectoryDeepCallback,
} from '../utils/traverse-directory-deep.js';

export function iterateAsyncApiChannels({
channels,
asyncapiDir,
outDir,
componentsFiles,
pathSeparator,
ext,
}: {
channels: Record<string, any> | undefined;
asyncapiDir: string;
outDir: string;
componentsFiles: ComponentsFiles;
pathSeparator: string;
ext: string;
}): ChannelsFiles {
const channelsFiles: ChannelsFiles = {};
if (!channels) return channelsFiles;
fs.mkdirSync(outDir, { recursive: true });

for (const channelName of Object.keys(channels)) {
const channelFile = `${path.join(outDir, pathToFilename(channelName, pathSeparator))}.${ext}`;
const channelData = channels[channelName];

if (isRef(channelData)) continue;

channelsFiles[channelName] = channelFile;
replace$Refs(channelData, path.dirname(channelFile), componentsFiles);
writeToFileByExtension(channelData, channelFile);
channels[channelName] = {
$ref: slash(path.relative(asyncapiDir, channelFile)),
};

traverseDirectoryDeep(outDir, traverseDirectoryDeepCallback, componentsFiles);
}
return channelsFiles;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { logger } from '@redocly/openapi-core';
import { blue } from 'colorette';
import * as fs from 'node:fs';
import * as path from 'node:path';

import { writeToFileByExtension } from '../../../utils/miscellaneous.js';
import { COMPONENTS } from '../constants.js';
import {
type ChannelsFiles,
type ComponentsFiles,
type AnyAsyncApiDefinition,
type AsyncApi2SplittableComponent,
type AsyncApi3SplittableComponent,
} from '../types.js';
import { createComponentDir } from '../utils/create-component-dir.js';
import { doesFileDiffer } from '../utils/does-file-differ.js';
import { getFileNamePath } from '../utils/get-file-name-path.js';
import { replace$Refs } from '../utils/replace-$-refs.js';
import { replaceChannelRefs } from '../utils/replace-channel-refs.js';
import { findAsyncApiComponentTypes } from './find-asyncapi-component-types.js';
import { removeAsyncApiEmptyComponents } from './remove-asyncapi-empty-components.js';

export function iterateAsyncApiComponents({
asyncapi,
asyncapiDir,
componentsFiles,
channelsFiles,
ext,
specVersion,
}: {
asyncapi: AnyAsyncApiDefinition;
asyncapiDir: string;
componentsFiles: ComponentsFiles;
channelsFiles: ChannelsFiles;
ext: string;
specVersion: 'async2' | 'async3';
}) {
const { components } = asyncapi;
if (components) {
const componentsDir = path.join(asyncapiDir, COMPONENTS);
fs.mkdirSync(componentsDir, { recursive: true });
const componentTypes = findAsyncApiComponentTypes(components, specVersion);
componentTypes.forEach(iterateComponentTypes);

function iterateComponentTypes(
componentType: AsyncApi2SplittableComponent | AsyncApi3SplittableComponent
) {
const componentDirPath = path.join(componentsDir, componentType);
createComponentDir(componentDirPath, componentType);
for (const componentName of Object.keys(components?.[componentType] || {})) {
const filename = getFileNamePath(componentDirPath, componentName, ext);
const componentData = components?.[componentType]?.[componentName];
replace$Refs(componentData, path.dirname(filename), componentsFiles);
replaceChannelRefs(componentData, path.dirname(filename), channelsFiles);

if (doesFileDiffer(filename, componentData)) {
logger.warn(
`warning: conflict for ${componentName} - file already exists with different content: ${blue(
filename
)} ... Skip.\n`
);
} else {
writeToFileByExtension(componentData, filename);
}

delete asyncapi.components?.[componentType]?.[componentName];
}
removeAsyncApiEmptyComponents(asyncapi, componentType);
}
}
}
Loading
Loading