diff --git a/src/cmds/openapi.js b/src/cmds/openapi.js index 6bb342a6d..aff820ca2 100644 --- a/src/cmds/openapi.js +++ b/src/cmds/openapi.js @@ -2,7 +2,6 @@ const APIError = require('../lib/apiError'); const chalk = require('chalk'); const { cleanHeaders } = require('../lib/fetch'); const config = require('config'); -const fs = require('fs'); const { debug, oraOptions } = require('../lib/logger'); const fetch = require('../lib/fetch'); const { handleRes } = require('../lib/fetch'); @@ -83,96 +82,103 @@ module.exports = class OpenAPICommand { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } - async function callApi(specPath, versionCleaned) { - const { bundledSpec, specType } = await prepareOas(specPath, true); - - async function success(data) { - const message = !isUpdate - ? `You've successfully uploaded a new ${specType} file to your ReadMe project!` - : `You've successfully updated an existing ${specType} file on your ReadMe project!`; - - debug(`successful ${data.status} response`); - const body = await data.json(); - debug(`successful response payload: ${JSON.stringify(body)}`); - - return Promise.resolve( - [ - message, - '', - `\t${chalk.green(`${data.headers.get('location')}`)}`, - '', - `To update your ${specType} definition, run the following:`, - '', - // eslint-disable-next-line no-underscore-dangle - `\t${chalk.green(`rdme openapi ${specPath} --key= --id=${body._id}`)}`, - ].join('\n') - ); - } - - async function error(res) { - return handleRes(res).catch(err => { - // If we receive an APIError, no changes needed! Throw it as is. - if (err instanceof APIError) { - throw err; - } - // If we receive certain text responses, it's likely a 5xx error from our server. - if ( - typeof err === 'string' && - (err.includes('Application Error') || // Heroku error - err.includes('520: Web server is returning an unknown error')) // Cloudflare error - ) { - throw new Error( - "We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks." - ); - } - // As a fallback, we throw a more generic error. + if (!id) { + selectedVersion = await getProjectVersion(version, key, true); + } + + debug(`selectedVersion: ${selectedVersion}`); + + // Reason we're hardcoding in command here is because `swagger` command + // relies on this and we don't want to use `swagger` in this function + const { bundledSpec, specPath, specType } = await prepareOas(spec, 'openapi'); + + async function success(data) { + const message = !isUpdate + ? `You've successfully uploaded a new ${specType} file to your ReadMe project!` + : `You've successfully updated an existing ${specType} file on your ReadMe project!`; + + debug(`successful ${data.status} response`); + const body = await data.json(); + debug(`successful response payload: ${JSON.stringify(body)}`); + + return Promise.resolve( + [ + message, + '', + `\t${chalk.green(`${data.headers.get('location')}`)}`, + '', + `To update your ${specType} definition, run the following:`, + '', + // eslint-disable-next-line no-underscore-dangle + `\t${chalk.green(`rdme openapi ${specPath} --key= --id=${body._id}`)}`, + ].join('\n') + ); + } + + async function error(res) { + return handleRes(res).catch(err => { + // If we receive an APIError, no changes needed! Throw it as is. + if (err instanceof APIError) { + throw err; + } + // If we receive certain text responses, it's likely a 5xx error from our server. + if ( + typeof err === 'string' && + (err.includes('Application Error') || // Heroku error + err.includes('520: Web server is returning an unknown error')) // Cloudflare error + ) { throw new Error( - `Yikes, something went wrong! Please try uploading your spec again and if the problem persists, get in touch with our support team at ${chalk.underline( - 'support@readme.io' - )}.` + "We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks." ); - }); - } + } + // As a fallback, we throw a more generic error. + throw new Error( + `Yikes, something went wrong! Please try uploading your spec again and if the problem persists, get in touch with our support team at ${chalk.underline( + 'support@readme.io' + )}.` + ); + }); + } - const registryUUID = await streamSpecToRegistry(bundledSpec); + const registryUUID = await streamSpecToRegistry(bundledSpec); + + const options = { + headers: cleanHeaders(key, { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'x-readme-version': selectedVersion, + }), + body: JSON.stringify({ registryUUID }), + }; + + function createSpec() { + options.method = 'post'; + spinner.start('Creating your API docs in ReadMe...'); + return fetch(`${config.get('host')}/api/v1/api-specification`, options).then(res => { + if (res.ok) { + spinner.succeed(`${spinner.text} done! 🦉`); + return success(res); + } + spinner.fail(); + return error(res); + }); + } - const options = { - headers: cleanHeaders(key, { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'x-readme-version': versionCleaned, - }), - body: JSON.stringify({ registryUUID }), - }; - - function createSpec() { - options.method = 'post'; - spinner.start('Creating your API docs in ReadMe...'); - return fetch(`${config.get('host')}/api/v1/api-specification`, options).then(res => { - if (res.ok) { - spinner.succeed(`${spinner.text} done! 🦉`); - return success(res); - } - spinner.fail(); - return error(res); - }); - } - - function updateSpec(specId) { - isUpdate = true; - options.method = 'put'; - spinner.start('Updating your API docs in ReadMe...'); - return fetch(`${config.get('host')}/api/v1/api-specification/${specId}`, options).then(res => { - if (res.ok) { - spinner.succeed(`${spinner.text} done! 🦉`); - return success(res); - } - spinner.fail(); - return error(res); - }); - } - - /* + function updateSpec(specId) { + isUpdate = true; + options.method = 'put'; + spinner.start('Updating your API docs in ReadMe...'); + return fetch(`${config.get('host')}/api/v1/api-specification/${specId}`, options).then(res => { + if (res.ok) { + spinner.succeed(`${spinner.text} done! 🦉`); + return success(res); + } + spinner.fail(); + return error(res); + }); + } + + /* 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 @@ -180,71 +186,38 @@ module.exports = class OpenAPICommand { - If found, prompt user to either create a new spec or update an existing one */ - function getSpecs(url) { - return fetch(`${config.get('host')}${url}`, { - method: 'get', - headers: cleanHeaders(key, { - 'x-readme-version': versionCleaned, - }), - }); - } - - if (!id) { - debug('no id parameter, retrieving list of API specs'); - const apiSettings = await getSpecs('/api/v1/api-specification'); - - const totalPages = Math.ceil(apiSettings.headers.get('x-total-count') / 10); - const parsedDocs = parse(apiSettings.headers.get('link')); - debug(`total pages: ${totalPages}`); - debug(`pagination result: ${JSON.stringify(parsedDocs)}`); - - const apiSettingsBody = await apiSettings.json(); - debug(`api settings list response payload: ${JSON.stringify(apiSettingsBody)}`); - if (!apiSettingsBody.length) return createSpec(); - - const { option } = await prompt(promptOpts.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs)); - debug(`selection result: ${option}`); - if (!option) return null; - return option === 'create' ? createSpec() : updateSpec(option); - } - - /* - Update an existing OAS file in Readme: - - Enter flow if user passes an id as cli arg - */ - return updateSpec(id); + function getSpecs(url) { + return fetch(`${config.get('host')}${url}`, { + method: 'get', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + }), + }); } if (!id) { - selectedVersion = await getProjectVersion(version, key, true); - } - - debug(`selectedVersion: ${selectedVersion}`); - - if (spec) { - return callApi(spec, selectedVersion); + debug('no id parameter, retrieving list of API specs'); + const apiSettings = await getSpecs('/api/v1/api-specification'); + + const totalPages = Math.ceil(apiSettings.headers.get('x-total-count') / 10); + const parsedDocs = parse(apiSettings.headers.get('link')); + debug(`total pages: ${totalPages}`); + debug(`pagination result: ${JSON.stringify(parsedDocs)}`); + + const apiSettingsBody = await apiSettings.json(); + debug(`api settings list response payload: ${JSON.stringify(apiSettingsBody)}`); + if (!apiSettingsBody.length) return createSpec(); + + const { option } = await prompt(promptOpts.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs)); + debug(`selection result: ${option}`); + if (!option) return null; + return option === 'create' ? createSpec() : updateSpec(option); } - // If the user didn't supply an API 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', 'swagger.yml', 'openapi.json', 'openapi.yaml', 'openapi.yml'].forEach(file => { - debug(`looking for definition with filename: ${file}`); - if (!fs.existsSync(file)) { - debug(`${file} not found`); - return; - } - - console.info(chalk.yellow(`We found ${file} and are attempting to upload it.`)); - resolve(callApi(file, selectedVersion)); - }); - - reject( - new Error( - "We couldn't find an OpenAPI or Swagger definition.\n\n" + - 'Please specify the path to your definition with `rdme openapi ./path/to/api/definition`.' - ) - ); - }); + /* + Update an existing OAS file in Readme: + - Enter flow if user passes an id as cli arg + */ + return updateSpec(id); } }; diff --git a/src/cmds/validate.js b/src/cmds/validate.js index 19b918318..0d3a0b07e 100644 --- a/src/cmds/validate.js +++ b/src/cmds/validate.js @@ -1,5 +1,4 @@ const chalk = require('chalk'); -const fs = require('fs'); const { debug } = require('../lib/logger'); const prepareOas = require('../lib/prepareOas'); @@ -36,34 +35,7 @@ module.exports = class ValidateCommand { debug(`command: ${this.command}`); debug(`opts: ${JSON.stringify(opts)}`); - async function validateSpec(specPath) { - const { specType } = await prepareOas(specPath); - return Promise.resolve(chalk.green(`${specPath} is a valid ${specType} API definition!`)); - } - - if (spec) { - return validateSpec(spec); - } - - // If the user didn't supply an API specification, let's try to locate what they've got, and validate 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', 'swagger.yml', 'openapi.json', 'openapi.yaml', 'openapi.yml'].forEach(file => { - debug(`looking for definition with filename: ${file}`); - if (!fs.existsSync(file)) { - debug(`${file} not found`); - return; - } - - console.info(chalk.yellow(`We found ${file} and are attempting to validate it.`)); - resolve(validateSpec(file)); - }); - - reject( - new Error( - "We couldn't find an OpenAPI or Swagger definition.\n\nPlease specify the path to your definition with `rdme validate ./path/to/api/definition`." - ) - ); - }); + const { specPath, specType } = await prepareOas(spec, this.command); + return Promise.resolve(chalk.green(`${specPath} is a valid ${specType} API definition!`)); } }; diff --git a/src/lib/prepareOas.js b/src/lib/prepareOas.js index 51e6c4fd6..f03a5be67 100644 --- a/src/lib/prepareOas.js +++ b/src/lib/prepareOas.js @@ -1,19 +1,50 @@ -const { debug, oraOptions } = require('./logger'); +const chalk = require('chalk'); +const fs = require('fs'); const OASNormalize = require('oas-normalize'); const ora = require('ora'); +const { debug, oraOptions } = require('./logger'); + /** * Normalizes, validates, and (optionally) bundles an OpenAPI definition. * - * @param {String} path path to spec file - * @param {Boolean} bundle boolean to indicate whether or not to - * return a bundled spec (defaults to false) + * @param {String} path path to spec file. if this is missing, the current directory is searched + * for certain file names + * @param {('openapi'|'validate')} command string to distinguish if it's being run in + * an 'openapi' or 'validate' context */ -module.exports = async function prepare(path, bundle = false) { - const spinner = ora({ text: `Validating API definition located at ${path}...`, ...oraOptions() }).start(); +module.exports = async function prepare(path, command) { + let specPath = path; + + if (!specPath) { + // If the user didn't supply an API specification, let's try to locate what they've got, and validate that. If they + // don't have any, let's let the user know how they can get one going. + specPath = await new Promise((resolve, reject) => { + ['swagger.json', 'swagger.yaml', 'swagger.yml', 'openapi.json', 'openapi.yaml', 'openapi.yml'].forEach(file => { + debug(`looking for definition with filename: ${file}`); + if (!fs.existsSync(file)) { + debug(`${file} not found`); + return; + } + + console.info( + chalk.yellow(`We found ${file} and are attempting to ${command === 'openapi' ? 'upload' : 'validate'} it.`) + ); + resolve(file); + }); + + reject( + new Error( + `We couldn't find an OpenAPI or Swagger definition.\n\nPlease specify the path to your definition with \`rdme ${command} ./path/to/api/definition\`.` + ) + ); + }); + } + + const spinner = ora({ text: `Validating API definition located at ${specPath}...`, ...oraOptions() }).start(); - debug(`about to normalize spec located at ${path}`); - const oas = new OASNormalize(path, { colorizeErrors: true, enablePaths: true }); + debug(`about to normalize spec located at ${specPath}`); + const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); debug('spec normalized'); const api = await oas.validate(false).catch(err => { @@ -32,7 +63,7 @@ module.exports = async function prepare(path, bundle = false) { let bundledSpec = ''; - if (bundle) { + if (command === 'openapi') { bundledSpec = await oas.bundle().then(res => { return JSON.stringify(res); }); @@ -40,5 +71,5 @@ module.exports = async function prepare(path, bundle = false) { debug('spec bundled'); } - return { bundledSpec, specType }; + return { bundledSpec, specPath, specType }; };