Skip to content

Commit 72f7328

Browse files
authored
refactor(openapi): cleaner approach to prepareOas helper (#1312)
## 🧰 Changes as part of the work in #1313, this PR refactors our `prepareOas` helper (and by extension, the entire `openapi` family of commands) to be a little bit more conformant to the oclif command class pattern. this will clean up our debug logs a bit and should make it a bit easier to add new `openapi` commands going forward. ## 🧬 QA & Testing no end user changes — if tests continue to pass we should be in good shape!
1 parent 5d5bb51 commit 72f7328

File tree

10 files changed

+67
-63
lines changed

10 files changed

+67
-63
lines changed

documentation/commands/openapi.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,15 @@ Resolves circular and recursive references in OpenAPI by replacing them with obj
143143

144144
```
145145
USAGE
146-
$ rdme openapi resolve [SPEC] [--out <value>] [--workingDirectory <value>]
146+
$ rdme openapi resolve [SPEC] [--out <value>] [--title <value>] [--workingDirectory <value>]
147147
148148
ARGUMENTS
149149
SPEC A path to your API definition — either a local file path or a URL. If your working directory and all
150150
subdirectories contain a single OpenAPI file, you can omit the path.
151151
152152
FLAGS
153153
--out=<value> Output file path to write resolved file to
154+
--title=<value> An override value for the `info.title` field in the API definition
154155
--workingDirectory=<value> Working directory (for usage with relative external references)
155156
156157
DESCRIPTION
@@ -184,7 +185,7 @@ Upload (or re-upload) your API definition to ReadMe.
184185

185186
```
186187
USAGE
187-
$ rdme openapi upload [SPEC] --key <value> [--slug <value>] [--useSpecVersion | --branch <value>]
188+
$ rdme openapi upload [SPEC] --key <value> [--slug <value>] [--title <value>] [--useSpecVersion | --branch <value>]
188189
189190
ARGUMENTS
190191
SPEC A path to your API definition — either a local file path or a URL. If your working directory and all
@@ -194,6 +195,7 @@ FLAGS
194195
--key=<value> (required) ReadMe project API key
195196
--branch=<value> [default: stable] ReadMe project version
196197
--slug=<value> Override the slug (i.e., the unique identifier) for your API definition.
198+
--title=<value> An override value for the `info.title` field in the API definition
197199
--useSpecVersion Use the OpenAPI `info.version` field for your ReadMe project version
198200
199201
DESCRIPTION

src/commands/openapi/convert.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import promptTerminal from '../../lib/promptWrapper.js';
1414
import { validateFilePath } from '../../lib/validatePromptInput.js';
1515

1616
export default class OpenAPIConvertCommand extends BaseCommand<typeof OpenAPIConvertCommand> {
17+
id = 'openapi convert' as const;
18+
1719
static summary = 'Converts an API definition to OpenAPI and bundles any external references.';
1820

1921
static description =
@@ -43,18 +45,15 @@ export default class OpenAPIConvertCommand extends BaseCommand<typeof OpenAPICon
4345
];
4446

4547
async run() {
46-
const { spec } = this.args;
47-
const { out, title, workingDirectory } = this.flags;
48+
const { out, workingDirectory } = this.flags;
4849

4950
if (workingDirectory) {
5051
const previousWorkingDirectory = process.cwd();
5152
process.chdir(workingDirectory);
5253
this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
5354
}
5455

55-
const { preparedSpec, specPath, specType } = await prepareOas(spec, 'openapi convert', {
56-
title,
57-
});
56+
const { preparedSpec, specPath, specType } = await prepareOas.call(this);
5857
const parsedPreparedSpec: OASDocument = JSON.parse(preparedSpec);
5958

6059
if (specType === 'OpenAPI') {

src/commands/openapi/inspect.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ function buildFullReport(analysis: Analysis, definitionVersion: string, tableBor
198198
}
199199

200200
export default class OpenAPIInspectCommand extends BaseCommand<typeof OpenAPIInspectCommand> {
201+
id = 'openapi inspect' as const;
202+
201203
static summary = 'Analyze an OpenAPI/Swagger definition for various OpenAPI and ReadMe feature usage.';
202204

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

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

257259
const spinner = ora({ ...oraOptions() });

src/commands/openapi/reduce.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import promptTerminal from '../../lib/promptWrapper.js';
1818
import { validateFilePath } from '../../lib/validatePromptInput.js';
1919

2020
export default class OpenAPIReduceCommand extends BaseCommand<typeof OpenAPIReduceCommand> {
21+
id = 'openapi reduce' as const;
22+
2123
static summary = 'Reduce an OpenAPI definition into a smaller subset.';
2224

2325
static description =
@@ -58,17 +60,16 @@ export default class OpenAPIReduceCommand extends BaseCommand<typeof OpenAPIRedu
5860
];
5961

6062
async run() {
61-
const { spec } = this.args;
6263
const opts = this.flags;
63-
const { title, workingDirectory } = opts;
64+
const { workingDirectory } = opts;
6465

6566
if (workingDirectory) {
6667
const previousWorkingDirectory = process.cwd();
6768
process.chdir(workingDirectory);
6869
this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
6970
}
7071

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

7475
if (specType !== 'OpenAPI') {

src/commands/openapi/resolve.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import prompts from 'prompts';
1111

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

2424
export default class OpenAPIResolveCommand extends BaseCommand<typeof OpenAPIResolveCommand> {
25+
id = 'openapi resolve' as const;
26+
2527
static summary = 'Resolves circular and recursive references in OpenAPI by replacing them with object schemas.';
2628

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

5153
static flags = {
5254
out: Flags.string({ description: 'Output file path to write resolved file to' }),
55+
title: titleFlag,
5356
workingDirectory: workingDirectoryFlag,
5457
};
5558

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

352-
const { preparedSpec, specPath, specType } = await prepareOas(spec, 'openapi resolve');
355+
const { preparedSpec, specPath, specType } = await prepareOas.call(this);
353356
if (specType !== 'OpenAPI') {
354357
throw new Error('Sorry, this command only supports OpenAPI 3.0+ definitions.');
355358
}

src/commands/openapi/upload.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import prompts from 'prompts';
1414
import slugify from 'slugify';
1515

1616
import BaseCommand from '../../lib/baseCommand.js';
17-
import { branchFlag, keyFlag, specArg } from '../../lib/flags.js';
17+
import { branchFlag, keyFlag, specArg, titleFlag } from '../../lib/flags.js';
1818
import isCI, { isTest } from '../../lib/isCI.js';
1919
import { oraOptions } from '../../lib/logger.js';
2020
import prepareOas from '../../lib/prepareOas.js';
2121
import promptTerminal from '../../lib/promptWrapper.js';
2222

2323
export default class OpenAPIUploadCommand extends BaseCommand<typeof OpenAPIUploadCommand> {
24+
id = 'openapi upload' as const;
25+
2426
static summary = 'Upload (or re-upload) your API definition to ReadMe.';
2527

2628
static description = [
@@ -56,6 +58,7 @@ export default class OpenAPIUploadCommand extends BaseCommand<typeof OpenAPIUplo
5658
"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.",
5759
].join('\n\n'),
5860
}),
61+
title: titleFlag,
5962
useSpecVersion: Flags.boolean({
6063
summary: 'Use the OpenAPI `info.version` field for your ReadMe project version',
6164
description:
@@ -119,9 +122,7 @@ export default class OpenAPIUploadCommand extends BaseCommand<typeof OpenAPIUplo
119122
}
120123

121124
async run() {
122-
const { spec } = this.args;
123-
124-
const { preparedSpec, specFileType, specType, specPath, specVersion } = await prepareOas(spec, 'openapi upload');
125+
const { preparedSpec, specFileType, specType, specPath, specVersion } = await prepareOas.call(this);
125126

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

src/commands/openapi/validate.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { githubFlag, specArg, workingDirectoryFlag } from '../../lib/flags.js';
55
import prepareOas from '../../lib/prepareOas.js';
66

77
export default class OpenAPIValidateCommand extends BaseCommand<typeof OpenAPIValidateCommand> {
8+
id = 'openapi validate' as const;
9+
810
static summary = 'Validate your OpenAPI/Swagger definition.';
911

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

44-
const { specPath, specType } = await prepareOas(this.args.spec, OpenAPIValidateCommand.id);
46+
const { specPath, specType } = await prepareOas.call(this);
4547

4648
return this.runCreateGHAHook({
4749
parsedOpts: { ...this.flags, spec: specPath },

src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,14 @@ export type APIv2PageUploadCommands =
9292
* These commands can do more than just upload pages, but they are all backed by the APIv2 representations.
9393
*/
9494
export type APIv2PageCommands = APIv2PageUploadCommands | DocsMigrateCommand;
95+
96+
/**
97+
* Every command that deals with OpenAPI definitions.
98+
*/
99+
export type OpenAPICommands =
100+
| OpenAPIConvertCommand
101+
| OpenAPIInspectCommand
102+
| OpenAPIReduceCommand
103+
| OpenAPIResolveCommand
104+
| OpenAPIUploadCommand
105+
| OpenAPIValidateCommand;

src/lib/baseCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default abstract class BaseCommand<T extends typeof OclifCommand> extends
5353
// protected property in the base oclif class
5454
declare debug: (...args: unknown[]) => void;
5555

56-
protected info(input: Parameters<typeof info>[0], opts: Parameters<typeof info>[1]): void {
56+
public info(input: Parameters<typeof info>[0], opts?: Parameters<typeof info>[1]): void {
5757
if (!this.jsonEnabled()) {
5858
info(input, opts);
5959
}

src/lib/prepareOas.ts

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { OpenAPI } from 'openapi-types';
2-
import type { CommandIdForTopic } from '../index.js';
2+
import type { CommandIdForTopic, OpenAPICommands } from '../index.js';
33

44
import chalk from 'chalk';
55
import OASNormalize from 'oas-normalize';
66
import { getAPIDefinitionType } from 'oas-normalize/lib/utils';
77
import ora from 'ora';
88

99
import isCI from './isCI.js';
10-
import { debug, info, oraOptions } from './logger.js';
10+
import { oraOptions } from './logger.js';
1111
import promptTerminal from './promptWrapper.js';
1212
import readdirRecursive from './readdirRecursive.js';
1313

@@ -49,26 +49,9 @@ function capitalizeSpecType<T extends 'openapi' | 'postman' | 'swagger' | 'unkno
4949
/**
5050
* Normalizes, validates, and (optionally) bundles an OpenAPI definition.
5151
*/
52-
export default async function prepareOas(
53-
/**
54-
* Path to a spec file. If this is missing, the current directory is searched for
55-
* certain file names.
56-
*/
57-
path: string | undefined,
58-
/**
59-
* The command context in which this is being run within (uploading a spec,
60-
* validation, or reducing one).
61-
*/
62-
command: `openapi ${OpenAPIAction}`,
63-
opts: {
64-
/**
65-
* An optional title to replace the value in the `info.title` field.
66-
* @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#info-object}
67-
*/
68-
title?: string;
69-
} = {},
70-
) {
71-
let specPath = path;
52+
export default async function prepareOas(this: OpenAPICommands) {
53+
let specPath = this.args.spec;
54+
const command = this.id satisfies `openapi ${OpenAPIAction}`;
7255

7356
if (!specPath) {
7457
/**
@@ -97,31 +80,31 @@ export default async function prepareOas(
9780
file.toLowerCase().endsWith('.yml'),
9881
);
9982

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

10285
const possibleSpecFiles: FoundSpecFile[] = (
10386
await Promise.all(
10487
jsonAndYamlFiles.map(file => {
105-
debug(`attempting to oas-normalize ${file}`);
88+
this.debug(`attempting to oas-normalize ${file}`);
10689
const oas = new OASNormalize(file, { enablePaths: true });
10790
return oas
10891
.version()
10992
.then(({ specification, version }) => {
110-
debug(`specification type for ${file}: ${specification}`);
111-
debug(`version for ${file}: ${version}`);
93+
this.debug(`specification type for ${file}: ${specification}`);
94+
this.debug(`version for ${file}: ${version}`);
11295
return ['openapi', 'swagger', 'postman'].includes(specification)
11396
? { filePath: file, specType: capitalizeSpecType(specification) as SpecType, version }
11497
: null;
11598
})
11699
.catch(e => {
117-
debug(`error extracting API definition specification version for ${file}: ${e.message}`);
100+
this.debug(`error extracting API definition specification version for ${file}: ${e.message}`);
118101
return null;
119102
});
120103
}),
121104
)
122105
).filter(truthy);
123106

124-
debug(`number of possible OpenAPI/Swagger files found: ${possibleSpecFiles.length}`);
107+
this.debug(`number of possible OpenAPI/Swagger files found: ${possibleSpecFiles.length}`);
125108

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

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

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

163-
debug(`about to normalize spec located at ${specPath}`);
146+
this.debug(`about to normalize spec located at ${specPath}`);
164147
const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true });
165-
debug('spec normalized');
148+
this.debug('spec normalized');
166149

167150
// We're retrieving the original specification type here instead of after validation because if
168151
// they give us a Postman collection we should tell them that we handled a Postman collection, not
@@ -182,42 +165,42 @@ export default async function prepareOas(
182165
})
183166
.catch((err: Error) => {
184167
spinner.fail();
185-
debug(`raw oas load error object: ${JSON.stringify(err)}`);
168+
this.debug(`raw oas load error object: ${JSON.stringify(err)}`);
186169
throw err;
187170
});
188171

189172
let api: OpenAPI.Document;
190173
await oas.validate().catch((err: Error) => {
191174
spinner.fail();
192-
debug(`raw validation error object: ${JSON.stringify(err)}`);
175+
this.debug(`raw validation error object: ${JSON.stringify(err)}`);
193176
throw err;
194177
});
195178

196179
// If we were supplied a Postman collection this will **always** convert it to OpenAPI 3.0.
197-
debug('converting the spec to OpenAPI 3.0 (if necessary)');
180+
this.debug('converting the spec to OpenAPI 3.0 (if necessary)');
198181
api = await oas.convert().catch((err: Error) => {
199182
spinner.fail();
200-
debug(`raw openapi conversion error object: ${JSON.stringify(err)}`);
183+
this.debug(`raw openapi conversion error object: ${JSON.stringify(err)}`);
201184
throw err;
202185
});
203186

204187
spinner.stop();
205188

206-
debug('👇👇👇👇👇 spec validated! logging spec below 👇👇👇👇👇');
207-
debug(api);
208-
debug('👆👆👆👆👆 finished logging spec 👆👆👆👆👆');
209-
debug(`spec type: ${specType}`);
189+
this.debug('👇👇👇👇👇 spec validated! logging spec below 👇👇👇👇👇');
190+
this.debug(api);
191+
this.debug('👆👆👆👆👆 finished logging spec 👆👆👆👆👆');
192+
this.debug(`spec type: ${specType}`);
210193

211-
if (opts.title) {
212-
debug(`renaming title field to ${opts.title}`);
213-
api.info.title = opts.title;
194+
if (this.flags.title) {
195+
this.debug(`renaming title field to ${this.flags.title}`);
196+
api.info.title = this.flags.title;
214197
}
215198

216199
const specFileType = oas.type;
217200

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

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

232-
debug('spec bundled');
215+
this.debug('spec bundled');
233216
}
234217

235218
return {

0 commit comments

Comments
 (0)