Skip to content

Commit a28c13b

Browse files
committed
Split openAPI in files with references
1 parent 80cec9e commit a28c13b

File tree

11 files changed

+204
-57
lines changed

11 files changed

+204
-57
lines changed

bin/__snapshots__/cli.test.js.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ OpenAPI-Format CLI options:
2222
| sort | false |
2323
| keepComments | true |
2424
| lineWidth | -1 |
25+
| bundle | true |
26+
| split | false |
2527
| filterFile | test/yaml-no-sort-keep-comments/customFilter.yaml |
2628
| verbose | 3 |
2729
|--------------|---------------------------------------------------|
@@ -91,6 +93,8 @@ Options:
9193
--lineWidth <lineWidth> max line width of YAML output (default: -1)
9294
--rename <oaTitle> overwrite the title in the OpenAPI document
9395
--convertTo <oaVersion> convert the OpenAPI document to OpenAPI version 3.1
96+
--no-bundle Bundle the local and remote $ref in the OpenAPI document.
97+
--split Split the OpenAPI document into a multi-file structure. (default: false)
9498
--json print the file to stdout as JSON
9599
--yaml print the file to stdout as YAML
96100
-p, --playground Open config in online playground
@@ -133,7 +137,8 @@ Total components removed: 16
133137
exports[`openapi-format CLI command should split the openapi file 1`] = `
134138
"================================================================================
135139
OpenAPI-Format CLI settings:
136-
- Input file: test/__utils__/mockTrain.yaml
140+
- Input file: ../__utils__/train.yaml
141+
- Output location: output.yaml
137142
================================================================================
138143
✅ OpenAPI formatted successfully
139144
================================================================================
@@ -224,6 +229,8 @@ OpenAPI-Format CLI options:
224229
| sort | false |
225230
| keepComments | false |
226231
| lineWidth | -1 |
232+
| bundle | true |
233+
| split | false |
227234
| convertTo | 3.1 |
228235
| verbose | 3 |
229236
|--------------|-------|

bin/cli.js

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -265,18 +265,27 @@ async function run(oaFile, options) {
265265

266266
options.yamlComments = fileOptions.yamlComments || {};
267267
if (options.output) {
268-
try {
269-
// Write OpenAPI string to file
270-
await openapiFormat.writeFile(options.output, resObj, options);
271-
infoOut(`- Output file:\t\t${options.output}`); // LOG - config file
272-
273-
if (options.split === true) {
274-
await openapiFormat.openapiSplit(resObj, options);
268+
if (options.split !== true) {
269+
try {
270+
// Write OpenAPI string to single file
271+
await openapiFormat.writeFile(options.output, resObj, options);
272+
infoOut(`- Output file:\t\t${options.output}`); // LOG - config file
273+
} catch (err) {
274+
console.error('\x1b[31m', `Output file error - no such file or directory "${options.output}"`);
275+
if (options.verbose >= 1) {
276+
console.error(err);
277+
}
275278
}
276-
} catch (err) {
277-
console.error('\x1b[31m', `Output file error - no such file or directory "${options.output}"`);
278-
if (options.verbose >= 1) {
279-
console.error(err);
279+
} else {
280+
try {
281+
// Write Split files
282+
await openapiFormat.openapiSplit(resObj, options);
283+
infoOut(`- Output location:\t${options.output}`); // LOG - config file
284+
} catch (err) {
285+
console.error('\x1b[31m', `Split error - no such file or directory "${options.output}"`);
286+
if (options.verbose >= 1) {
287+
console.error(err);
288+
}
280289
}
281290
}
282291
} else {

bin/cli.test.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const testUtils = require('../test/__utils__/test-utils');
22
const fs = require('fs');
33
const {describe, it, expect} = require('@jest/globals');
4-
const {getLocalFile, getRemoteFile, parseFile} = require('../utils/file');
4+
const {getLocalFile} = require('../utils/file');
55

66
describe('openapi-format CLI command', () => {
77
it('should output the help', async () => {
@@ -221,16 +221,34 @@ describe('openapi-format CLI command', () => {
221221

222222
it('should split the openapi file', async () => {
223223
const path = `test/_split`;
224-
const inputFile = `test/__utils__/mockTrain.yaml`;
225-
// const outputFile = `${path}/output.yaml`;
226-
// const output = await getLocalFile(outputFile);
224+
const inputFile = `../__utils__/train.yaml`;
225+
const outputFile = `output.yaml`;
226+
227+
const snap = await getLocalFile(`${path}/snap.yaml`);
228+
const snap_station = await getLocalFile(`${path}/snap_station.yaml`);
229+
const snap_station_id = await getLocalFile(`${path}/snap_station_id.yaml`);
230+
231+
let result = await testUtils.cli([inputFile,`--output ${outputFile}`, `--split`, `--no-sort`], path);
232+
233+
const outputPath = `${path}/${outputFile}`
234+
const outputStationPath = `${path}/paths/stations_{station_id}.yaml`
235+
const outputStationIdPath = `${path}/components/parameters/StationId.yaml`
236+
// const outputStationSchemaPath = `${path}/components/schemas/Station.yaml`
237+
const output = await getLocalFile(outputPath);
238+
const output_station = await getLocalFile(outputStationPath);
239+
const output_station_id = await getLocalFile(outputStationIdPath);
227240

228-
let result = await testUtils.cli([inputFile, `--split`, `--no-sort`], '.');
229241
// console.log('result', result)
230242
expect(result.code).toBe(0);
231243
expect(result.stdout).toContain('formatted successfully');
232244
expect(result.stdout).toMatchSnapshot();
233-
// expect(sanitize(result.stderr)).toStrictEqual(sanitize(output));
245+
expect(sanitize(snap)).toStrictEqual(sanitize(output));
246+
expect(sanitize(snap_station)).toStrictEqual(sanitize(output_station));
247+
expect(sanitize(snap_station_id)).toStrictEqual(sanitize(output_station_id));
248+
249+
fs.rmSync(`${path}/paths`, { recursive: true, force: true });
250+
fs.rmSync(`${path}/components`, { recursive: true, force: true });
251+
fs.unlinkSync(outputPath);
234252
});
235253

236254
it.skip('should generate a playground share URL', async () => {

openapi-format.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ const {
3030
} = require('./utils/convert');
3131
const {parseFile, writeFile, stringify, detectFormat, parseString, analyzeOpenApi, readFile} = require('./utils/file');
3232
const {parseTpl, getOperation} = require('./utils/parseTpl');
33-
const {writeMainOpenAPISpec, writePaths, writeComponents} = require('./utils/split');
33+
const {writePaths, writeComponents, writeSplitOpenAPISpec} = require('./utils/split');
34+
const {dirname, extname} = require('node:path');
35+
3436

3537
/**
3638
* OpenAPI sort function
@@ -1022,12 +1024,23 @@ async function openapiGenerate(oaObj, options) {
10221024
* @param options
10231025
* @returns {Promise<void>}
10241026
*/
1025-
async function openapiSplit(oaObj, options) {
1026-
await writeComponents(oaObj?.components, options);
1027+
async function openapiSplit(oaObj, options = {}) {
1028+
if(!options.output) {
1029+
throw new Error('Output is required')
1030+
}
1031+
1032+
options.outputDir = dirname(options.output);
1033+
options.extension = extname(options.output).substring(1)
10271034

1028-
await writePaths(oaObj?.paths, options);
1035+
if(oaObj?.components) {
1036+
await writeComponents(oaObj.components, options);
1037+
}
1038+
1039+
if(oaObj?.paths) {
1040+
await writePaths(oaObj.paths, options);
1041+
}
10291042

1030-
await writeMainOpenAPISpec(oaObj, options);
1043+
await writeSplitOpenAPISpec(oaObj, options);
10311044
}
10321045

10331046
/**

test/__utils__/train.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Train Travel API
4+
version: 1.0.0
5+
description: A compact version of the Train Travel API
6+
7+
paths:
8+
/stations/{station_id}:
9+
get:
10+
summary: Get station information
11+
operationId: getStation
12+
parameters:
13+
- $ref: '#/components/parameters/StationId'
14+
responses:
15+
'200':
16+
description: Successful operation
17+
content:
18+
application/json:
19+
schema:
20+
$ref: '#/components/schemas/Station'
21+
'404':
22+
description: Station not found
23+
components:
24+
schemas:
25+
Station:
26+
type: object
27+
properties:
28+
id:
29+
type: string
30+
description: Unique identifier for the station
31+
name:
32+
type: string
33+
description: Name of the station
34+
location:
35+
type: object
36+
properties:
37+
latitude:
38+
type: number
39+
format: float
40+
longitude:
41+
type: number
42+
format: float
43+
description: Location of the station
44+
parameters:
45+
StationId:
46+
name: station_id
47+
in: path
48+
required: true
49+
schema:
50+
type: string
51+
description: The ID of the station

test/_split/snap.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Train Travel API
4+
version: 1.0.0
5+
description: A compact version of the Train Travel API
6+
paths:
7+
'/stations/{station_id}':
8+
$ref: 'paths/stations_{station_id}.yaml'
9+
components:
10+
schemas:
11+
Station:
12+
$ref: "components/schemas/Station.yaml"
13+
parameters:
14+
StationId:
15+
$ref: "components/parameters/StationId.yaml"

test/_split/snap_station.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
get:
2+
summary: Get station information
3+
operationId: getStation
4+
parameters:
5+
- $ref: "../components/parameters/StationId.yaml"
6+
responses:
7+
'200':
8+
description: Successful operation
9+
content:
10+
application/json:
11+
schema:
12+
$ref: "../components/schemas/Station.yaml"
13+
'404':
14+
description: Station not found

test/_split/snap_station_id.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: station_id
2+
in: path
3+
required: true
4+
schema:
5+
type: string
6+
description: The ID of the station

test/split.test.js

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
'use strict';
22

3-
const {writeMainOpenAPISpec, writePaths, writeComponents, sanitizeFileName} = require('./../utils/split');
4-
const of = require('./../openapi-format');
3+
const {writeFile} = require('./../utils/file');
4+
const {
5+
writeSplitOpenAPISpec,
6+
writePaths,
7+
writeComponents,
8+
sanitizeFileName,
9+
convertComponentsToRef
10+
} = require('./../utils/split');
511
const {describe, it, expect} = require('@jest/globals');
612
const path = require('path');
7-
const {writeFile} = require('./../utils/file');
8-
const {convertComponentsToRef} = require('../utils/split');
13+
const fs = require('node:fs');
914

1015
jest.mock('./../utils/file', () => ({
1116
writeFile: jest.fn()
1217
}));
1318

19+
jest.mock('node:fs', () => ({
20+
mkdirSync: jest.fn()
21+
}));
22+
1423
describe('openapi-format CLI splits tests', () => {
1524
const options = {
1625
outputDir: '/fake/output/dir',
17-
someOtherOption: true
26+
format: 'yaml',
27+
output: 'openapi.yaml'
1828
};
1929

2030
const openapiDoc = {
@@ -40,7 +50,7 @@ describe('openapi-format CLI splits tests', () => {
4050
});
4151

4252
it('should split and write main openapi spec with $refs', async () => {
43-
await writeMainOpenAPISpec(openapiDoc, options);
53+
await writeSplitOpenAPISpec(openapiDoc, options);
4454

4555
// Assert that the main openapi.yaml file is written with $refs
4656
expect(writeFile).toHaveBeenCalledWith(
@@ -71,7 +81,7 @@ describe('openapi-format CLI splits tests', () => {
7181
// Assert that each path is written to its own file
7282
expect(writeFile).toHaveBeenCalledWith(
7383
path.join(options.outputDir, 'paths', 'example_foo_{id}.yaml'),
74-
{'/example/foo/{id}': paths['/example/foo/{id}']},
84+
paths['/example/foo/{id}'],
7585
options
7686
);
7787
});
@@ -84,7 +94,7 @@ describe('openapi-format CLI splits tests', () => {
8494
// Assert that each component is written to its own file
8595
expect(writeFile).toHaveBeenCalledWith(
8696
path.join(options.outputDir, 'components/schemas', 'ExampleSchema.yaml'),
87-
{ExampleSchema: components.schemas.ExampleSchema},
97+
components.schemas.ExampleSchema,
8898
options
8999
);
90100
});
@@ -111,10 +121,8 @@ describe('openapi-format CLI splits tests', () => {
111121
}
112122
};
113123

114-
const ext = 'yaml';
115-
116124
// Call convertComponentsToRef without mocking traverse
117-
const result = convertComponentsToRef(components, ext);
125+
const result = convertComponentsToRef(components, 'yaml', '.');
118126

119127
// Assert that the $ref is converted correctly
120128
expect(result.schemas.ExampleSchema.properties.related.$ref).toBe('components/schemas/RelatedSchema.yaml');
@@ -142,10 +150,8 @@ describe('openapi-format CLI splits tests', () => {
142150
}
143151
};
144152

145-
const ext = 'yaml';
146-
147153
// Call convertComponentsToRef to check nested $ref conversion
148-
const result = convertComponentsToRef(components, ext);
154+
const result = convertComponentsToRef(components, 'yaml', '.');
149155

150156
// Assert that the deeply nested $ref is converted
151157
expect(result.schemas.NestedSchema.properties.nested.properties.deep.$ref).toBe(

utils/file.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const bundler = require('api-ref-bundler');
55
const yaml = require('@stoplight/yaml');
66
const http = require('http');
77
const https = require('https');
8+
const {dirname} = require('node:path');
89

910
/**
1011
* Converts a string object to a JSON/YAML object.
@@ -212,6 +213,11 @@ async function writeFile(filePath, data, options = {}) {
212213
output = await stringify(data, options);
213214
}
214215

216+
const dir = dirname(filePath);
217+
if (!fs.existsSync(dir)) {
218+
fs.mkdirSync(dir, { recursive: true });
219+
}
220+
215221
// Write the output to the file
216222
fs.writeFileSync(filePath, output, 'utf8');
217223
} catch (err) {

0 commit comments

Comments
 (0)