Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions documentation/commands/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,15 @@ Resolves circular and recursive references in OpenAPI by replacing them with obj

```
USAGE
$ rdme openapi resolve [SPEC] [--out <value>] [--workingDirectory <value>]
$ rdme openapi resolve [SPEC] [--out <value>] [--title <value>] [--workingDirectory <value>]

ARGUMENTS
SPEC A path to your API definition β€” either a local file path or a URL. If your working directory and all
subdirectories contain a single OpenAPI file, you can omit the path.

FLAGS
--out=<value> Output file path to write resolved file to
--title=<value> An override value for the `info.title` field in the API definition
--workingDirectory=<value> Working directory (for usage with relative external references)

DESCRIPTION
Expand Down Expand Up @@ -184,7 +185,7 @@ Upload (or re-upload) your API definition to ReadMe.

```
USAGE
$ rdme openapi upload [SPEC] --key <value> [--slug <value>] [--useSpecVersion | --branch <value>]
$ rdme openapi upload [SPEC] --key <value> [--slug <value>] [--title <value>] [--useSpecVersion | --branch <value>]

ARGUMENTS
SPEC A path to your API definition β€” either a local file path or a URL. If your working directory and all
Expand All @@ -194,6 +195,7 @@ FLAGS
--key=<value> (required) ReadMe project API key
--branch=<value> [default: stable] ReadMe project version
--slug=<value> Override the slug (i.e., the unique identifier) for your API definition.
--title=<value> An override value for the `info.title` field in the API definition
--useSpecVersion Use the OpenAPI `info.version` field for your ReadMe project version

DESCRIPTION
Expand Down
9 changes: 4 additions & 5 deletions src/commands/openapi/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import promptTerminal from '../../lib/promptWrapper.js';
import { validateFilePath } from '../../lib/validatePromptInput.js';

export default class OpenAPIConvertCommand extends BaseCommand<typeof OpenAPIConvertCommand> {
id = 'openapi convert' as const;

static summary = 'Converts an API definition to OpenAPI and bundles any external references.';

static description =
Expand Down Expand Up @@ -43,18 +45,15 @@ export default class OpenAPIConvertCommand extends BaseCommand<typeof OpenAPICon
];

