feat: support for webhooks to the split command (#550)

Co-authored-by: Anton_Kozachuk <anton.kozachuk@cloudmobilizd.com>
Co-authored-by: romanhotsiy <gotsijroman@gmail.com>
This commit is contained in:
Anton Kozachuk
2022-02-17 16:22:45 +02:00
committed by GitHub
parent 964c2ba797
commit 98d6122a9f
16 changed files with 629 additions and 91 deletions

View File

@@ -26,7 +26,7 @@ function getEntrypoints(folderPath: string) {
return Object.keys(redoclyYaml.apiDefinitions); return Object.keys(redoclyYaml.apiDefinitions);
} }
function getBundleResult(params: string[], folderPath: string) { function getCommandOutput(params: string[], folderPath: string) {
const result = spawnSync('ts-node', params, { const result = spawnSync('ts-node', params, {
cwd: folderPath, cwd: folderPath,
env: { env: {
@@ -72,6 +72,47 @@ describe('E2E', () => {
} }
}); });
describe('split', () => {
test('without option: outDir', () => {
const folderPath = join(__dirname, `split/missing-outDir`);
const args = [
'--transpile-only',
'../../../packages/cli/src/index.ts',
'split',
'../../../__tests__/split/test-split/spec.json',
];
const result = getCommandOutput(args, folderPath);
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
});
test('swagger', () => {
const folderPath = join(__dirname, `split/oas2`);
const args = [
'--transpile-only',
'../../../packages/cli/src/index.ts',
'split',
'../../../__tests__/split/oas2/openapi.yaml',
'--outDir=output'
];
const result = getCommandOutput(args, folderPath);
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
});
test('openapi with no errors', () => {
const folderPath = join(__dirname, `split/oas3-no-errors`);
const file = '../../../__tests__/split/oas3-no-errors/openapi.yaml';
const args = [
'--transpile-only',
'../../../packages/cli/src/index.ts',
'split',
file,
'--outDir=output'
];
const result = getCommandOutput(args, folderPath);
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
});
});
describe('join', () => { describe('join', () => {
const folderPath = join(__dirname, 'join'); const folderPath = join(__dirname, 'join');
const contents = readdirSync(folderPath); const contents = readdirSync(folderPath);
@@ -171,14 +212,14 @@ describe('E2E', () => {
test.each(['codeframe','stylish','json','checkstyle'])('bundle lint: should be formatted by format: %s', (format) => { test.each(['codeframe','stylish','json','checkstyle'])('bundle lint: should be formatted by format: %s', (format) => {
const params = [...args, `--format=${format}`]; const params = [...args, `--format=${format}`];
const result = getBundleResult(params, folderPath); const result = getCommandOutput(params, folderPath);
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, `${format}-format-snapshot.js`)); (<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, `${format}-format-snapshot.js`));
}); });
test.each(['noFormatParameter','emptyFormatValue'])('bundle lint: no format parameter or empty value should be formatted as codeframe', (format) => { test.each(['noFormatParameter','emptyFormatValue'])('bundle lint: no format parameter or empty value should be formatted as codeframe', (format) => {
const formatArgument = format === 'emptyFormatValue' ? ['--format'] : []; const formatArgument = format === 'emptyFormatValue' ? ['--format'] : [];
const params = [...args, ... formatArgument]; const params = [...args, ... formatArgument];
const result = getBundleResult(params, folderPath); const result = getCommandOutput(params, folderPath);
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, `${format}-snapshot.js`)); (<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, `${format}-snapshot.js`));
}); });
}); });
@@ -193,7 +234,7 @@ describe('E2E', () => {
"--remove-unused-components", "--remove-unused-components",
...entryPoints, ...entryPoints,
]; ];
const result = getBundleResult(args, folderPath); const result = getCommandOutput(args, folderPath);
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'remove-unused-components-snapshot.js')); (<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'remove-unused-components-snapshot.js'));
}); });
}); });

