@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/OpenAPICodeSample.jsx +17 -16
  3. package/dist/OpenAPIDisclosure.d.ts +2 -1
  4. package/dist/OpenAPIDisclosure.jsx +1 -1
  5. package/dist/OpenAPIDisclosureGroup.d.ts +1 -1
  6. package/dist/OpenAPIDisclosureGroup.jsx +2 -2
  7. package/dist/OpenAPIOperation.jsx +21 -7
  8. package/dist/OpenAPIPath.d.ts +3 -2
  9. package/dist/OpenAPIPath.jsx +4 -15
  10. package/dist/OpenAPIRequestBody.jsx +1 -1
  11. package/dist/OpenAPIResponse.jsx +1 -1
  12. package/dist/OpenAPIResponseExample.jsx +6 -5
  13. package/dist/OpenAPIResponses.d.ts +1 -1
  14. package/dist/OpenAPIResponses.jsx +2 -2
  15. package/dist/OpenAPISchema.d.ts +5 -1
  16. package/dist/OpenAPISchema.jsx +72 -61
  17. package/dist/OpenAPISchemaName.d.ts +5 -3
  18. package/dist/OpenAPISchemaName.jsx +25 -4
  19. package/dist/OpenAPISecurities.jsx +2 -2
  20. package/dist/OpenAPITabs.d.ts +3 -3
  21. package/dist/OpenAPITabs.jsx +17 -14
  22. package/dist/ScalarApiButton.jsx +1 -1
  23. package/dist/code-samples.js +239 -17
  24. package/dist/contentTypeChecks.d.ts +9 -0
  25. package/dist/contentTypeChecks.js +27 -0
  26. package/dist/generateSchemaExample.js +2 -1
  27. package/dist/resolveOpenAPIOperation.d.ts +3 -3
  28. package/dist/resolveOpenAPIOperation.js +1 -1
  29. package/dist/stringifyOpenAPI.d.ts +1 -1
  30. package/dist/stringifyOpenAPI.js +8 -2
  31. package/dist/tsconfig.build.tsbuildinfo +1 -1
  32. package/dist/types.d.ts +14 -2
  33. package/dist/util/server.d.ts +9 -0
  34. package/dist/util/server.js +44 -0
  35. package/dist/utils.d.ts +2 -2
  36. package/dist/utils.js +7 -6
  37. package/package.json +3 -8
  38. package/src/InteractiveSection.tsx +4 -4
  39. package/src/OpenAPICodeSample.tsx +20 -19
  40. package/src/OpenAPIDisclosure.tsx +4 -3
  41. package/src/OpenAPIDisclosureGroup.tsx +5 -5
  42. package/src/OpenAPIOperation.tsx +32 -10
  43. package/src/OpenAPIOperationContext.tsx +1 -1
  44. package/src/OpenAPIPath.tsx +11 -10
  45. package/src/OpenAPIRequestBody.tsx +2 -2
  46. package/src/OpenAPIResponse.tsx +3 -3
  47. package/src/OpenAPIResponseExample.tsx +7 -6
  48. package/src/OpenAPIResponses.tsx +4 -4
  49. package/src/OpenAPISchema.test.ts +5 -5
  50. package/src/OpenAPISchema.tsx +134 -73
  51. package/src/OpenAPISchemaName.tsx +40 -7
  52. package/src/OpenAPISecurities.tsx +3 -3
  53. package/src/OpenAPITabs.tsx +23 -17
  54. package/src/ScalarApiButton.tsx +3 -3
  55. package/src/code-samples.test.ts +594 -2
  56. package/src/code-samples.ts +238 -17
  57. package/src/contentTypeChecks.ts +35 -0
  58. package/src/generateSchemaExample.ts +22 -18
  59. package/src/json2xml.test.ts +1 -1
  60. package/src/resolveOpenAPIOperation.test.ts +6 -6
  61. package/src/resolveOpenAPIOperation.ts +7 -7
  62. package/src/stringifyOpenAPI.ts +13 -2
  63. package/src/types.ts +11 -1
  64. package/src/util/server.test.ts +58 -0
  65. package/src/util/server.ts +47 -0
  66. package/src/utils.ts +9 -5
  67. package/dist/OpenAPIServerURL.d.ts +0 -11
  68. package/dist/OpenAPIServerURL.jsx +0 -67
  69. package/dist/OpenAPIServerURLVariable.d.ts +0 -8
  70. package/dist/OpenAPIServerURLVariable.jsx +0 -8
  71. package/src/OpenAPIServerURL.tsx +0 -73
  72. package/src/OpenAPIServerURLVariable.tsx +0 -14
@@ -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 (headers) {
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 && Object.keys(body).length > 0) {
40
- lines.push(`--data '${stringifyOpenAPI(body)}'`);
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: JSON.stringify(${stringifyOpenAPI(body, null, 2)}),\n`, 4);
95
+ code += indent(`body: ${body}\n`, 4);
62
96
  }
63
97
 
64
- code += `});\n`;
65
- code += `const data = await response.json();`;
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
- if (headers) {
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
- code += indent(`json=${stringifyOpenAPI(body)}\n`, 4);
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
- code += ')\n';
85
- code += `data = response.json()`;
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') + '\n'
176
+ .join('\n')}\n`
118
177
  : '';
119
178
 
120
- const bodyString = body ? `\n${stringifyOpenAPI(body, null, 2)}` : '';
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 (e) {
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 = '/' + parts.join('/');
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(schema, {
20
- emptyString: 'text',
21
- variables: {
22
- 'date-time': new Date().toISOString(),
23
- date: new Date().toISOString().split('T')[0],
24
- email: 'name@gmail.com',
25
- hostname: 'example.com',
26
- ipv4: '0.0.0.0',
27
- ipv6: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
28
- uri: 'https://example.com',
29
- uuid: '123e4567-e89b-12d3-a456-426614174000',
30
- binary: 'binary',
31
- byte: 'Ynl0ZXM=',
32
- password: 'password',
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
- ...options,
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;
@@ -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 { it, expect, describe } from 'bun:test';
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 { toJSON, fromJSON } from 'flatted';
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);
@@ -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, transformer?: null, indent?: number): string {
5
- return JSON.stringify(body, transformer, indent);
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
- CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
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
+ }