diff --git a/__tests__/cmds/swagger.test.js b/__tests__/cmds/openapi.test.js similarity index 87% rename from __tests__/cmds/swagger.test.js rename to __tests__/cmds/openapi.test.js index 10481e5fd..628a8579b 100644 --- a/__tests__/cmds/swagger.test.js +++ b/__tests__/cmds/openapi.test.js @@ -3,6 +3,7 @@ const config = require('config'); const fs = require('fs'); const promptHandler = require('../../src/lib/prompts'); const swagger = require('../../src/cmds/swagger'); +const openapi = require('../../src/cmds/openapi'); const key = 'Xmw4bGctRVIQz7R7dQXqH9nQe5d0SPQs'; const version = '1.0.0'; @@ -13,7 +14,7 @@ const getCommandOutput = () => { return [console.warn.mock.calls.join('\n\n'), console.log.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); }; -describe('rdme swagger', () => { +describe('rdme openapi', () => { const exampleRefLocation = `${config.host}/project/example-project/1.0.1/refs/ex`; beforeAll(() => nock.disableNetConnect()); @@ -53,7 +54,7 @@ describe('rdme swagger', () => { // to break. fs.copyFileSync('./__tests__/__fixtures__/swagger.json', './swagger.json'); - return swagger.run({ key }).then(() => { + return openapi.run({ key }).then(() => { expect(console.log).toHaveBeenCalledTimes(2); const output = getCommandOutput(); @@ -86,7 +87,7 @@ describe('rdme swagger', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }); - return expect(swagger.run({ spec: './__tests__/__fixtures__/swagger.json', key, version })) + return expect(openapi.run({ spec: './__tests__/__fixtures__/swagger.json', key, version })) .rejects.toThrow('The version you specified') .then(() => mock.done()); }); @@ -103,7 +104,7 @@ describe('rdme swagger', () => { .basicAuth({ user: key }) .reply(201, { _id: 1 }, { location: exampleRefLocation }); - return swagger.run({ spec: './__tests__/__fixtures__/swagger.json', key, version }).then(() => { + return openapi.run({ spec: './__tests__/__fixtures__/swagger.json', key, version }).then(() => { expect(console.log).toHaveBeenCalledTimes(1); const output = getCommandOutput(); @@ -134,7 +135,7 @@ describe('rdme swagger', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }); - return expect(swagger.run({ spec: './__tests__/__fixtures__/invalid-swagger.json', key, version })) + return expect(openapi.run({ spec: './__tests__/__fixtures__/invalid-swagger.json', key, version })) .rejects.toThrow('README VALIDATION ERROR "x-samples-languages" must be of type "Array"') .then(() => mock.done()); }); @@ -161,7 +162,7 @@ describe('rdme swagger', () => { .basicAuth({ user: key }) .reply(201, { _id: 1 }, { location: exampleRefLocation }); - return swagger.run({ spec: './__tests__/__fixtures__/swagger.json', key }).then(() => { + return openapi.run({ spec: './__tests__/__fixtures__/swagger.json', key }).then(() => { mock.done(); }); }); @@ -174,7 +175,7 @@ describe('rdme swagger', () => { .basicAuth({ user: key }) .reply(201, { body: '{ id: 1 }' }); - return swagger.run({ spec: './__tests__/__fixtures__/swagger.json', key, id, version }).then(() => { + return openapi.run({ spec: './__tests__/__fixtures__/swagger.json', key, id, version }).then(() => { mock.done(); }); }); @@ -187,7 +188,7 @@ describe('rdme swagger', () => { .basicAuth({ user: key }) .reply(201, { id: 1 }, { location: exampleRefLocation }); - return swagger.run({ spec: './__tests__/__fixtures__/swagger.json', token: `${key}-${id}`, version }).then(() => { + return openapi.run({ spec: './__tests__/__fixtures__/swagger.json', token: `${key}-${id}`, version }).then(() => { expect(console.warn).toHaveBeenCalledTimes(1); expect(console.log).toHaveBeenCalledTimes(1); @@ -200,7 +201,7 @@ describe('rdme swagger', () => { }); it('should error if no api key provided', async () => { - await expect(swagger.run({ spec: './__tests__/__fixtures__/swagger.json' })).rejects.toThrow( + await expect(openapi.run({ spec: './__tests__/__fixtures__/swagger.json' })).rejects.toThrow( 'No project API key provided. Please use `--key`.' ); }); @@ -211,7 +212,7 @@ describe('rdme swagger', () => { .basicAuth({ user: key }) .reply(200, { version: '1.0.0' }); - await expect(swagger.run({ key, version })).rejects.toThrow(/We couldn't find a Swagger or OpenAPI file./); + await expect(openapi.run({ key, version })).rejects.toThrow(/We couldn't find a Swagger or OpenAPI file./); mock.done(); }); @@ -219,8 +220,19 @@ describe('rdme swagger', () => { it('should throw an error if file is invalid', async () => { const id = '5aa0409b7cf527a93bfb44df'; - await expect(swagger.run({ spec: './__tests__/__fixtures__/invalid-oas.json', key, id, version })).rejects.toThrow( + await expect(openapi.run({ spec: './__tests__/__fixtures__/invalid-oas.json', key, id, version })).rejects.toThrow( 'Token "Error" does not exist.' ); }); }); + +describe('rdme swagger', () => { + it('should run `rdme openapi`', async () => { + const id = '5aa0409b7cf527a93bfb44df'; + + await expect(swagger.run({ spec: '', key, id, version })).rejects.toThrow( + "We couldn't find a Swagger or OpenAPI file.\n\n" + + 'Run `rdme openapi ./path/to/file` to upload an existing file or `rdme oas init` to create a fresh one!' + ); + }); +}); diff --git a/__tests__/index.test.js b/__tests__/index.test.js index d5f992fce..53842d1cf 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -88,11 +88,7 @@ describe('cli', () => { }); }); - it('should not show related commands on commands that have none', () => { - return cli(['swagger', '--help']).then(output => { - expect(output).not.toContain('Related commands'); - }); - }); + it.todo('should not show related commands on commands that have none'); }); describe('subcommands', () => { diff --git a/src/cmds/openapi.js b/src/cmds/openapi.js new file mode 100644 index 000000000..69c4824dc --- /dev/null +++ b/src/cmds/openapi.js @@ -0,0 +1,208 @@ +require('colors'); +const request = require('request-promise-native'); +const fs = require('fs'); +const path = require('path'); +const config = require('config'); +const { prompt } = require('enquirer'); +const OASNormalize = require('oas-normalize'); +const promptOpts = require('../lib/prompts'); +const APIError = require('../lib/apiError'); + +exports.command = 'openapi'; +exports.usage = 'openapi [file] [options]'; +exports.description = 'Upload, or sync, your Swagger/OpenAPI file to ReadMe.'; +exports.category = 'apis'; +exports.position = 1; + +exports.hiddenArgs = ['token', 'spec']; +exports.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'id', + type: String, + description: `Unique identifier for your specification. Use this if you're resyncing an existing specification`, + }, + { + name: 'token', + type: String, + description: 'Project token. Deprecated, please use `--key` instead', + }, + { + name: 'version', + type: String, + description: 'Project version', + }, + { + name: 'spec', + type: String, + defaultOption: true, + }, +]; + +exports.run = async function (opts) { + const { spec, version } = opts; + let { key, id } = opts; + let selectedVersion; + let isUpdate; + + if (!key && opts.token) { + console.warn('Using `rdme` with --token has been deprecated. Please use `--key` and `--id` instead.'); + + [key, id] = opts.token.split('-'); + } + + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } + + async function callApi(specPath, versionCleaned) { + // @todo Tailor messaging to what is actually being handled here. If the user is uploading an OpenAPI file, never mention that they uploaded/updated a Swagger file. + + function success(data) { + const message = !isUpdate + ? "You've successfully uploaded a new Swagger file to your ReadMe project!" + : "You've successfully updated a Swagger file on your ReadMe project!"; + + console.log( + [ + message, + '', + `\t${`${data.headers.location}`.green}`, + '', + 'To update your Swagger or OpenAPI file, run the following:', + '', + // eslint-disable-next-line no-underscore-dangle + `\trdme openapi FILE --key=${key} --id=${JSON.parse(data.body)._id}`.green, + ].join('\n') + ); + } + + function error(err) { + try { + const parsedError = JSON.parse(err.error); + return Promise.reject(new APIError(parsedError)); + } catch (e) { + return Promise.reject(new Error('There was an error uploading!')); + } + } + + const options = { + formData: { + spec: fs.createReadStream(path.resolve(process.cwd(), specPath)), + }, + headers: { + 'x-readme-version': versionCleaned, + 'x-readme-source': 'cli', + }, + auth: { user: key }, + resolveWithFullResponse: true, + }; + + function createSpec() { + return request.post(`${config.host}/api/v1/api-specification`, options).then(success, error); + } + + function updateSpec(specId) { + isUpdate = true; + + return request.put(`${config.host}/api/v1/api-specification/${specId}`, options).then(success, error); + } + + if (spec) { + const oas = new OASNormalize(spec, { enablePaths: true }); + await oas.validate().catch(err => { + return Promise.reject(err); + }); + } + + /* + Create a new OAS file in Readme: + - Enter flow if user does not pass an id as cli arg + - Check to see if any existing files exist with a specific version + - If none exist, default to creating a new instance of a spec + - If found, prompt user to either create a new spec or update an existing one + */ + + if (!id) { + const apiSettings = await request.get(`${config.host}/api/v1/api-specification`, { + headers: { + 'x-readme-version': versionCleaned, + }, + json: true, + auth: { user: key }, + }); + + if (!apiSettings.length) return createSpec(); + + const { option, specId } = await prompt(promptOpts.createOasPrompt(apiSettings)); + return option === 'create' ? createSpec() : updateSpec(specId); + } + + /* + Update an existing OAS file in Readme: + - Enter flow if user passes an id as cli arg + */ + return updateSpec(id); + } + + async function getSwaggerVersion(versionFlag) { + const options = { json: {}, auth: { user: key } }; + + try { + if (versionFlag) { + options.json.version = versionFlag; + const foundVersion = await request.get(`${config.host}/api/v1/version/${versionFlag}`, options); + + return foundVersion.version; + } + + const versionList = await request.get(`${config.host}/api/v1/version`, options); + const { option, versionSelection, newVersion } = await prompt( + promptOpts.generatePrompts(versionList, versionFlag) + ); + + if (option === 'update') return versionSelection; + + options.json = { from: versionList[0].version, version: newVersion, is_stable: false }; + await request.post(`${config.host}/api/v1/version`, options); + + return newVersion; + } catch (err) { + return Promise.reject(new APIError(err)); + } + } + + if (!id) { + selectedVersion = await getSwaggerVersion(version).catch(e => { + return Promise.reject(e); + }); + } + + if (spec) { + return callApi(spec, selectedVersion); + } + + // If the user didn't supply a specification, let's try to locate what they've got, and upload + // that. If they don't have any, let's let the user know how they can get one going. + return new Promise((resolve, reject) => { + ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { + if (!fs.existsSync(file)) { + return; + } + + console.log(`We found ${file} and are attempting to upload it.`.yellow); + resolve(callApi(file, selectedVersion)); + }); + + reject( + new Error( + "We couldn't find a Swagger or OpenAPI file.\n\n" + + 'Run `rdme openapi ./path/to/file` to upload an existing file or `rdme oas init` to create a fresh one!' + ) + ); + }); +}; diff --git a/src/cmds/swagger.js b/src/cmds/swagger.js index c318b2582..0db96fd38 100644 --- a/src/cmds/swagger.js +++ b/src/cmds/swagger.js @@ -1,18 +1,11 @@ require('colors'); -const request = require('request-promise-native'); -const fs = require('fs'); -const path = require('path'); -const config = require('config'); -const { prompt } = require('enquirer'); -const OASNormalize = require('oas-normalize'); -const promptOpts = require('../lib/prompts'); -const APIError = require('../lib/apiError'); +const openapi = require('./openapi'); exports.command = 'swagger'; exports.usage = 'swagger [file] [options]'; -exports.description = 'Upload, or sync, your Swagger/OpenAPI file to ReadMe.'; +exports.description = 'Alias for `rdme openapi`.'; exports.category = 'apis'; -exports.position = 1; +exports.position = 2; exports.hiddenArgs = ['token', 'spec']; exports.args = [ @@ -44,165 +37,5 @@ exports.args = [ ]; exports.run = async function (opts) { - const { spec, version } = opts; - let { key, id } = opts; - let selectedVersion; - let isUpdate; - - if (!key && opts.token) { - console.warn('Using `rdme` with --token has been deprecated. Please use `--key` and `--id` instead.'); - - [key, id] = opts.token.split('-'); - } - - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); - } - - async function callApi(specPath, versionCleaned) { - // @todo Tailor messaging to what is actually being handled here. If the user is uploading an OpenAPI file, never mention that they uploaded/updated a Swagger file. - - function success(data) { - const message = !isUpdate - ? "You've successfully uploaded a new Swagger file to your ReadMe project!" - : "You've successfully updated a Swagger file on your ReadMe project!"; - - console.log( - [ - message, - '', - `\t${`${data.headers.location}`.green}`, - '', - 'To update your Swagger or OpenAPI file, run the following:', - '', - // eslint-disable-next-line no-underscore-dangle - `\trdme swagger FILE --key=${key} --id=${JSON.parse(data.body)._id}`.green, - ].join('\n') - ); - } - - function error(err) { - try { - const parsedError = JSON.parse(err.error); - return Promise.reject(new APIError(parsedError)); - } catch (e) { - return Promise.reject(new Error('There was an error uploading!')); - } - } - - const options = { - formData: { - spec: fs.createReadStream(path.resolve(process.cwd(), specPath)), - }, - headers: { - 'x-readme-version': versionCleaned, - 'x-readme-source': 'cli', - }, - auth: { user: key }, - resolveWithFullResponse: true, - }; - - function createSpec() { - return request.post(`${config.host}/api/v1/api-specification`, options).then(success, error); - } - - function updateSpec(specId) { - isUpdate = true; - - return request.put(`${config.host}/api/v1/api-specification/${specId}`, options).then(success, error); - } - - if (spec) { - const oas = new OASNormalize(spec, { enablePaths: true }); - await oas.validate().catch(err => { - return Promise.reject(err); - }); - } - - /* - Create a new OAS file in Readme: - - Enter flow if user does not pass an id as cli arg - - Check to see if any existing files exist with a specific version - - If none exist, default to creating a new instance of a spec - - If found, prompt user to either create a new spec or update an existing one - */ - - if (!id) { - const apiSettings = await request.get(`${config.host}/api/v1/api-specification`, { - headers: { - 'x-readme-version': versionCleaned, - }, - json: true, - auth: { user: key }, - }); - - if (!apiSettings.length) return createSpec(); - - const { option, specId } = await prompt(promptOpts.createOasPrompt(apiSettings)); - return option === 'create' ? createSpec() : updateSpec(specId); - } - - /* - Update an existing OAS file in Readme: - - Enter flow if user passes an id as cli arg - */ - return updateSpec(id); - } - - async function getSwaggerVersion(versionFlag) { - const options = { json: {}, auth: { user: key } }; - - try { - if (versionFlag) { - options.json.version = versionFlag; - const foundVersion = await request.get(`${config.host}/api/v1/version/${versionFlag}`, options); - - return foundVersion.version; - } - - const versionList = await request.get(`${config.host}/api/v1/version`, options); - const { option, versionSelection, newVersion } = await prompt( - promptOpts.generatePrompts(versionList, versionFlag) - ); - - if (option === 'update') return versionSelection; - - options.json = { from: versionList[0].version, version: newVersion, is_stable: false }; - await request.post(`${config.host}/api/v1/version`, options); - - return newVersion; - } catch (err) { - return Promise.reject(new APIError(err)); - } - } - - if (!id) { - selectedVersion = await getSwaggerVersion(version).catch(e => { - return Promise.reject(e); - }); - } - - if (spec) { - return callApi(spec, selectedVersion); - } - - // If the user didn't supply a specification, let's try to locate what they've got, and upload - // that. If they don't have any, let's let the user know how they can get one going. - return new Promise((resolve, reject) => { - ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { - if (!fs.existsSync(file)) { - return; - } - - console.log(`We found ${file} and are attempting to upload it.`.yellow); - resolve(callApi(file, selectedVersion)); - }); - - reject( - new Error( - "We couldn't find a Swagger or OpenAPI file.\n\n" + - 'Run `rdme swagger ./path/to/file` to upload an existing file or `rdme oas init` to create a fresh one!' - ) - ); - }); + return openapi.run(opts); };