@gitbook/react-openapi 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/OpenAPICodeSample.jsx +17 -16
- package/dist/OpenAPIDisclosure.d.ts +2 -1
- package/dist/OpenAPIDisclosure.jsx +1 -1
- package/dist/OpenAPIDisclosureGroup.d.ts +1 -1
- package/dist/OpenAPIDisclosureGroup.jsx +2 -2
- package/dist/OpenAPIOperation.jsx +21 -7
- package/dist/OpenAPIPath.d.ts +3 -2
- package/dist/OpenAPIPath.jsx +4 -15
- package/dist/OpenAPIRequestBody.jsx +1 -1
- package/dist/OpenAPIResponse.jsx +1 -1
- package/dist/OpenAPIResponseExample.jsx +6 -5
- package/dist/OpenAPIResponses.d.ts +1 -1
- package/dist/OpenAPIResponses.jsx +2 -2
- package/dist/OpenAPISchema.d.ts +5 -1
- package/dist/OpenAPISchema.jsx +72 -61
- package/dist/OpenAPISchemaName.d.ts +5 -3
- package/dist/OpenAPISchemaName.jsx +25 -4
- package/dist/OpenAPISecurities.jsx +2 -2
- package/dist/OpenAPITabs.d.ts +3 -3
- package/dist/OpenAPITabs.jsx +17 -14
- package/dist/ScalarApiButton.jsx +1 -1
- package/dist/code-samples.js +239 -17
- package/dist/contentTypeChecks.d.ts +9 -0
- package/dist/contentTypeChecks.js +27 -0
- package/dist/generateSchemaExample.js +2 -1
- package/dist/resolveOpenAPIOperation.d.ts +3 -3
- package/dist/resolveOpenAPIOperation.js +1 -1
- package/dist/stringifyOpenAPI.d.ts +1 -1
- package/dist/stringifyOpenAPI.js +8 -2
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/util/server.d.ts +9 -0
- package/dist/util/server.js +44 -0
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +7 -6
- package/package.json +3 -8
- package/src/InteractiveSection.tsx +4 -4
- package/src/OpenAPICodeSample.tsx +20 -19
- package/src/OpenAPIDisclosure.tsx +4 -3
- package/src/OpenAPIDisclosureGroup.tsx +5 -5
- package/src/OpenAPIOperation.tsx +32 -10
- package/src/OpenAPIOperationContext.tsx +1 -1
- package/src/OpenAPIPath.tsx +11 -10
- package/src/OpenAPIRequestBody.tsx +2 -2
- package/src/OpenAPIResponse.tsx +3 -3
- package/src/OpenAPIResponseExample.tsx +7 -6
- package/src/OpenAPIResponses.tsx +4 -4
- package/src/OpenAPISchema.test.ts +5 -5
- package/src/OpenAPISchema.tsx +134 -73
- package/src/OpenAPISchemaName.tsx +40 -7
- package/src/OpenAPISecurities.tsx +3 -3
- package/src/OpenAPITabs.tsx +23 -17
- package/src/ScalarApiButton.tsx +3 -3
- package/src/code-samples.test.ts +594 -2
- package/src/code-samples.ts +238 -17
- package/src/contentTypeChecks.ts +35 -0
- package/src/generateSchemaExample.ts +22 -18
- package/src/json2xml.test.ts +1 -1
- package/src/resolveOpenAPIOperation.test.ts +6 -6
- package/src/resolveOpenAPIOperation.ts +7 -7
- package/src/stringifyOpenAPI.ts +13 -2
- package/src/types.ts +11 -1
- package/src/util/server.test.ts +58 -0
- package/src/util/server.ts +47 -0
- package/src/utils.ts +9 -5
- package/dist/OpenAPIServerURL.d.ts +0 -11
- package/dist/OpenAPIServerURL.jsx +0 -67
- package/dist/OpenAPIServerURLVariable.d.ts +0 -8
- package/dist/OpenAPIServerURLVariable.jsx +0 -8
- package/src/OpenAPIServerURL.tsx +0 -73
- package/src/OpenAPIServerURLVariable.tsx +0 -14
package/src/code-samples.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isCSV,
|
|
3
|
+
isFormData,
|
|
4
|
+
isFormUrlEncoded,
|
|
5
|
+
isGraphQL,
|
|
6
|
+
isPDF,
|
|
7
|
+
isPlainObject,
|
|
8
|
+
isText,
|
|
9
|
+
isXML,
|
|
10
|
+
} from './contentTypeChecks';
|
|
1
11
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
|
2
12
|
|
|
3
13
|
export interface CodeSampleInput {
|
|
@@ -30,14 +40,27 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
|
|
30
40
|
|
|
31
41
|
lines.push(`--url '${url}'`);
|
|
32
42
|
|
|
33
|
-
if (
|
|
43
|
+
if (body) {
|
|
44
|
+
const bodyContent = BodyGenerators.getCurlBody(body, headers);
|
|
45
|
+
|
|
46
|
+
if (bodyContent) {
|
|
47
|
+
body = bodyContent.body;
|
|
48
|
+
headers = bodyContent.headers;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
34
53
|
Object.entries(headers).forEach(([key, value]) => {
|
|
35
54
|
lines.push(`--header '${key}: ${value}'`);
|
|
36
55
|
});
|
|
37
56
|
}
|
|
38
57
|
|
|
39
|
-
if (body
|
|
40
|
-
|
|
58
|
+
if (body) {
|
|
59
|
+
if (Array.isArray(body)) {
|
|
60
|
+
lines.push(...body);
|
|
61
|
+
} else {
|
|
62
|
+
lines.push(body);
|
|
63
|
+
}
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator);
|
|
@@ -50,19 +73,30 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
|
|
50
73
|
generate: ({ method, url, headers, body }) => {
|
|
51
74
|
let code = '';
|
|
52
75
|
|
|
76
|
+
if (body) {
|
|
77
|
+
const lines = BodyGenerators.getJavaScriptBody(body, headers);
|
|
78
|
+
|
|
79
|
+
if (lines) {
|
|
80
|
+
// add the generated code to the top
|
|
81
|
+
code += lines.code;
|
|
82
|
+
body = lines.body;
|
|
83
|
+
headers = lines.headers;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
code += `const response = await fetch('${url}', {
|
|
54
88
|
method: '${method.toUpperCase()}',\n`;
|
|
55
89
|
|
|
56
|
-
if (headers) {
|
|
90
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
57
91
|
code += indent(`headers: ${stringifyOpenAPI(headers, null, 2)},\n`, 4);
|
|
58
92
|
}
|
|
59
93
|
|
|
60
94
|
if (body) {
|
|
61
|
-
code += indent(`body:
|
|
95
|
+
code += indent(`body: ${body}\n`, 4);
|
|
62
96
|
}
|
|
63
97
|
|
|
64
|
-
code +=
|
|
65
|
-
code +=
|
|
98
|
+
code += '});\n\n';
|
|
99
|
+
code += 'const data = await response.json();';
|
|
66
100
|
|
|
67
101
|
return code;
|
|
68
102
|
},
|
|
@@ -73,16 +107,35 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
|
|
73
107
|
syntax: 'python',
|
|
74
108
|
generate: ({ method, url, headers, body }) => {
|
|
75
109
|
let code = 'import requests\n\n';
|
|
110
|
+
|
|
111
|
+
if (body) {
|
|
112
|
+
const lines = BodyGenerators.getPythonBody(body, headers);
|
|
113
|
+
|
|
114
|
+
// add the generated code to the top
|
|
115
|
+
if (lines) {
|
|
116
|
+
code += lines.code;
|
|
117
|
+
body = lines.body;
|
|
118
|
+
headers = lines.headers;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
76
122
|
code += `response = requests.${method.toLowerCase()}(\n`;
|
|
77
123
|
code += indent(`"${url}",\n`, 4);
|
|
78
|
-
|
|
124
|
+
|
|
125
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
79
126
|
code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4);
|
|
80
127
|
}
|
|
128
|
+
|
|
81
129
|
if (body) {
|
|
82
|
-
|
|
130
|
+
if (body === 'files') {
|
|
131
|
+
code += indent(`files=${body}\n`, 4);
|
|
132
|
+
} else {
|
|
133
|
+
code += indent(`data=${stringifyOpenAPI(body)}\n`, 4);
|
|
134
|
+
}
|
|
83
135
|
}
|
|
84
|
-
|
|
85
|
-
code +=
|
|
136
|
+
|
|
137
|
+
code += ')\n\n';
|
|
138
|
+
code += 'data = response.json()';
|
|
86
139
|
return code;
|
|
87
140
|
},
|
|
88
141
|
},
|
|
@@ -99,6 +152,12 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
|
|
99
152
|
// handle unicode chars with a text encoder
|
|
100
153
|
const encoder = new TextEncoder();
|
|
101
154
|
|
|
155
|
+
const bodyString = BodyGenerators.getHTTPBody(body, headers);
|
|
156
|
+
|
|
157
|
+
if (bodyString) {
|
|
158
|
+
body = bodyString;
|
|
159
|
+
}
|
|
160
|
+
|
|
102
161
|
headers = {
|
|
103
162
|
...headers,
|
|
104
163
|
'Content-Length': encoder.encode(bodyContent).length.toString(),
|
|
@@ -110,14 +169,14 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
|
|
110
169
|
}
|
|
111
170
|
|
|
112
171
|
const headerString = headers
|
|
113
|
-
? Object.entries(headers)
|
|
172
|
+
? `${Object.entries(headers)
|
|
114
173
|
.map(([key, value]) =>
|
|
115
|
-
key.toLowerCase() !== 'host' ? `${key}: ${value}` :
|
|
174
|
+
key.toLowerCase() !== 'host' ? `${key}: ${value}` : ''
|
|
116
175
|
)
|
|
117
|
-
.join('\n')
|
|
176
|
+
.join('\n')}\n`
|
|
118
177
|
: '';
|
|
119
178
|
|
|
120
|
-
const bodyString = body ? `\n${
|
|
179
|
+
const bodyString = body ? `\n${body}` : '';
|
|
121
180
|
|
|
122
181
|
const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1
|
|
123
182
|
Host: ${host}
|
|
@@ -141,7 +200,7 @@ export function parseHostAndPath(url: string) {
|
|
|
141
200
|
const urlObj = new URL(url);
|
|
142
201
|
const path = urlObj.pathname || '/';
|
|
143
202
|
return { host: urlObj.host, path };
|
|
144
|
-
} catch (
|
|
203
|
+
} catch (_e) {
|
|
145
204
|
// If the URL was invalid do our best to parse the URL.
|
|
146
205
|
// Check for the protocol part and pull it off to grab the host
|
|
147
206
|
const splitted = url.split('//');
|
|
@@ -152,8 +211,170 @@ export function parseHostAndPath(url: string) {
|
|
|
152
211
|
// pull off the host (mutates)
|
|
153
212
|
const host = parts.shift();
|
|
154
213
|
// add a leading slash and join the paths again
|
|
155
|
-
const path =
|
|
214
|
+
const path = `/${parts.join('/')}`;
|
|
156
215
|
|
|
157
216
|
return { host, path };
|
|
158
217
|
}
|
|
159
218
|
}
|
|
219
|
+
|
|
220
|
+
// Body Generators
|
|
221
|
+
const BodyGenerators = {
|
|
222
|
+
getCurlBody(body: any, headers?: Record<string, string>) {
|
|
223
|
+
if (!body || !headers) return undefined;
|
|
224
|
+
|
|
225
|
+
// Copy headers to avoid mutating the original object
|
|
226
|
+
const headersCopy = { ...headers };
|
|
227
|
+
const contentType: string = headersCopy['Content-Type'] || '';
|
|
228
|
+
|
|
229
|
+
if (isFormData(contentType)) {
|
|
230
|
+
body = isPlainObject(body)
|
|
231
|
+
? Object.entries(body).map(([key, value]) => `--form '${key}=${String(value)}'`)
|
|
232
|
+
: `--form 'file=@${body}'`;
|
|
233
|
+
} else if (isFormUrlEncoded(contentType)) {
|
|
234
|
+
body = isPlainObject(body)
|
|
235
|
+
? `--data '${Object.entries(body)
|
|
236
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
237
|
+
.join('&')}'`
|
|
238
|
+
: String(body);
|
|
239
|
+
} else if (isText(contentType)) {
|
|
240
|
+
body = `--data '${String(body).replace(/"/g, '')}'`;
|
|
241
|
+
} else if (isXML(contentType) || isCSV(contentType)) {
|
|
242
|
+
// We use --data-binary to avoid cURL converting newlines to \r\n
|
|
243
|
+
body = `--data-binary $'${stringifyOpenAPI(body).replace(/"/g, '')}'`;
|
|
244
|
+
} else if (isGraphQL(contentType)) {
|
|
245
|
+
body = `--data '${stringifyOpenAPI(body)}'`;
|
|
246
|
+
// Set Content-Type to application/json for GraphQL, recommended by GraphQL spec
|
|
247
|
+
headersCopy['Content-Type'] = 'application/json';
|
|
248
|
+
} else if (isPDF(contentType)) {
|
|
249
|
+
// We use --data-binary to avoid cURL converting newlines to \r\n
|
|
250
|
+
body = `--data-binary '@${String(body)}'`;
|
|
251
|
+
} else {
|
|
252
|
+
body = `--data '${stringifyOpenAPI(body, null, 2)}'`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
body,
|
|
257
|
+
headers: headersCopy,
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
getJavaScriptBody: (body: any, headers?: Record<string, string>) => {
|
|
261
|
+
if (!body || !headers) return;
|
|
262
|
+
|
|
263
|
+
let code = '';
|
|
264
|
+
|
|
265
|
+
// Copy headers to avoid mutating the original object
|
|
266
|
+
const headersCopy = { ...headers };
|
|
267
|
+
const contentType: string = headersCopy['Content-Type'] || '';
|
|
268
|
+
|
|
269
|
+
// Use FormData for file uploads
|
|
270
|
+
if (isFormData(contentType)) {
|
|
271
|
+
code += 'const formData = new FormData();\n\n';
|
|
272
|
+
if (isPlainObject(body)) {
|
|
273
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
274
|
+
code += `formData.append("${key}", "${String(value)}");\n`;
|
|
275
|
+
});
|
|
276
|
+
} else if (typeof body === 'string') {
|
|
277
|
+
code += `formData.append("file", "${body}");\n`;
|
|
278
|
+
}
|
|
279
|
+
code += '\n';
|
|
280
|
+
body = 'formData';
|
|
281
|
+
} else if (isFormUrlEncoded(contentType)) {
|
|
282
|
+
// Use URLSearchParams for form-urlencoded data
|
|
283
|
+
code += 'const params = new URLSearchParams();\n\n';
|
|
284
|
+
if (isPlainObject(body)) {
|
|
285
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
286
|
+
code += `params.append("${key}", "${String(value)}");\n`;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
code += '\n';
|
|
290
|
+
body = 'params.toString()';
|
|
291
|
+
} else if (isGraphQL(contentType)) {
|
|
292
|
+
if (isPlainObject(body)) {
|
|
293
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
294
|
+
code += `const ${key} = \`\n${indent(String(value), 4)}\`;\n\n`;
|
|
295
|
+
});
|
|
296
|
+
body = `JSON.stringify({ ${Object.keys(body).join(', ')} })`;
|
|
297
|
+
// Set Content-Type to application/json for GraphQL, recommended by GraphQL spec
|
|
298
|
+
headersCopy['Content-Type'] = 'application/json';
|
|
299
|
+
} else {
|
|
300
|
+
code += `const query = \`\n${indent(String(body), 4)}\`;\n\n`;
|
|
301
|
+
body = 'JSON.stringify(query)';
|
|
302
|
+
}
|
|
303
|
+
} else if (isCSV(contentType)) {
|
|
304
|
+
code += 'const csv = `\n';
|
|
305
|
+
code += indent(String(body), 4);
|
|
306
|
+
code += '`;\n\n';
|
|
307
|
+
body = 'csv';
|
|
308
|
+
} else if (isPDF(contentType)) {
|
|
309
|
+
// Use FormData to upload PDF files
|
|
310
|
+
code += 'const formData = new FormData();\n\n';
|
|
311
|
+
code += `formData.append("file", "${body}");\n\n`;
|
|
312
|
+
body = 'formData';
|
|
313
|
+
} else if (isXML(contentType)) {
|
|
314
|
+
code += 'const xml = `\n';
|
|
315
|
+
code += indent(String(body), 4);
|
|
316
|
+
code += '`;\n\n';
|
|
317
|
+
body = 'xml';
|
|
318
|
+
} else if (isText(contentType)) {
|
|
319
|
+
body = stringifyOpenAPI(body, null, 2);
|
|
320
|
+
} else {
|
|
321
|
+
body = `JSON.stringify(${stringifyOpenAPI(body, null, 2)})`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { body, code, headers: headersCopy };
|
|
325
|
+
},
|
|
326
|
+
getPythonBody: (body: any, headers?: Record<string, string>) => {
|
|
327
|
+
if (!body || !headers) return;
|
|
328
|
+
let code = '';
|
|
329
|
+
const contentType: string = headers['Content-Type'] || '';
|
|
330
|
+
|
|
331
|
+
if (isFormData(contentType)) {
|
|
332
|
+
code += 'files = {\n';
|
|
333
|
+
if (isPlainObject(body)) {
|
|
334
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
335
|
+
code += `${indent(`"${key}": "${String(value)}",`, 4)}\n`;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
code += '}\n\n';
|
|
339
|
+
body = 'files';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (isPDF(contentType)) {
|
|
343
|
+
code += 'files = {\n';
|
|
344
|
+
code += `${indent(`"file": "${body}",`, 4)}\n`;
|
|
345
|
+
code += '}\n\n';
|
|
346
|
+
body = 'files';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { body, code, headers };
|
|
350
|
+
},
|
|
351
|
+
getHTTPBody: (body: any, headers?: Record<string, string>) => {
|
|
352
|
+
if (!body || !headers) return undefined;
|
|
353
|
+
|
|
354
|
+
const contentType: string = headers['Content-Type'] || '';
|
|
355
|
+
|
|
356
|
+
const typeHandlers = {
|
|
357
|
+
pdf: () => `${stringifyOpenAPI(body, null, 2)}`,
|
|
358
|
+
formUrlEncoded: () => {
|
|
359
|
+
const encoded = isPlainObject(body)
|
|
360
|
+
? Object.entries(body)
|
|
361
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
362
|
+
.join('&')
|
|
363
|
+
: String(body);
|
|
364
|
+
return `"${encoded}"`;
|
|
365
|
+
},
|
|
366
|
+
text: () => `"${String(body)}"`,
|
|
367
|
+
xmlOrCsv: () => `"${stringifyOpenAPI(body).replace(/"/g, '')}"`,
|
|
368
|
+
default: () => `${stringifyOpenAPI(body, null, 2)}`,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (isPDF(contentType)) return typeHandlers.pdf();
|
|
372
|
+
if (isFormUrlEncoded(contentType)) return typeHandlers.formUrlEncoded();
|
|
373
|
+
if (isText(contentType)) return typeHandlers.text();
|
|
374
|
+
if (isXML(contentType) || isCSV(contentType)) {
|
|
375
|
+
return typeHandlers.xmlOrCsv();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return typeHandlers.default();
|
|
379
|
+
},
|
|
380
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function isJSON(contentType?: string): boolean {
|
|
2
|
+
return contentType?.toLowerCase().includes('application/json') || false;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isXML(contentType?: string): boolean {
|
|
6
|
+
return contentType?.toLowerCase().includes('application/xml') || false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isGraphQL(contentType?: string): boolean {
|
|
10
|
+
return contentType?.toLowerCase().includes('application/graphql') || false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isCSV(contentType?: string): boolean {
|
|
14
|
+
return contentType?.toLowerCase().includes('text/csv') || false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isPDF(contentType?: string): boolean {
|
|
18
|
+
return contentType?.toLowerCase().includes('application/pdf') || false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isText(contentType?: string): boolean {
|
|
22
|
+
return contentType?.toLowerCase().includes('text/plain') || false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isFormUrlEncoded(contentType?: string): boolean {
|
|
26
|
+
return contentType?.toLowerCase().includes('application/x-www-form-urlencoded') || false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isFormData(contentType?: string): boolean {
|
|
30
|
+
return !!contentType && contentType.toLowerCase().includes('multipart/form-data');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isPlainObject(value: unknown): boolean {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
@@ -14,25 +14,29 @@ type GenerateSchemaExampleOptions = Pick<
|
|
|
14
14
|
*/
|
|
15
15
|
export function generateSchemaExample(
|
|
16
16
|
schema: OpenAPIV3.SchemaObject,
|
|
17
|
-
options?: GenerateSchemaExampleOptions
|
|
17
|
+
options?: GenerateSchemaExampleOptions
|
|
18
18
|
): JSONValue | undefined {
|
|
19
|
-
return getExampleFromSchema(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
return getExampleFromSchema(
|
|
20
|
+
schema,
|
|
21
|
+
{
|
|
22
|
+
emptyString: 'text',
|
|
23
|
+
variables: {
|
|
24
|
+
'date-time': new Date().toISOString(),
|
|
25
|
+
date: new Date().toISOString().split('T')[0],
|
|
26
|
+
email: 'name@gmail.com',
|
|
27
|
+
hostname: 'example.com',
|
|
28
|
+
ipv4: '0.0.0.0',
|
|
29
|
+
ipv6: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
|
30
|
+
uri: 'https://example.com',
|
|
31
|
+
uuid: '123e4567-e89b-12d3-a456-426614174000',
|
|
32
|
+
binary: 'binary',
|
|
33
|
+
byte: 'Ynl0ZXM=',
|
|
34
|
+
password: 'password',
|
|
35
|
+
},
|
|
36
|
+
...options,
|
|
33
37
|
},
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
3 // Max depth for circular references
|
|
39
|
+
);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
@@ -40,7 +44,7 @@ export function generateSchemaExample(
|
|
|
40
44
|
*/
|
|
41
45
|
export function generateMediaTypeExample(
|
|
42
46
|
mediaType: OpenAPIV3.MediaTypeObject,
|
|
43
|
-
options?: GenerateSchemaExampleOptions
|
|
47
|
+
options?: GenerateSchemaExampleOptions
|
|
44
48
|
): JSONValue | undefined {
|
|
45
49
|
if (mediaType.example) {
|
|
46
50
|
return mediaType.example;
|
package/src/json2xml.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ describe('getUrlFromServerState', () => {
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
expect(xml).toBe(
|
|
22
|
-
'<?xml version="1.0"?>\n<urls>\n\t<url>https://example.com</url>\n\t<url>https://example.com</url>\n</urls>\n'
|
|
22
|
+
'<?xml version="1.0"?>\n<urls>\n\t<url>https://example.com</url>\n\t<url>https://example.com</url>\n</urls>\n'
|
|
23
23
|
);
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
|
|
4
3
|
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
|
|
4
|
+
import { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
|
|
5
5
|
|
|
6
6
|
async function fetchFilesystem(url: string) {
|
|
7
7
|
const response = await fetch(url);
|
|
@@ -19,7 +19,7 @@ async function fetchFilesystem(url: string) {
|
|
|
19
19
|
describe('#resolveOpenAPIOperation', () => {
|
|
20
20
|
it('should resolve refs', async () => {
|
|
21
21
|
const filesystem = await fetchFilesystem(
|
|
22
|
-
'https://petstore3.swagger.io/api/v3/openapi.json'
|
|
22
|
+
'https://petstore3.swagger.io/api/v3/openapi.json'
|
|
23
23
|
);
|
|
24
24
|
const resolved = await resolveOpenAPIOperation(filesystem, { method: 'put', path: '/pet' });
|
|
25
25
|
|
|
@@ -49,7 +49,7 @@ describe('#resolveOpenAPIOperation', () => {
|
|
|
49
49
|
|
|
50
50
|
it('should support yaml', async () => {
|
|
51
51
|
const filesystem = await fetchFilesystem(
|
|
52
|
-
'https://petstore3.swagger.io/api/v3/openapi.yaml'
|
|
52
|
+
'https://petstore3.swagger.io/api/v3/openapi.yaml'
|
|
53
53
|
);
|
|
54
54
|
const resolved = await resolveOpenAPIOperation(filesystem, { method: 'put', path: '/pet' });
|
|
55
55
|
|
|
@@ -98,7 +98,7 @@ describe('#resolveOpenAPIOperation', () => {
|
|
|
98
98
|
|
|
99
99
|
it('should resolve to null if the method is not supported', async () => {
|
|
100
100
|
const filesystem = await fetchFilesystem(
|
|
101
|
-
'https://petstore3.swagger.io/api/v3/openapi.json'
|
|
101
|
+
'https://petstore3.swagger.io/api/v3/openapi.json'
|
|
102
102
|
);
|
|
103
103
|
const resolved = await resolveOpenAPIOperation(filesystem, {
|
|
104
104
|
method: 'dontexist',
|
|
@@ -144,7 +144,7 @@ describe('#resolveOpenAPIOperation', () => {
|
|
|
144
144
|
|
|
145
145
|
it('should resolve a ref with whitespace', async () => {
|
|
146
146
|
const filesystem = await fetchFilesystem(
|
|
147
|
-
' https://petstore3.swagger.io/api/v3/openapi.json'
|
|
147
|
+
' https://petstore3.swagger.io/api/v3/openapi.json'
|
|
148
148
|
);
|
|
149
149
|
const resolved = await resolveOpenAPIOperation(filesystem, {
|
|
150
150
|
method: 'put',
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fromJSON, toJSON } from 'flatted';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
type OpenAPIV3xDocument,
|
|
5
4
|
type Filesystem,
|
|
6
5
|
type OpenAPIV3,
|
|
7
6
|
type OpenAPIV3_1,
|
|
7
|
+
type OpenAPIV3xDocument,
|
|
8
8
|
dereference,
|
|
9
9
|
} from '@gitbook/openapi-parser';
|
|
10
|
-
import { OpenAPIOperationData } from './types';
|
|
10
|
+
import type { OpenAPIOperationData } from './types';
|
|
11
11
|
import { checkIsReference } from './utils';
|
|
12
12
|
|
|
13
13
|
export { toJSON, fromJSON };
|
|
@@ -20,7 +20,7 @@ export async function resolveOpenAPIOperation(
|
|
|
20
20
|
operationDescriptor: {
|
|
21
21
|
path: string;
|
|
22
22
|
method: string;
|
|
23
|
-
}
|
|
23
|
+
}
|
|
24
24
|
): Promise<OpenAPIOperationData | null> {
|
|
25
25
|
const { path, method } = operationDescriptor;
|
|
26
26
|
const schema = await memoDereferenceFilesystem(filesystem);
|
|
@@ -102,7 +102,7 @@ async function dereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3x
|
|
|
102
102
|
*/
|
|
103
103
|
function getPathObject(
|
|
104
104
|
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
|
|
105
|
-
path: string
|
|
105
|
+
path: string
|
|
106
106
|
): OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject | null {
|
|
107
107
|
if (schema.paths?.[path]) {
|
|
108
108
|
return schema.paths[path];
|
|
@@ -115,7 +115,7 @@ function getPathObject(
|
|
|
115
115
|
*/
|
|
116
116
|
function getPathObjectParameter(
|
|
117
117
|
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
|
|
118
|
-
path: string
|
|
118
|
+
path: string
|
|
119
119
|
):
|
|
120
120
|
| (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[]
|
|
121
121
|
| (OpenAPIV3.ParameterObject | OpenAPIV3_1.ReferenceObject)[]
|
|
@@ -133,7 +133,7 @@ function getPathObjectParameter(
|
|
|
133
133
|
function getOperationByPathAndMethod(
|
|
134
134
|
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
|
|
135
135
|
path: string,
|
|
136
|
-
method: string
|
|
136
|
+
method: string
|
|
137
137
|
): OpenAPIV3.OperationObject | null {
|
|
138
138
|
// Types are buffy for OpenAPIV3_1.OperationObject, so we use v3
|
|
139
139
|
const pathObject = getPathObject(schema, path);
|
package/src/stringifyOpenAPI.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stringify an OpenAPI object. Same API as JSON.stringify.
|
|
3
3
|
*/
|
|
4
|
-
export function stringifyOpenAPI(body: unknown,
|
|
5
|
-
return JSON.stringify(
|
|
4
|
+
export function stringifyOpenAPI(body: unknown, _?: null, indent?: number): string {
|
|
5
|
+
return JSON.stringify(
|
|
6
|
+
body,
|
|
7
|
+
(key, value) => {
|
|
8
|
+
// Ignore internal keys
|
|
9
|
+
if (key.startsWith('x-gitbook-')) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return value;
|
|
14
|
+
},
|
|
15
|
+
indent
|
|
16
|
+
);
|
|
6
17
|
}
|
package/src/types.ts
CHANGED
|
@@ -5,8 +5,18 @@ import type {
|
|
|
5
5
|
} from '@gitbook/openapi-parser';
|
|
6
6
|
|
|
7
7
|
export interface OpenAPIContextProps extends OpenAPIClientContext {
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Render a code block.
|
|
10
|
+
*/
|
|
11
|
+
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Render the heading of the operation.
|
|
14
|
+
*/
|
|
9
15
|
renderHeading: (props: { deprecated: boolean; title: string }) => React.ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Render the document of the operation.
|
|
18
|
+
*/
|
|
19
|
+
renderDocument: (props: { document: object }) => React.ReactNode;
|
|
10
20
|
|
|
11
21
|
/** Spec url for the Scalar Api Client */
|
|
12
22
|
specUrl: string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
3
|
+
import { getDefaultServerURL, interpolateServerURL } from './server';
|
|
4
|
+
|
|
5
|
+
describe('#interpolateServerURL', () => {
|
|
6
|
+
it('interpolates the server URL with the default values of the variables', () => {
|
|
7
|
+
const server: OpenAPIV3.ServerObject = {
|
|
8
|
+
url: 'https://{username}.example.com/{basePath}',
|
|
9
|
+
variables: {
|
|
10
|
+
username: { default: 'user' },
|
|
11
|
+
basePath: { default: 'v1' },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const result = interpolateServerURL(server);
|
|
15
|
+
expect(result).toBe('https://user.example.com/v1');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns the URL with placeholders if no variables are provided', () => {
|
|
19
|
+
const server: OpenAPIV3.ServerObject = {
|
|
20
|
+
url: 'https://{username}.example.com/{basePath}',
|
|
21
|
+
};
|
|
22
|
+
const result = interpolateServerURL(server);
|
|
23
|
+
expect(result).toBe('https://{username}.example.com/{basePath}');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns the URL with mixed placeholders and default values', () => {
|
|
27
|
+
const server: OpenAPIV3.ServerObject = {
|
|
28
|
+
url: 'https://{username}.example.com/{basePath}',
|
|
29
|
+
variables: {
|
|
30
|
+
basePath: { default: 'v1' },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const result = interpolateServerURL(server);
|
|
34
|
+
expect(result).toBe('https://{username}.example.com/v1');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('#getDefaultServerURL', () => {
|
|
39
|
+
it('returns the default server URL', () => {
|
|
40
|
+
const servers: OpenAPIV3.ServerObject[] = [
|
|
41
|
+
{
|
|
42
|
+
url: 'https://{username}.example.com/{basePath}',
|
|
43
|
+
variables: {
|
|
44
|
+
username: { default: 'user' },
|
|
45
|
+
basePath: { default: 'v1' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
const result = getDefaultServerURL(servers);
|
|
50
|
+
expect(result).toBe('https://user.example.com/v1');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns empty string if no servers are provided', () => {
|
|
54
|
+
const servers: OpenAPIV3.ServerObject[] = [];
|
|
55
|
+
const result = getDefaultServerURL(servers);
|
|
56
|
+
expect(result).toBe('');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get the default URL for the server.
|
|
5
|
+
*/
|
|
6
|
+
export function getDefaultServerURL(servers: OpenAPIV3.ServerObject[]): string {
|
|
7
|
+
const server = servers[0];
|
|
8
|
+
if (!server) {
|
|
9
|
+
// Return empty string if no server is found to display nothing
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return interpolateServerURL(server);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Interpolate the server URL with the default values of the variables.
|
|
18
|
+
*/
|
|
19
|
+
export function interpolateServerURL(server: OpenAPIV3.ServerObject) {
|
|
20
|
+
const parts = parseServerURL(server?.url ?? '');
|
|
21
|
+
|
|
22
|
+
return parts
|
|
23
|
+
.map((part) => {
|
|
24
|
+
if (part.kind === 'text') {
|
|
25
|
+
return part.text;
|
|
26
|
+
}
|
|
27
|
+
return server.variables?.[part.name]?.default ?? `{${part.name}}`;
|
|
28
|
+
})
|
|
29
|
+
.join('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseServerURL(url: string) {
|
|
33
|
+
const parts = url.split(/{([^}]+)}/g);
|
|
34
|
+
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
|
35
|
+
for (let i = 0; i < parts.length; i++) {
|
|
36
|
+
const part = parts[i];
|
|
37
|
+
if (!part) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (i % 2 === 0) {
|
|
41
|
+
result.push({ kind: 'text', text: part });
|
|
42
|
+
} else {
|
|
43
|
+
result.push({ kind: 'variable', name: part });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|