View File

@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E split without option: outDir 1`] = `
index.ts split [entrypoint]
Split definition into a multi-file structure.
Positionals:
entrypoint API definition file that you want to split [string] [required]
Options:
--version Show version number. [boolean]
--help Show help. [boolean]
--outDir Output directory where files will be saved. [string] [required]
Missing required argument: outDir
`;

View File

@@ -0,0 +1,3 @@
{
"openapi": "3.0.1"
}

View File

@@ -0,0 +1,45 @@
swagger: "2.0"
info:
version: "1.0.0"
title: "Swagger Petstore"
description: "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification"
termsOfService: "http://swagger.io/terms/"
contact:
name: "Swagger API Team"
license:
name: "MIT"
host: "petstore.swagger.io"
basePath: "/api"
schemes:
- "http"
consumes:
- "application/json"
produces:
- "application/json"
paths:
/pets:
get:
description: "Returns all pets from the system that the user has access to"
produces:
- "application/json"
responses:
"200":
description: "A list of pets."
schema:
type: "array"
items:
$ref: "#/definitions/Pet"
definitions:
Pet:
type: "object"
required:
- "id"
- "name"
properties:
id:
type: "integer"
format: "int64"
name:
type: "string"
tag:
type: "string"

View File

@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E split swagger 1`] = `
OpenAPI 2 is not supported by this command
`;

View File

