-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathutil.ts
executable file
·253 lines (230 loc) · 7.63 KB
/
util.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
244
245
246
247
248
249
250
251
252
253
import { convert as convertApiFormat } from 'api-spec-converter';
import * as ct from 'content-type';
import * as express from 'express';
import { Response } from 'express';
import * as fs from 'fs';
import * as http from 'http';
import * as yaml from 'js-yaml';
import { Request as OasRequest } from 'openapi-backend';
import * as path from 'path';
import * as qs from 'qs';
import * as waitOn from 'wait-on';
import { ResponseParsingError } from '../types/errors';
import { ValidationResults } from '../types/validation';
import * as rp from 'request-promise-native';
function isSwaggerV2(apiDoc: any): boolean {
return apiDoc.swagger === '2.0';
}
function isOpenAPIv3(apiDoc: any): boolean {
return typeof apiDoc.openapi === 'string' && apiDoc.openapi.startsWith('3.');
}
/**
* Provides the format of the OpenAPI document.
* @param apiDoc A parsed OpenAPI document as a plain Object.
*/
export function getAPIDocFormat(
apiDoc: any,
): ('openapi-2.0' | 'openapi-3.0') | null {
const validators: { [key: string]: (apiDoc: any) => boolean } = {
'openapi-2.0': isSwaggerV2,
'openapi-3.0': isOpenAPIv3,
};
for (const format in validators) {
if (!Object.prototype.hasOwnProperty.call(validators, format)) continue;
const validator = validators[format];
if (validator(apiDoc)) return format as 'openapi-2.0' | 'openapi-3.0';
}
return null;
}
export function parseJsonOrYaml(filePath: string, data: string): any {
switch (path.extname(filePath)) {
case '.json':
return JSON.parse(data);
case '.yaml':
case '.yml':
return yaml.safeLoad(data);
case '.':
throw new Error('Will not read a file that has no extension.');
default:
throw new Error('Wrong file extension.');
}
}
export function readJsonOrYamlSync(filePath: string): any {
return parseJsonOrYaml(filePath, fs.readFileSync(filePath, 'utf8'));
}
export function readFileSync(filePath: string): any {
return readJsonOrYamlSync(filePath);
}
export async function fetchAndReadFile(uri: string): Promise<any> {
return rp(uri).then(responseBody => parseJsonOrYaml(uri, responseBody));
}
/**
* Converts a OpenAPI document to v3. It detects the filetype of the document
* and returns the contents as an Object. Returns the contents of the
* unmodified file when no conversion is necessary.
*/
export async function convertToOpenApiV3(
apiDoc: any,
filePath: string,
): Promise<any> {
switch (getAPIDocFormat(apiDoc)) {
case 'openapi-2.0': {
const apiDocTarget = await convertApiFormat({
from: 'swagger_2',
to: 'openapi_3',
source: filePath,
});
return apiDocTarget.spec;
}
case 'openapi-3.0':
// Return unmodified OpenAPI document
return apiDoc;
default:
throw new Error('Unsupported API document format');
}
}
/**
* Parses a request depending on the 'Content-Type' header. Supports JSON and
* URL-encoded formats. The request body should be a Buffer.
*/
export function parseRequest(req: express.Request): any {
const contentTypeString = req.get('content-type');
if (!contentTypeString) {
throw new Error('Received request with an empty Content-Type header.');
}
if (!(req.body instanceof Buffer)) {
throw new Error('Can not parse a request body which is not a Buffer.');
}
const contentType = ct.parse(contentTypeString);
const charset = contentType.parameters['charset'] || 'utf-8';
if (req.is('application/json') || req.is('json')) {
return JSON.parse(req.body.toString(charset));
}
if (req.is('application/x-www-form-urlencoded')) {
return qs.parse(req.body.toString(charset));
}
throw new Error(`No parser available for content type '${contentType}'.`);
}
/** Converts an express.Request to a simplified OpenAPI request. */
export function toOasRequest(req: express.Request): OasRequest {
const oasRequest: OasRequest = {
method: req.method,
path: req.params[0],
headers: req.headers as {
[key: string]: string | string[];
},
query: req.query as any,
};
// Parse when body is present
if (typeof req.body !== 'undefined' && req.body instanceof Buffer) {
try {
oasRequest.body = parseRequest(req);
} catch (e) {
throw new ResponseParsingError('Failed to parse request body. ' + e);
}
}
return oasRequest;
}
/**
* Parses a response body depending on the 'Content-Type' header. Supports JSON
* and URL-encoded formats. The response body should be a string.
*/
export function parseResponseBody(
res: http.IncomingMessage & {
body: unknown;
},
): any {
const contentTypeString = res.headers['content-type'];
if (!contentTypeString) {
throw new Error('Received response with an empty Content-Type header.');
}
const contentType = ct.parse(contentTypeString);
if (typeof res.body !== 'string') {
throw new Error('Can not parse a response body which is not a string.');
}
if (contentType.type === 'application/json' || contentType.type === 'json') {
return JSON.parse(res.body);
}
if (contentType.type === 'application/x-www-form-urlencoded') {
return qs.parse(res.body);
}
throw new Error(`No parser available for content type '${contentType}'.`);
}
/**
* Copies the headers from a source response into a target response,
* overwriting existing values.
*/
export function copyHeaders(
sourceResponse: any,
targetResponse: Response,
): void {
for (const key in sourceResponse.headers) {
if (!Object.prototype.hasOwnProperty.call(sourceResponse.headers, key)) {
continue;
}
targetResponse.setHeader(key, sourceResponse.headers[key]);
}
}
/**
* Sets a custom openapi-cop validation header ('openapi-cop-validation-result')
* to the validation results as JSON.
*/
export function setValidationHeader(
res: Response,
validationResults: ValidationResults,
): void {
res.setHeader(
'openapi-cop-validation-result',
JSON.stringify(validationResults),
);
}
/**
* Sets a custom openapi-cop validation header ('openapi-cop-validation-result')
* to the validation results as JSON.
*/
export function setSourceRequestHeader(
res: Response,
oasRequest: OasRequest,
): void {
res.setHeader('openapi-cop-source-request', JSON.stringify(oasRequest));
}
/** Closes the server and waits until the port is again free. */
export async function closeServer(server: http.Server): Promise<void> {
const port = (server.address() as any).port;
await new Promise<void>((resolve, reject) => {
server.close(err => {
if (err) return reject(err);
resolve();
});
});
await waitOn({ resources: [`http://localhost:${port}`], reverse: true });
}
/**
* Recursively maps a nested object (JSON) given a mapping function. Maps in
* depth-first order. If it finds an array it applies the mapping function
* for object elements.
*
* @param obj Object to be mapped on.
* @param fn Mapping function that returns the new value.
* @param traversalPath internal parameter used to track the current traversal path
*/
export function mapWalkObject(obj: any, fn: (currentObj: any, traversalPath: Array<string>) => any, traversalPath: Array<string> = []): any {
let objCopy = Object.assign({}, obj);
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const value = obj[key];
if (value.constructor === Object) {
objCopy[key] = mapWalkObject(value, fn, [...traversalPath, key]);
} else if (value.constructor === Array) {
objCopy[key] = objCopy[key].map((e: any) => {
if (e.constructor === Object) {
return mapWalkObject(e, fn, [...traversalPath, key]);
} else {
return e;
}
});
}
}
objCopy = fn(objCopy, traversalPath);
return objCopy;
}