Skip to content

Commit

Permalink
refactor(openapi): DRY and reorg (#556)
Browse files Browse the repository at this point in the history
  • Loading branch information
kanadgupta authored Aug 1, 2022
1 parent cf02b5c commit 9ec4ad6
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 188 deletions.
269 changes: 121 additions & 148 deletions src/cmds/openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -83,168 +82,142 @@ 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=<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('<title>Application Error</title>') || // Heroku error
err.includes('520: Web server is returning an unknown error</title>')) // 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=<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('<title>Application Error</title>') || // Heroku error
err.includes('520: Web server is returning an unknown error</title>')) // 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(
'[email protected]'
)}.`
"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(
'[email protected]'
)}.`
);
});
}

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
- 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
*/

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);
}
};
32 changes: 2 additions & 30 deletions src/cmds/validate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const chalk = require('chalk');
const fs = require('fs');
const { debug } = require('../lib/logger');
const prepareOas = require('../lib/prepareOas');

Expand Down Expand Up @@ -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!`));
}
};
Loading

0 comments on commit 9ec4ad6

Please sign in to comment.