async run() {
const { spec } = this.args;
const { out, title, workingDirectory } = this.flags;
const { out, workingDirectory } = this.flags;

if (workingDirectory) {
const previousWorkingDirectory = process.cwd();
process.chdir(workingDirectory);
this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
}

const { preparedSpec, specPath, specType } = await prepareOas(spec, 'openapi convert', {
title,
});
const { preparedSpec, specPath, specType } = await prepareOas.call(this);
const parsedPreparedSpec: OASDocument = JSON.parse(preparedSpec);

if (specType === 'OpenAPI') {
Expand Down
4 changes: 3 additions & 1 deletion src/commands/openapi/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ function buildFullReport(analysis: Analysis, definitionVersion: string, tableBor
}

export default class OpenAPIInspectCommand extends BaseCommand<typeof OpenAPIInspectCommand> {
id = 'openapi inspect' as const;

static summary = 'Analyze an OpenAPI/Swagger definition for various OpenAPI and ReadMe feature usage.';

static description =
Expand Down Expand Up @@ -251,7 +253,7 @@ export default class OpenAPIInspectCommand extends BaseCommand<typeof OpenAPIIns
this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
}

const { preparedSpec, definitionVersion } = await prepareOas(spec, 'openapi inspect');
const { preparedSpec, definitionVersion } = await prepareOas.call(this);
const parsedPreparedSpec: OASDocument = JSON.parse(preparedSpec);

const spinner = ora({ ...oraOptions() });
Expand Down
7 changes: 4 additions & 3 deletions src/commands/openapi/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import promptTerminal from '../../lib/promptWrapper.js';
import { validateFilePath } from '../../lib/validatePromptInput.js';

export default class OpenAPIReduceCommand extends BaseCommand<typeof OpenAPIReduceCommand> {
id = 'openapi reduce' as const;

static summary = 'Reduce an OpenAPI definition into a smaller subset.';

static description =
Expand Down Expand Up @@ -58,17 +60,16 @@ export default class OpenAPIReduceCommand extends BaseCommand<typeof OpenAPIRedu
];

async run() {
const { spec } = this.args;
const opts = this.flags;
const { title, workingDirectory } = opts;
const { workingDirectory } = opts;

if (workingDirectory) {
const previousWorkingDirectory = process.cwd();
process.chdir(workingDirectory);
this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
}

const { preparedSpec, specPath, specType } = await prepareOas(spec, 'openapi reduce', { title });
const { preparedSpec, specPath, specType } = await prepareOas.call(this);
const parsedPreparedSpec: OASDocument = JSON.parse(preparedSpec);

if (specType !== 'OpenAPI') {
Expand Down
7 changes: 5 additions & 2 deletions src/commands/openapi/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import prompts from 'prompts';

import analyzeOas from '../../lib/analyzeOas.js';
import BaseCommand from '../../lib/baseCommand.js';
import { specArg, workingDirectoryFlag } from '../../lib/flags.js';
import { specArg, titleFlag, workingDirectoryFlag } from '../../lib/flags.js';
import { oraOptions } from '../../lib/logger.js';
import prepareOas from '../../lib/prepareOas.js';
import promptTerminal from '../../lib/promptWrapper.js';
Expand All @@ -22,6 +22,8 @@ type Schema = OpenAPIV31.ReferenceObject | OpenAPIV31.SchemaObject;
type SchemaCollection = Record<string, Schema>;

export default class OpenAPIResolveCommand extends BaseCommand<typeof OpenAPIResolveCommand> {
id = 'openapi resolve' as const;

static summary = 'Resolves circular and recursive references in OpenAPI by replacing them with object schemas.';

static description =
Expand Down Expand Up @@ -50,6 +52,7 @@ export default class OpenAPIResolveCommand extends BaseCommand<typeof OpenAPIRes

static flags = {
out: Flags.string({ description: 'Output file path to write resolved file to' }),
title: titleFlag,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the resolve command doing with this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for consistency i've mostly had it where every command that ends up outputting a new OAS file (i.e., pretty much every command besides inspect and validate) could benefit from this flag since it allows people to set custom info.title fields on the output

workingDirectory: workingDirectoryFlag,
};

Expand Down Expand Up @@ -349,7 +352,7 @@ export default class OpenAPIResolveCommand extends BaseCommand<typeof OpenAPIRes
this.debug(`Switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
}

const { preparedSpec, specPath, specType } = await prepareOas(spec, 'openapi resolve');
const { preparedSpec, specPath, specType } = await prepareOas.call(this);
if (specType !== 'OpenAPI') {
throw new Error('Sorry, this command only supports OpenAPI 3.0+ definitions.');
}
Expand Down
9 changes: 5 additions & 4 deletions src/commands/openapi/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import prompts from 'prompts';
import slugify from 'slugify';

import BaseCommand from '../../lib/baseCommand.js';
import { branchFlag, keyFlag, specArg } from '../../lib/flags.js';
import { branchFlag, keyFlag, specArg, titleFlag } from '../../lib/flags.js';
import isCI, { isTest } from '../../lib/isCI.js';
import { oraOptions } from '../../lib/logger.js';
import prepareOas from '../../lib/prepareOas.js';
import promptTerminal from '../../lib/promptWrapper.js';

export default class OpenAPIUploadCommand extends BaseCommand<typeof OpenAPIUploadCommand> {
id = 'openapi upload' as const;

static summary = 'Upload (or re-upload) your API definition to ReadMe.';

static description = [
Expand Down Expand Up @@ -56,6 +58,7 @@ export default class OpenAPIUploadCommand extends BaseCommand<typeof OpenAPIUplo
"You do not need to include a file extension (i.e., either `custom-slug.json` or `custom-slug` will work). If you do, it must match the file extension of the file you're uploading.",
].join('\n\n'),
}),
title: titleFlag,
useSpecVersion: Flags.boolean({
summary: 'Use the OpenAPI `info.version` field for your ReadMe project version',
description:
Expand Down Expand Up @@ -119,9 +122,7 @@ export default class OpenAPIUploadCommand extends BaseCommand<typeof OpenAPIUplo
}

async run() {
const { spec } = this.args;

const { preparedSpec, specFileType, specType, specPath, specVersion } = await prepareOas(spec, 'openapi upload');
const { preparedSpec, specFileType, specType, specPath, specVersion } = await prepareOas.call(this);

const branch = this.flags.useSpecVersion ? specVersion : this.flags.branch;

Expand Down
4 changes: 3 additions & 1 deletion src/commands/openapi/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { githubFlag, specArg, workingDirectoryFlag } from '../../lib/flags.js';
import prepareOas from '../../lib/prepareOas.js';

export default class OpenAPIValidateCommand extends BaseCommand<typeof OpenAPIValidateCommand> {
id = 'openapi validate' as const;

static summary = 'Validate your OpenAPI/Swagger definition.';

static description =
Expand Down Expand Up @@ -41,7 +43,7 @@ export default class OpenAPIValidateCommand extends BaseCommand<typeof OpenAPIVa
this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
}

const { specPath, specType } = await prepareOas(this.args.spec, OpenAPIValidateCommand.id);
const { specPath, specType } = await prepareOas.call(this);

return this.runCreateGHAHook({
parsedOpts: { ...this.flags, spec: specPath },
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,14 @@ export type APIv2PageUploadCommands =
* These commands can do more than just upload pages, but they are all backed by the APIv2 representations.
*/
export type APIv2PageCommands = APIv2PageUploadCommands | DocsMigrateCommand;

/**
* Every command that deals with OpenAPI definitions.
*/
export type OpenAPICommands =
| OpenAPIConvertCommand
| OpenAPIInspectCommand
| OpenAPIReduceCommand
| OpenAPIResolveCommand
| OpenAPIUploadCommand
| OpenAPIValidateCommand;
2 changes: 1 addition & 1 deletion src/lib/baseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default abstract class BaseCommand<T extends typeof OclifCommand> extends
// protected property in the base oclif class
declare debug: (...args: unknown[]) => void;

protected info(input: Parameters<typeof info>[0], opts: Parameters<typeof info>[1]): void {
public info(input: Parameters<typeof info>[0], opts?: Parameters<typeof info>[1]): void {
if (!this.jsonEnabled()) {
info(input, opts);
}
Expand Down
71 changes: 27 additions & 44 deletions src/lib/prepareOas.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { OpenAPI } from 'openapi-types';
import type { CommandIdForTopic } from '../index.js';
import type { CommandIdForTopic, OpenAPICommands } from '../index.js';

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 { oraOptions } from './logger.js';
import promptTerminal from './promptWrapper.js';
import readdirRecursive from './readdirRecursive.js';

Expand Down Expand Up @@ -49,26 +49,9 @@ function capitalizeSpecType<T extends 'openapi' | 'postman' | 'swagger' | 'unkno
/**
* 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: string | undefined,
/**
* The command context in which this is being run within (uploading a spec,
* validation, or reducing one).
*/
command: `openapi ${OpenAPIAction}`,
opts: {
/**
* An optional title to replace the value in the `info.title` field.
* @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#info-object}
*/
title?: string;
} = {},
) {
let specPath = path;
export default async function prepareOas(this: OpenAPICommands) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args are no longer required since we can grab all of it from the this context ✨

let specPath = this.args.spec;
const command = this.id satisfies `openapi ${OpenAPIAction}`;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this allows us to assert that the command id property matches predefined the openapi {action} structure... common TS + oclif w


if (!specPath) {
/**
Expand Down Expand Up @@ -97,31 +80,31 @@ export default async function prepareOas(
file.toLowerCase().endsWith('.yml'),
);

debug(`number of JSON or YAML files found: ${jsonAndYamlFiles.length}`);
this.debug(`number of JSON or YAML files found: ${jsonAndYamlFiles.length}`);

const possibleSpecFiles: FoundSpecFile[] = (
await Promise.all(
jsonAndYamlFiles.map(file => {
debug(`attempting to oas-normalize ${file}`);
this.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}`);
this.debug(`specification type for ${file}: ${specification}`);
this.debug(`version for ${file}: ${version}`);
return ['openapi', 'swagger', 'postman'].includes(specification)
? { filePath: file, specType: capitalizeSpecType(specification) as SpecType, version }
: null;
})
.catch(e => {
debug(`error extracting API definition specification version for ${file}: ${e.message}`);
this.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}`);
this.debug(`number of possible OpenAPI/Swagger files found: ${possibleSpecFiles.length}`);

if (!possibleSpecFiles.length) {
fileFindingSpinner.fail();
Expand All @@ -134,7 +117,7 @@ export default async function prepareOas(

if (possibleSpecFiles.length === 1) {
fileFindingSpinner.stop();
info(chalk.yellow(`We found ${specPath} and are attempting to ${action} it.`));
this.info(chalk.yellow(`We found ${specPath} and are attempting to ${action} it.`));
} else if (possibleSpecFiles.length > 1) {
if (isCI()) {
fileFindingSpinner.fail();
Expand All @@ -160,9 +143,9 @@ export default async function prepareOas(

const spinner = ora({ text: `Validating the API definition located at ${specPath}...`, ...oraOptions() }).start();

debug(`about to normalize spec located at ${specPath}`);
this.debug(`about to normalize spec located at ${specPath}`);
const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true });
debug('spec normalized');
this.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
Expand All @@ -182,42 +165,42 @@ export default async function prepareOas(
})
.catch((err: Error) => {
spinner.fail();
debug(`raw oas load error object: ${JSON.stringify(err)}`);
this.debug(`raw oas load error object: ${JSON.stringify(err)}`);
throw err;
});

let api: OpenAPI.Document;
await oas.validate().catch((err: Error) => {
spinner.fail();
debug(`raw validation error object: ${JSON.stringify(err)}`);
this.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)');
this.debug('converting the spec to OpenAPI 3.0 (if necessary)');
api = await oas.convert().catch((err: Error) => {
spinner.fail();
debug(`raw openapi conversion error object: ${JSON.stringify(err)}`);
this.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}`);
this.debug('πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡ spec validated! logging spec below πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡');
this.debug(api);
this.debug('πŸ‘†πŸ‘†πŸ‘†πŸ‘†πŸ‘† finished logging spec πŸ‘†πŸ‘†πŸ‘†πŸ‘†πŸ‘†');
this.debug(`spec type: ${specType}`);

if (opts.title) {
debug(`renaming title field to ${opts.title}`);
api.info.title = opts.title;
if (this.flags.title) {
this.debug(`renaming title field to ${this.flags.title}`);
api.info.title = this.flags.title;
}

const specFileType = oas.type;

// No need to optional chain here since `info.version` is required to pass validation
const specVersion: string = api.info.version;
debug(`version in spec: ${specVersion}`);
this.debug(`version in spec: ${specVersion}`);

const commandsThatBundle: (typeof command)[] = [
'openapi inspect',
Expand All @@ -229,7 +212,7 @@ export default async function prepareOas(
if (commandsThatBundle.includes(command)) {
api = await oas.bundle();

debug('spec bundled');
this.debug('spec bundled');
}

return {
Expand Down
Loading