Skip to content

Commit 3a32cc2

Browse files
authored
feat(openapi): specify spec path and type in success response (#480)
* feat(openapi): specify spec path in success response * docs: corresponding docs update * refactor: separate OAS prep into its own function This way we can reuse it in our `validate` and `openapi` commands and allow for a shared way to specify whether the file is OpenAPI or Swagger. This also ended up fixing some inconsistencies with our debugging and error handling * fix: remove old `oas` guidance * feat: specify the spec type in another place * docs: update docs accordingly * fix(prepareOas): error handling + debugging
1 parent 7ffc1f5 commit 3a32cc2

File tree

6 files changed

+109
-82
lines changed

6 files changed

+109
-82
lines changed

__tests__/cmds/__snapshots__/validate.test.js.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`rdme validate error handling should throw an error if an in valid Swagger definition is supplied 1`] = `
4-
[Error: Swagger schema validation failed.
4+
[SyntaxError: Swagger schema validation failed.
55
66
ADDITIONAL PROPERTY must NOT have additional properties
77
@@ -15,7 +15,7 @@ ADDITIONAL PROPERTY must NOT have additional properties
1515
`;
1616

1717
exports[`rdme validate error handling should throw an error if an invalid OpenAPI 3.1 definition is supplied 1`] = `
18-
[Error: OpenAPI schema validation failed.
18+
[SyntaxError: OpenAPI schema validation failed.
1919
2020
REQUIRED must have required property 'name'
2121

__tests__/cmds/openapi.test.js

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,25 @@ const key = 'API_KEY';
1515
const id = '5aa0409b7cf527a93bfb44df';
1616
const version = '1.0.0';
1717
const exampleRefLocation = `${config.get('host')}/project/example-project/1.0.1/refs/ex`;
18-
const successfulMessageBase = [
18+
const successfulMessageBase = (specPath, specType) => [
1919
'',
2020
`\t${chalk.green(exampleRefLocation)}`,
2121
'',
22-
'To update your OpenAPI or Swagger definition, run the following:',
22+
`To update your ${specType} definition, run the following:`,
2323
'',
24-
`\t${chalk.green(`rdme openapi FILE --key=${key} --id=1`)}`,
24+
`\t${chalk.green(`rdme openapi ${specPath} --key=${key} --id=1`)}`,
2525
];
26-
const successfulUpload = [
27-
"You've successfully uploaded a new OpenAPI file to your ReadMe project!",
28-
...successfulMessageBase,
29-
].join('\n');
30-
31-
const successfulUpdate = [
32-
"You've successfully updated an OpenAPI file on your ReadMe project!",
33-
...successfulMessageBase,
34-
].join('\n');
26+
const successfulUpload = (specPath, specType = 'OpenAPI') =>
27+
[
28+
`You've successfully uploaded a new ${specType} file to your ReadMe project!`,
29+
...successfulMessageBase(specPath, specType),
30+
].join('\n');
31+
32+
const successfulUpdate = (specPath, specType = 'OpenAPI') =>
33+
[
34+
`You've successfully updated an existing ${specType} file on your ReadMe project!`,
35+
...successfulMessageBase(specPath, specType),
36+
].join('\n');
3537

3638
const testWorkingDir = process.cwd();
3739