@@ -0,0 +1,111 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@@ -0,0 +1,119 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E split openapi with no errors 1`] = `
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
type: array
items:
$ref: ./Pet.yaml
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Pets'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
openapi: 3.0.0
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
$ref: paths/pets.yaml
/pets/{petId}:
$ref: paths/pets@{petId}.yaml
🪓 Document: ../../../__tests__/split/oas3-no-errors/openapi.yaml is successfully split
and all related files are saved to the directory: output
../../../__tests__/split/oas3-no-errors/openapi.yaml: split processed in <test>ms
`;

View File

@@ -44,5 +44,27 @@
} }
} }
} }
},
"x-webhooks": {
"test": {
"post": {
"summary": "New pet",
"description": "Information about a new pet in the systems",
"operationId": "newPet",
"tags": ["pet"],
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Test" }
}
}
},
"responses": {
"200": {
"description": "Return a 200 status to indicate that the data was received successfully"
}
}
}
}
} }
} }

View File

@@ -0,0 +1,88 @@
{
"openapi": "3.1.0",
"info": {
"title": "Webhook Example",
"version": "1.0.0"
},
"paths": {
"/pets": {
"get": {
"summary": "List all pets",
"operationId": "listPets",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "A paged array of pets",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
}
}
}
}
}
}
},
"webhooks": {
"test": {
"post": {
"requestBody": {
"description": "Information about a new pet in the system",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
"responses": {
"200": {
"description": "Return a 200 status to indicate that the data was received successfully"
}
}
}
}
},
"components": {
"schemas": {
"Pet": {
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"Pets": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Pet"
}
}
}
}
}

View File

@@ -1,32 +1,76 @@
import { iteratePaths } from '../index'; import { iteratePathItems, handleSplit } from '../index';
import * as path from 'path'; import * as path from 'path';
import * as openapiCore from '@redocly/openapi-core'; import * as openapiCore from '@redocly/openapi-core';
import {
ComponentsFiles,
} from '../types';
import { blue, green } from 'colorette';
jest.mock('../../../utils', () => ({ jest.mock('../../../utils', () => ({
...jest.requireActual('../../../utils'), ...jest.requireActual('../../../utils'),
writeYaml: jest.fn(), writeYaml: jest.fn(),
})); }));
jest.mock('@redocly/openapi-core', () => ({
...jest.requireActual('@redocly/openapi-core'),
isRef: jest.fn(),
}));
describe('#split', () => { describe('#split', () => {
it('should have correct paths for mac', () => {
const pathsDir = 'test/paths';
const openapiDir = 'test'; const openapiDir = 'test';
const componentsFiles: ComponentsFiles = {};
it('should split the file and show the success message', async () => {
const filePath = "./packages/cli/src/commands/split/__tests__/fixtures/spec.json";
jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
await handleSplit (
{
entrypoint: filePath,
outDir: openapiDir,
}
);
expect(process.stderr.write).toBeCalledTimes(2);
expect((process.stderr.write as jest.Mock).mock.calls[0][0]).toBe(
`🪓 Document: ${blue(filePath!)} ${green('is successfully split')}
and all related files are saved to the directory: ${blue(openapiDir)} \n`
);
expect((process.stderr.write as jest.Mock).mock.calls[1][0]).toContain(
`${filePath}: split processed in <test>ms`
);
});
it('should have correct path with paths', () => {
const openapi = require("./fixtures/spec.json");
jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'paths/test.yaml'); jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'paths/test.yaml');
jest.spyOn(path, 'relative').mockImplementation(() => 'paths/test.yaml'); jest.spyOn(path, 'relative').mockImplementation(() => 'paths/test.yaml');
iteratePaths(require("./fixtures/spec.json"), pathsDir, openapiDir); iteratePathItems(openapi.paths, openapiDir, path.join(openapiDir, 'paths'), componentsFiles);
expect(openapiCore.slash).toHaveBeenCalledWith('paths/test.yaml'); expect(openapiCore.slash).toHaveBeenCalledWith('paths/test.yaml');
expect(path.relative).toHaveBeenCalledWith('test', 'test/paths/test.yaml'); expect(path.relative).toHaveBeenCalledWith('test', 'test/paths/test.yaml');
}); });
it('should have correct paths for windows', () => { it('should have correct path with webhooks', () => {
const pathsDir = 'test\\paths'; const openapi = require("./fixtures/webhooks.json");
const openapiDir = 'test';
jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'paths\\test.yaml'); jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'webhooks/test.yaml');
jest.spyOn(path, 'relative').mockImplementation(() => 'paths\\test.yaml'); jest.spyOn(path, 'relative').mockImplementation(() => 'webhooks/test.yaml');
iteratePaths(require("./fixtures/spec.json"), pathsDir, openapiDir); iteratePathItems(openapi.webhooks, openapiDir, path.join(openapiDir, 'webhooks'), componentsFiles, 'webhook_');
expect(openapiCore.slash).toHaveBeenCalledWith('paths\\test.yaml');
expect(path.relative).toHaveBeenCalledWith('test', 'test\\paths/test.yaml'); expect(openapiCore.slash).toHaveBeenCalledWith('webhooks/test.yaml');
expect(path.relative).toHaveBeenCalledWith('test', 'test/webhooks/test.yaml');
});
it('should have correct path with x-webhooks', () => {
const openapi = require("./fixtures/spec.json");
jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'webhooks/test.yaml');
jest.spyOn(path, 'relative').mockImplementation(() => 'webhooks/test.yaml');
iteratePathItems(openapi['x-webhooks'], openapiDir, path.join(openapiDir, 'webhooks'), componentsFiles, 'webhook_');
expect(openapiCore.slash).toHaveBeenCalledWith('webhooks/test.yaml');
expect(path.relative).toHaveBeenCalledWith('test', 'test/webhooks/test.yaml');
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { red, blue, yellow, green } from 'colorette'; import { red, blue, yellow, green } from 'colorette';
import * as fs from 'fs'; import * as fs from 'fs';
import { parseYaml, slash } from '@redocly/openapi-core'; import { parseYaml, slash, isRef } from '@redocly/openapi-core';
import * as path from 'path'; import * as path from 'path';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
const isEqual = require('lodash.isequal'); const isEqual = require('lodash.isequal');
@@ -12,6 +12,7 @@ import {
Oas2Definition, Oas2Definition,
Oas3Schema, Oas3Schema,
Oas3Definition, Oas3Definition,
Oas3_1Definition,
Oas3Components, Oas3Components,
Oas3ComponentName, Oas3ComponentName,
ComponentsFiles, ComponentsFiles,
@@ -20,19 +21,19 @@ import {
OPENAPI3_COMPONENT, OPENAPI3_COMPONENT,
COMPONENTS, COMPONENTS,
componentsPath, componentsPath,
PATHS,
OPENAPI3_METHOD_NAMES, OPENAPI3_METHOD_NAMES,
OPENAPI3_COMPONENT_NAMES OPENAPI3_COMPONENT_NAMES,
} from './types' Referenced
} from './types';
export async function handleSplit (argv: { export async function handleSplit (argv: {
entrypoint?: string; entrypoint: string;
outDir: string outDir: string
}) { }) {
const startedAt = performance.now(); const startedAt = performance.now();
const { entrypoint, outDir } = argv; const { entrypoint, outDir } = argv;
validateDefinitionFileName(entrypoint!); validateDefinitionFileName(entrypoint!);
const openapi = readYaml(entrypoint!) as Oas3Definition; const openapi = readYaml(entrypoint!) as Oas3Definition | Oas3_1Definition;
splitDefinition(openapi, outDir); splitDefinition(openapi, outDir);
process.stderr.write( process.stderr.write(
`🪓 Document: ${blue(entrypoint!)} ${green('is successfully split')} `🪓 Document: ${blue(entrypoint!)} ${green('is successfully split')}
@@ -41,23 +42,16 @@ export async function handleSplit (argv: {
printExecutionTime('split', startedAt, entrypoint!); printExecutionTime('split', startedAt, entrypoint!);
} }
function splitDefinition(openapi: Oas3Definition, openapiDir: string) { function splitDefinition(openapi: Oas3Definition | Oas3_1Definition, openapiDir: string) {
fs.mkdirSync(openapiDir, { recursive: true }); fs.mkdirSync(openapiDir, { recursive: true });
const pathsDir = path.join(openapiDir, PATHS);
fs.mkdirSync(pathsDir, { recursive: true });
const componentsFiles: ComponentsFiles = {}; const componentsFiles: ComponentsFiles = {};
iteratePaths(openapi, pathsDir, openapiDir);
iterateComponents(openapi, openapiDir, componentsFiles); iterateComponents(openapi, openapiDir, componentsFiles);
iteratePathItems(openapi.paths, openapiDir, path.join(openapiDir, 'paths'), componentsFiles);
const webhooks = (openapi as Oas3_1Definition).webhooks || (openapi as Oas3Definition)['x-webhooks'];
// use webhook_ prefix for code samples to prevent potential name-clashes with paths samples
iteratePathItems(webhooks, openapiDir, path.join(openapiDir, 'webhooks'), componentsFiles, 'webhook_');
function traverseDirectoryDeepCallback(filename: string, directory: string) {
if (isNotYaml(filename)) return;
const pathData = readYaml(filename);
replace$Refs(pathData, directory, componentsFiles);
writeYaml(pathData, filename);
}
traverseDirectoryDeep(pathsDir, traverseDirectoryDeepCallback);
replace$Refs(openapi, openapiDir, componentsFiles); replace$Refs(openapi, openapiDir, componentsFiles);
writeYaml(openapi, path.join(openapiDir, 'openapi.yaml')); writeYaml(openapi, path.join(openapiDir, 'openapi.yaml'));
} }
@@ -82,7 +76,7 @@ function validateDefinitionFileName(fileName: string) {
if (!fs.existsSync(fileName)) exitWithError(`File ${blue(fileName)} does not exist \n`); if (!fs.existsSync(fileName)) exitWithError(`File ${blue(fileName)} does not exist \n`);
const file = loadFile(fileName); const file = loadFile(fileName);
if ((file as Oas2Definition).swagger) exitWithError('OpenAPI 2 is not supported by this command'); if ((file as Oas2Definition).swagger) exitWithError('OpenAPI 2 is not supported by this command');
if (!(file as Oas3Definition).openapi) exitWithError('File does not conform to the OpenAPI Specification. OpenAPI version is not specified'); if (!(file as Oas3Definition | Oas3_1Definition).openapi) exitWithError('File does not conform to the OpenAPI Specification. OpenAPI version is not specified');
return true; return true;
} }
@@ -100,19 +94,30 @@ function langToExt(lang: string) {
return langObj[lang]; return langObj[lang];
} }
function traverseDirectoryDeep(directory: string, callback: any) { function traverseDirectoryDeep(directory: string, callback: any, componentsFiles: object) {
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) return; if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) return;
const files = fs.readdirSync(directory); const files = fs.readdirSync(directory);
for (const f of files) { for (const f of files) {
const filename = path.join(directory, f); const filename = path.join(directory, f);
if (fs.statSync(filename).isDirectory()) { if (fs.statSync(filename).isDirectory()) {
traverseDirectoryDeep(filename, callback); traverseDirectoryDeep(filename, callback, componentsFiles);
} else { } else {
callback(filename, directory); callback(filename, directory, componentsFiles);
} }
} }
} }
function traverseDirectoryDeepCallback(
filename: string,
directory: string,
componentsFiles: object,
) {
if (isNotYaml(filename)) return;
const pathData = readYaml(filename);
replace$Refs(pathData, directory, componentsFiles);
writeYaml(pathData, filename);
}
function crawl(object: any, visitor: any) { function crawl(object: any, visitor: any) {
if (!isObject(object)) return; if (!isObject(object)) return;
for (const key of Object.keys(object)) { for (const key of Object.keys(object)) {
@@ -199,7 +204,7 @@ function doesFileDiffer(filename: string, componentData: any) {
return fs.existsSync(filename) && !isEqual(readYaml(filename), componentData); return fs.existsSync(filename) && !isEqual(readYaml(filename), componentData);
} }
function removeEmptyComponents(openapi: Oas3Definition, componentType: Oas3ComponentName) { function removeEmptyComponents(openapi: Oas3Definition | Oas3_1Definition, componentType: Oas3ComponentName) {
if (openapi.components && isEmptyObject(openapi.components[componentType])) { if (openapi.components && isEmptyObject(openapi.components[componentType])) {
delete openapi.components[componentType]; delete openapi.components[componentType];
} }
@@ -237,49 +242,57 @@ function gatherComponentsFiles(
componentsFiles[componentType][componentName] = { inherits, filename }; componentsFiles[componentType][componentName] = { inherits, filename };
} }
function iteratePaths( function iteratePathItems(
openapi: Oas3Definition, pathItems: Record<string, Referenced<Oas3PathItem>> | undefined,
pathsDir: string, openapiDir: string,
openapiDir: string outDir: string,
componentsFiles: object,
codeSamplesPathPrefix: string = '',
) { ) {
const { paths } = openapi; if (!pathItems) return;
if (paths) { fs.mkdirSync(outDir, { recursive: true });
for (const oasPath of Object.keys(paths)) {
const pathFile = path.join(pathsDir, pathToFilename(oasPath)) + '.yaml'; for (const pathName of Object.keys(pathItems)) {
const pathData: Oas3PathItem = paths[oasPath] as Oas3PathItem; const pathFile = `${path.join(outDir, pathToFilename(pathName))}.yaml`;
const pathData = pathItems[pathName] as Oas3PathItem;
if (isRef(pathData)) continue;
for (const method of OPENAPI3_METHOD_NAMES) { for (const method of OPENAPI3_METHOD_NAMES) {
const methodData = pathData[method]; const methodData = pathData[method];
const methodDataXCode = methodData?.['x-code-samples'] || methodData?.['x-codeSamples']; const methodDataXCode = methodData?.['x-code-samples'] || methodData?.['x-codeSamples'];
if (!methodDataXCode || !Array.isArray(methodDataXCode)) { continue; } if (!methodDataXCode || !Array.isArray(methodDataXCode)) {
continue;
}
for (const sample of methodDataXCode) { for (const sample of methodDataXCode) {
if (sample.source && (sample.source as any).$ref) continue; if (sample.source && (sample.source as any).$ref) continue;
const sampleFileName = path.join( const sampleFileName = path.join(
openapiDir, openapiDir,
'code_samples', 'code_samples',
sample.lang, sample.lang,
pathToFilename(oasPath), codeSamplesPathPrefix + pathToFilename(pathName),
method + langToExt(sample.lang) method + langToExt(sample.lang),
); );
fs.mkdirSync(path.dirname(sampleFileName), { recursive: true }); fs.mkdirSync(path.dirname(sampleFileName), { recursive: true });
fs.writeFileSync(sampleFileName, sample.source); fs.writeFileSync(sampleFileName, sample.source);
// @ts-ignore // @ts-ignore
sample.source = { sample.source = {
$ref: slash(path.relative(pathsDir, sampleFileName)) $ref: slash(path.relative(outDir, sampleFileName))
}; };
} }
} }
writeYaml(pathData, pathFile); writeYaml(pathData, pathFile);
paths[oasPath] = { pathItems[pathName] = {
$ref: slash(path.relative(openapiDir, pathFile)) $ref: slash(path.relative(openapiDir, pathFile))
}; };
}
traverseDirectoryDeep(outDir, traverseDirectoryDeepCallback, componentsFiles);
} }
} }
function iterateComponents( function iterateComponents(
openapi: Oas3Definition, openapi: Oas3Definition | Oas3_1Definition,
openapiDir: string, openapiDir: string,
componentsFiles: ComponentsFiles componentsFiles: ComponentsFiles
) { ) {
@@ -331,6 +344,4 @@ function iterateComponents(
} }
} }
export { export { iteratePathItems };
iteratePaths,
}

View File

@@ -1,14 +1,18 @@
import { import {
Oas3Schema, Oas3Schema,
Oas3_1Schema,
Oas3Definition, Oas3Definition,
Oas3_1Definition,
Oas3Components, Oas3Components,
Oas3PathItem, Oas3PathItem,
Oas3Paths, Oas3Paths,
Oas3ComponentName, Oas3ComponentName,
Oas2Definition Oas3_1Webhooks,
Oas2Definition,
Referenced
} from "@redocly/openapi-core"; } from "@redocly/openapi-core";
export { Oas3Definition, Oas2Definition, Oas3Components, Oas3Paths, Oas3PathItem, Oas3ComponentName, Oas3Schema } export { Oas3_1Definition, Oas3Definition, Oas2Definition, Oas3Components, Oas3Paths, Oas3PathItem, Oas3ComponentName, Oas3_1Schema, Oas3Schema, Oas3_1Webhooks, Referenced }
export type Definition = Oas3Definition | Oas2Definition; export type Definition = Oas3_1Definition | Oas3Definition | Oas2Definition;
export interface ComponentsFiles { export interface ComponentsFiles {
[schemas: string]: any; [schemas: string]: any;
} }
@@ -18,6 +22,8 @@ export interface refObj {
export const COMPONENTS = 'components'; export const COMPONENTS = 'components';
export const PATHS = 'paths'; export const PATHS = 'paths';
export const WEBHOOKS = 'webhooks';
export const xWEBHOOKS = 'x-webhooks';
export const componentsPath = `#/${COMPONENTS}/`; export const componentsPath = `#/${COMPONENTS}/`;
enum OPENAPI3_METHOD { enum OPENAPI3_METHOD {

View File

@@ -35,13 +35,21 @@ yargs
'split [entrypoint]', 'split [entrypoint]',
'Split definition into a multi-file structure.', 'Split definition into a multi-file structure.',
(yargs) => (yargs) =>
yargs.positional('entrypoint', { type: 'string' }).option({ yargs
.positional('entrypoint', {
description: 'API definition file that you want to split',
type: 'string'
})
.option({
outDir: { outDir: {
description: 'Output directory where files will be saved.', description: 'Output directory where files will be saved.',
required: true, required: true,
type: 'string', type: 'string',
}, },
}), })
.demandOption(
'entrypoint'
),
handleSplit, handleSplit,
) )
.command( .command(
@@ -207,7 +215,7 @@ yargs
description: 'Remove unused components.', description: 'Remove unused components.',
type: 'boolean', type: 'boolean',
default: false, default: false,
} },
}), }),
(argv) => { (argv) => {
handleBundle(argv, version); handleBundle(argv, version);

View File

@@ -65,7 +65,7 @@ export function pathToFilename(path: string) {
return path return path
.replace(/~1/g, '/') .replace(/~1/g, '/')
.replace(/~0/g, '~') .replace(/~0/g, '~')
.substring(1) .replace(/^\//, '')
.replace(/\//g, '@'); .replace(/\//g, '@');
} }

View File

@@ -5,12 +5,16 @@ export { Oas2Types } from './types/oas2';
export { ConfigTypes } from './types/redocly-yaml'; export { ConfigTypes } from './types/redocly-yaml';
export { export {
Oas3Definition, Oas3Definition,
Oas3_1Definition,
Oas3Components, Oas3Components,
Oas3PathItem, Oas3PathItem,
Oas3Paths, Oas3Paths,
Oas3ComponentName, Oas3ComponentName,
Oas3Schema, Oas3Schema,
Oas3_1Schema,
Oas3Tag, Oas3Tag,
Oas3_1Webhooks,
Referenced
} from './typings/openapi'; } from './typings/openapi';
export { Oas2Definition } from './typings/swagger'; export { Oas2Definition } from './typings/swagger';
export { StatsAccumulator, StatsName } from './typings/common'; export { StatsAccumulator, StatsName } from './typings/common';
@@ -30,7 +34,7 @@ export {
makeDocumentFromString, makeDocumentFromString,
} from './resolve'; } from './resolve';
export { parseYaml, stringifyYaml } from './js-yaml'; export { parseYaml, stringifyYaml } from './js-yaml';
export { unescapePointer } from './ref-utils'; export { unescapePointer, isRef } from './ref-utils';
export { detectOpenAPI, OasMajorVersion, openAPIMajor, OasVersion } from './oas-types'; export { detectOpenAPI, OasMajorVersion, openAPIMajor, OasVersion } from './oas-types';
export { normalizeVisitors } from './visitors'; export { normalizeVisitors } from './visitors';

View File

@@ -7,6 +7,7 @@ export interface Oas3Definition {
security?: Oas3SecurityRequirement[]; security?: Oas3SecurityRequirement[];
tags?: Oas3Tag[]; tags?: Oas3Tag[];
externalDocs?: Oas3ExternalDocs; externalDocs?: Oas3ExternalDocs;
'x-webhooks'?: Oas3_1Webhooks;
} }
export interface Oas3Info { export interface Oas3Info {
@@ -154,6 +155,14 @@ export interface Oas3_1Schema extends Oas3Schema {
examples?: any[]; examples?: any[];
} }
export interface Oas3_1Definition extends Oas3Definition {
webhooks?: Oas3_1Webhooks;
}
export interface Oas3_1Webhooks {
[webhook: string]: Referenced<Oas3PathItem>;
}
export interface Oas3Discriminator { export interface Oas3Discriminator {
propertyName: string; propertyName: string;
mapping?: { [name: string]: string }; mapping?: { [name: string]: string };