UNPKG

rdme

Version:

ReadMe's official CLI and GitHub Action.

180 lines (179 loc) 7.83 kB
import chalk from 'chalk'; import OASNormalize from 'oas-normalize'; import { getAPIDefinitionType } from 'oas-normalize/lib/utils'; import ora from 'ora'; import isCI from './isCI.js'; import { debug, info, oraOptions } from './logger.js'; import promptTerminal from './promptWrapper.js'; import readdirRecursive from './readdirRecursive.js'; function truthy(value) { return !!value; } function capitalizeSpecType(type) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (type === 'openapi' ? 'OpenAPI' : type.charAt(0).toUpperCase() + type.slice(1)); } /** * Normalizes, validates, and (optionally) bundles an OpenAPI definition. */ export default async function prepareOas( /** * Path to a spec file. If this is missing, the current directory is searched for * certain file names. */ path, /** * The command context in which this is being run within (uploading a spec, * validation, or reducing one). */ command, opts = {}) { let specPath = path; if (!specPath) { /** * Scans working directory for a potential OpenAPI or Swagger file. * Any files in the `.git` directory or defined in a top-level `.gitignore` file * are skipped. * * A "potential OpenAPI or Swagger file" is defined as a YAML or JSON file * that has an `openapi` or `swagger` property defined at the top-level. * * If multiple potential files are found, the user must select a single file. * * An error is thrown in the following cases: * - if in a CI environment and multiple files are found * - no files are found */ const fileFindingSpinner = ora({ text: 'Looking for API definitions...', ...oraOptions() }).start(); const action = command.replace('openapi ', ''); const jsonAndYamlFiles = readdirRecursive('.', true).filter(file => file.toLowerCase().endsWith('.json') || file.toLowerCase().endsWith('.yaml') || file.toLowerCase().endsWith('.yml')); debug(`number of JSON or YAML files found: ${jsonAndYamlFiles.length}`); const possibleSpecFiles = (await Promise.all(jsonAndYamlFiles.map(file => { debug(`attempting to oas-normalize ${file}`); const oas = new OASNormalize(file, { enablePaths: true }); return oas .version() .then(({ specification, version }) => { debug(`specification type for ${file}: ${specification}`); debug(`version for ${file}: ${version}`); return ['openapi', 'swagger', 'postman'].includes(specification) ? { filePath: file, specType: capitalizeSpecType(specification), version } : null; }) .catch(e => { debug(`error extracting API definition specification version for ${file}: ${e.message}`); return null; }); }))).filter(truthy); debug(`number of possible OpenAPI/Swagger files found: ${possibleSpecFiles.length}`); if (!possibleSpecFiles.length) { fileFindingSpinner.fail(); throw 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\`.`); } specPath = possibleSpecFiles[0].filePath; if (possibleSpecFiles.length === 1) { fileFindingSpinner.stop(); info(chalk.yellow(`We found ${specPath} and are attempting to ${action} it.`)); } else if (possibleSpecFiles.length > 1) { if (isCI()) { fileFindingSpinner.fail(); throw new Error('Multiple API definitions found in current directory. Please specify file.'); } fileFindingSpinner.stop(); const selection = await promptTerminal({ name: 'file', message: `Multiple potential API definitions found! Which one would you like to ${action}?`, type: 'select', choices: possibleSpecFiles.map(file => ({ title: file.filePath, value: file.filePath, description: `${file.specType} ${file.version}`, })), }); specPath = selection.file; } } const spinner = ora({ text: `Validating the API definition located at ${specPath}...`, ...oraOptions() }).start(); debug(`about to normalize spec located at ${specPath}`); const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); debug('spec normalized'); // We're retrieving the original specification type here instead of after validation because if // they give us a Postman collection we should tell them that we handled a Postman collection, not // an OpenAPI definition (eventhough we'll actually convert it to OpenAPI under the hood). // // And though `.validate()` will run `.load()` itself running `.load()` here will not have any // performance implications as `oas-normalizes` caches the result of `.load()` the first time you // run it. const { specType, definitionVersion } = await oas .load() .then(async (schema) => { const type = getAPIDefinitionType(schema); return { specType: capitalizeSpecType(type), definitionVersion: await oas.version(), }; }) .catch((err) => { spinner.fail(); debug(`raw oas load error object: ${JSON.stringify(err)}`); throw err; }); let api; await oas.validate().catch((err) => { spinner.fail(); debug(`raw validation error object: ${JSON.stringify(err)}`); throw err; }); // If we were supplied a Postman collection this will **always** convert it to OpenAPI 3.0. debug('converting the spec to OpenAPI 3.0 (if necessary)'); api = await oas.convert().catch((err) => { spinner.fail(); debug(`raw openapi conversion error object: ${JSON.stringify(err)}`); throw err; }); spinner.stop(); debug('👇👇👇👇👇 spec validated! logging spec below 👇👇👇👇👇'); debug(api); debug('👆👆👆👆👆 finished logging spec 👆👆👆👆👆'); debug(`spec type: ${specType}`); if (opts.title) { debug(`renaming title field to ${opts.title}`); api.info.title = opts.title; } const specFileType = oas.type; // No need to optional chain here since `info.version` is required to pass validation const specVersion = api.info.version; debug(`version in spec: ${specVersion}`); const commandsThatBundle = [ 'openapi inspect', 'openapi reduce', 'openapi resolve', 'openapi upload', ]; if (commandsThatBundle.includes(command)) { api = await oas.bundle(); debug('spec bundled'); } return { preparedSpec: JSON.stringify(api), /** A string indicating whether the spec file is a local path, a URL, etc. */ specFileType, /** The path/URL to the spec file */ specPath, /** A string indicating whether the spec file is OpenAPI, Swagger, etc. */ specType, /** * The `info.version` field, extracted from the normalized spec. * This is **not** the OpenAPI version (e.g., 3.1, 3.0), * this is a user input that we use to specify the version in ReadMe * (if they use the `useSpecVersion` flag) */ specVersion, /** * This is the `openapi`, `swagger`, or `postman` specification version of their API definition. */ definitionVersion, }; }