@@ -60,13 +62,13 @@ describe('rdme openapi', () => {
6062

6163
describe('upload', () => {
6264
it.each([
63-
['Swagger 2.0', 'json', '2.0'],
64-
['Swagger 2.0', 'yaml', '2.0'],
65-
['OpenAPI 3.0', 'json', '3.0'],
66-
['OpenAPI 3.0', 'yaml', '3.0'],
67-
['OpenAPI 3.1', 'json', '3.1'],
68-
['OpenAPI 3.1', 'yaml', '3.1'],
69-
])('should support uploading a %s definition (format: %s)', async (_, format, specVersion) => {
65+
['Swagger 2.0', 'json', '2.0', 'Swagger'],
66+
['Swagger 2.0', 'yaml', '2.0', 'Swagger'],
67+
['OpenAPI 3.0', 'json', '3.0', 'OpenAPI'],
68+
['OpenAPI 3.0', 'yaml', '3.0', 'OpenAPI'],
69+
['OpenAPI 3.1', 'json', '3.1', 'OpenAPI'],
70+
['OpenAPI 3.1', 'yaml', '3.1', 'OpenAPI'],
71+
])('should support uploading a %s definition (format: %s)', async (_, format, specVersion, type) => {
7072
const mock = getApiNock()
7173
.get('/api/v1/api-specification')
7274
.basicAuth({ user: key })
@@ -78,13 +80,15 @@ describe('rdme openapi', () => {
7880
.basicAuth({ user: key })
7981
.reply(201, { _id: 1 }, { location: exampleRefLocation });
8082

83+
const spec = require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`);
84+
8185
await expect(
8286
openapi.run({
83-
spec: require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`),
87+
spec,
8488
key,
8589
version,
8690
})
87-
).resolves.toBe(successfulUpload);
91+
).resolves.toBe(successfulUpload(spec, type));
8892

8993
expect(console.info).toHaveBeenCalledTimes(0);
9094

@@ -114,7 +118,7 @@ describe('rdme openapi', () => {
114118
// to break.
115119
fs.copyFileSync(require.resolve('@readme/oas-examples/2.0/json/petstore.json'), './swagger.json');
116120

117-
await expect(openapi.run({ key })).resolves.toBe(successfulUpload);
121+
await expect(openapi.run({ key })).resolves.toBe(successfulUpload('swagger.json', 'Swagger'));
118122

119123
expect(console.info).toHaveBeenCalledTimes(1);
120124

@@ -128,26 +132,28 @@ describe('rdme openapi', () => {
128132

129133
describe('updates / resyncs', () => {
130134
it.each([
131-
['Swagger 2.0', 'json', '2.0'],
132-
['Swagger 2.0', 'yaml', '2.0'],
133-
['OpenAPI 3.0', 'json', '3.0'],
134-
['OpenAPI 3.0', 'yaml', '3.0'],
135-
['OpenAPI 3.1', 'json', '3.1'],
136-
['OpenAPI 3.1', 'yaml', '3.1'],
137-
])('should support updating a %s definition (format: %s)', async (_, format, specVersion) => {
135+
['Swagger 2.0', 'json', '2.0', 'Swagger'],
136+
['Swagger 2.0', 'yaml', '2.0', 'Swagger'],
137+
['OpenAPI 3.0', 'json', '3.0', 'OpenAPI'],
138+
['OpenAPI 3.0', 'yaml', '3.0', 'OpenAPI'],
139+
['OpenAPI 3.1', 'json', '3.1', 'OpenAPI'],
140+
['OpenAPI 3.1', 'yaml', '3.1', 'OpenAPI'],
141+
])('should support updating a %s definition (format: %s)', async (_, format, specVersion, type) => {
138142
const mock = getApiNock()
139143
.put(`/api/v1/api-specification/${id}`, body => body.match('form-data; name="spec"'))
140144
.basicAuth({ user: key })
141145
.reply(201, { _id: 1 }, { location: exampleRefLocation });
142146

147+
const spec = require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`);
148+
143149
await expect(
144150
openapi.run({
145-
spec: require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`),
151+
spec,
146152
key,
147153
id,
148154
version,
149155
})
150-
).resolves.toBe(successfulUpdate);
156+
).resolves.toBe(successfulUpdate(spec, type));
151157

152158
return mock.done();
153159
});
@@ -158,9 +164,9 @@ describe('rdme openapi', () => {
158164
.basicAuth({ user: key })
159165
.reply(201, { _id: 1 }, { location: exampleRefLocation });
160166

161-
await expect(
162-
openapi.run({ spec: require.resolve('@readme/oas-examples/3.1/json/petstore.json'), key, id, version })
163-
).resolves.toBe(successfulUpdate);
167+
const spec = require.resolve('@readme/oas-examples/3.1/json/petstore.json');
168+
169+
await expect(openapi.run({ spec, key, id, version })).resolves.toBe(successfulUpdate(spec));
164170

165171
expect(console.warn).toHaveBeenCalledTimes(1);
166172
expect(console.info).toHaveBeenCalledTimes(0);
@@ -225,9 +231,9 @@ describe('rdme openapi', () => {
225231
.basicAuth({ user: key })
226232
.reply(201, { _id: 1 }, { location: exampleRefLocation });
227233

228-
await expect(
229-
openapi.run({ spec: require.resolve('@readme/oas-examples/2.0/json/petstore.json'), key })
230-
).resolves.toBe(successfulUpload);
234+
const spec = require.resolve('@readme/oas-examples/2.0/json/petstore.json');
235+
236+
await expect(openapi.run({ spec, key })).resolves.toBe(successfulUpload(spec, 'Swagger'));
231237

232238
return mock.done();
233239
});
@@ -251,9 +257,9 @@ describe('rdme openapi', () => {
251257
.basicAuth({ user: key })
252258
.reply(201, { _id: 1 }, { location: exampleRefLocation });
253259

254-
await expect(openapi.run({ spec: './__tests__/__fixtures__/ref-oas/petstore.json', key, version })).resolves.toBe(
255-
successfulUpload
256-
);
260+
const spec = './__tests__/__fixtures__/ref-oas/petstore.json';
261+
262+
await expect(openapi.run({ spec, key, version })).resolves.toBe(successfulUpload(spec));
257263

258264
expect(console.info).toHaveBeenCalledTimes(0);
259265

@@ -280,14 +286,16 @@ describe('rdme openapi', () => {
280286
.basicAuth({ user: key })
281287
.reply(201, { _id: 1 }, { location: exampleRefLocation });
282288

289+
const spec = 'petstore.json';
290+
283291
await expect(
284292
openapi.run({
285-
spec: 'petstore.json',
293+
spec,
286294
key,
287295
version,
288296
workingDirectory: './__tests__/__fixtures__/relative-ref-oas',
289297
})
290-
).resolves.toBe(successfulUpload);
298+
).resolves.toBe(successfulUpload(spec));
291299

292300
expect(console.info).toHaveBeenCalledTimes(0);
293301

@@ -460,7 +468,7 @@ describe('rdme swagger', () => {
460468
it('should run `rdme openapi`', () => {
461469
return expect(swagger.run({ spec: '', key, id, version })).rejects.toThrow(
462470
"We couldn't find an OpenAPI or Swagger definition.\n\n" +
463-
'Run `rdme openapi ./path/to/api/definition` to upload an existing definition or `rdme oas init` to create a fresh one!'
471+
'Please specify the path to your definition with `rdme openapi ./path/to/api/definition`.'
464472
);
465473
});
466474
});

documentation/rdme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ You've successfully updated an OpenAPI file on your ReadMe project!
133133
134134
http://dash.readme.com/project/{your_project}/v1.0/refs/pet
135135
136-
To update your OpenAPI or Swagger definition, run the following:
136+
To update your OpenAPI definition, run the following:
137137
138-
rdme openapi FILE --key=<<user>> --id=API_DEFINITION_ID
138+
rdme openapi [path-to-file.json] --key=<<user>> --id=API_DEFINITION_ID
139139
```
140140

141141
</details>

src/cmds/openapi.js

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ const chalk = require('chalk');
22
const fs = require('fs');
33
const config = require('config');
44
const { prompt } = require('enquirer');
5-
const OASNormalize = require('oas-normalize');
65
const promptOpts = require('../lib/prompts');
76
const APIError = require('../lib/apiError');
87
const { getProjectVersion } = require('../lib/versionSelect');
@@ -12,6 +11,7 @@ const FormData = require('form-data');
1211
const parse = require('parse-link-header');
1312
const { file: tmpFile } = require('tmp-promise');
1413
const { debug } = require('../lib/logger');
14+
const prepareOas = require('../lib/prepareOas');
1515

1616
module.exports = class OpenAPICommand {
1717
constructor() {
@@ -81,13 +81,12 @@ module.exports = class OpenAPICommand {
8181
}
8282

8383
async function callApi(specPath, versionCleaned) {
84-
debug(`bundling and validating spec located at ${specPath}`);
85-
// @todo Tailor messaging to what is actually being handled here. If the user is uploading a Swagger file, never mention that they uploaded/updated an OpenAPI file.
84+
const { bundledSpec, specType } = await prepareOas(specPath, true);
8685

8786
async function success(data) {
8887
const message = !isUpdate
89-
? "You've successfully uploaded a new OpenAPI file to your ReadMe project!"
90-
: "You've successfully updated an OpenAPI file on your ReadMe project!";
88+
? `You've successfully uploaded a new ${specType} file to your ReadMe project!`
89+
: `You've successfully updated an existing ${specType} file on your ReadMe project!`;
9190

9291
debug(`successful ${data.status} response`);
9392
const body = await data.json();
@@ -99,10 +98,10 @@ module.exports = class OpenAPICommand {
9998
'',
10099
`\t${chalk.green(`${data.headers.get('location')}`)}`,
101100
'',
102-
'To update your OpenAPI or Swagger definition, run the following:',
101+
`To update your ${specType} definition, run the following:`,
103102
'',
104103
// eslint-disable-next-line no-underscore-dangle
105-
`\t${chalk.green(`rdme openapi FILE --key=${key} --id=${body._id}`)}`,
104+
`\t${chalk.green(`rdme openapi ${specPath} --key=${key} --id=${body._id}`)}`,
106105
].join('\n')
107106
);
108107
}
@@ -127,17 +126,6 @@ module.exports = class OpenAPICommand {
127126
}
128127
}
129128

130-
const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true });
131-
debug('spec normalized');
132-
133-
await oas.validate(false);
134-
debug('spec validated');
135-
136-
const bundledSpec = await oas.bundle().then(res => {
137-
return JSON.stringify(res);
138-
});
139-
debug('spec bundled');
140-
141129
// Create a temporary file to write the bundled spec to,
142130
// which we will then stream into the form data body
143131
const { path } = await tmpFile({ prefix: 'rdme-openapi-', postfix: '.json' });
@@ -244,7 +232,7 @@ module.exports = class OpenAPICommand {
244232
reject(
245233
new Error(
246234
"We couldn't find an OpenAPI or Swagger definition.\n\n" +
247-
'Run `rdme openapi ./path/to/api/definition` to upload an existing definition or `rdme oas init` to create a fresh one!'
235+
'Please specify the path to your definition with `rdme openapi ./path/to/api/definition`.'
248236
)
249237
);
250238
});

src/cmds/validate.js

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const chalk = require('chalk');
22
const fs = require('fs');
3-
const OASNormalize = require('oas-normalize');
43
const { debug } = require('../lib/logger');
4+
const prepareOas = require('../lib/prepareOas');
55

66
module.exports = class ValidateCommand {
77
constructor() {
@@ -37,20 +37,8 @@ module.exports = class ValidateCommand {
3737
debug(`opts: ${JSON.stringify(opts)}`);
3838

3939
async function validateSpec(specPath) {
40-
const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true });
41-
42-
return oas
43-
.validate(false)
44-
.then(api => {
45-
if (api.swagger) {
46-
return Promise.resolve(chalk.green(`${specPath} is a valid Swagger API definition!`));
47-
}
48-
return Promise.resolve(chalk.green(`${specPath} is a valid OpenAPI API definition!`));
49-
})
50-
.catch(err => {
51-
debug(`raw validation error object: ${JSON.stringify(err)}`);
52-
return Promise.reject(new Error(err.message));
53-
});
40+
const { specType } = await prepareOas(specPath);
41+
return Promise.resolve(chalk.green(`${specPath} is a valid ${specType} API definition!`));
5442
}
5543

5644
if (spec) {
@@ -73,7 +61,7 @@ module.exports = class ValidateCommand {
7361

7462
reject(
7563
new Error(
76-
"We couldn't find an OpenAPI or Swagger definition.\n\nIf you need help creating one run `rdme oas init`!"
64+
"We couldn't find an OpenAPI or Swagger definition.\n\nPlease specify the path to your definition with `rdme validate ./path/to/api/definition`."
7765
)
7866
);
7967
});

src/lib/prepareOas.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const OASNormalize = require('oas-normalize');
2+
const { debug } = require('./logger');
3+
4+
/**
5+
* Normalizes, validates, and (optionally) bundles an OpenAPI definition.
6+
*
7+
* @param {String} path path to spec file
8+
* @param {Boolean} bundle boolean to indicate whether or not to
9+
* return a bundled spec (defaults to false)
10+
*/
11+
module.exports = async function prepare(path, bundle = false) {
12+
debug(`about to normalize spec located at ${path}`);
13+
const oas = new OASNormalize(path, { colorizeErrors: true, enablePaths: true });
14+
debug('spec normalized');
15+
16+
const api = await oas.validate(false).catch(err => {
17+
debug(`raw validation error object: ${JSON.stringify(err)}`);
18+
throw err;
19+
});
20+
debug('👇👇👇👇👇 spec validated! logging spec below 👇👇👇👇👇');
21+
debug(api);
22+
debug('👆👆👆👆👆 finished logging spec 👆👆👆👆👆');
23+
24+
const specType = api.swagger ? 'Swagger' : 'OpenAPI';
25+
debug(`spec type: ${specType}`);
26+
27+
let bundledSpec = '';
28+
29+
if (bundle) {
30+
bundledSpec = await oas
31+
.bundle()
32+
.then(res => {
33+
return JSON.stringify(res);
34+
})
35+
.catch(err => {
36+
debug(`raw bundling error object: ${JSON.stringify(err)}`);
37+
throw err;
38+
});
39+
debug('spec bundled');
40+
}
41+
42+
return { bundledSpec, specType };
43+
};

0 commit comments

Comments
 (0)