-
Notifications
You must be signed in to change notification settings - Fork 43
/
Copy pathprepareOas.ts
243 lines (205 loc) · 8.16 KB
/
prepareOas.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import type { CommandIdForTopic } from '../index.js';
import type { OpenAPI } from 'openapi-types';
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';
export type SpecFileType = OASNormalize['type'];
type SpecType = 'OpenAPI' | 'Postman' | 'Swagger';
interface FoundSpecFile {
/** path to the spec file */
filePath: string;
specType: SpecType;
/**
* OpenAPI or Postman specification version
* @example '3.1'
*/
version: string;
}
interface FileSelection {
file: string;
}
// source: https://stackoverflow.com/a/58110124
type Truthy<T> = T extends '' | 0 | false | null | undefined ? never : T;
function truthy<T>(value: T): value is Truthy<T> {
return !!value;
}
type OpenAPIAction = CommandIdForTopic<'openapi'>;
const capitalizeSpecType = (type: string) =>
type === 'openapi' ? 'OpenAPI' : type.charAt(0).toUpperCase() + type.slice(1);
/**
* Normalizes, validates, and (optionally) bundles an OpenAPI definition.
*
* @param path Path to a spec file. If this is missing, the current directory is searched for
* certain file names.
* @param command The command context in which this is being run within (uploading a spec,
* validation, or reducing one).
*/
export default async function prepareOas(
path: string | undefined,
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;
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: OpenAPIAction = command.replace('openapi ', '') as OpenAPIAction;
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: FoundSpecFile[] = (
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) as SpecType, 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: FileSelection = 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: Error) => {
spinner.fail();
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)}`);
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: Error) => {
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: string = api.info.version;
debug(`version in spec: ${specVersion}`);
const commandsThatBundle: (typeof command)[] = ['openapi inspect', 'openapi reduce', '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,
};
}