@gitbook/react-openapi 1.0.1 → 1.0.3

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 (53) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/OpenAPICodeSample.jsx +11 -7
  3. package/dist/OpenAPIOperation.jsx +25 -8
  4. package/dist/OpenAPIResponse.jsx +16 -14
  5. package/dist/OpenAPIResponseExample.jsx +157 -47
  6. package/dist/OpenAPISchema.d.ts +2 -2
  7. package/dist/OpenAPISchema.jsx +50 -39
  8. package/dist/OpenAPISchemaName.d.ts +2 -1
  9. package/dist/OpenAPISchemaName.jsx +25 -4
  10. package/dist/OpenAPISpec.jsx +2 -26
  11. package/dist/OpenAPITabs.jsx +6 -2
  12. package/dist/code-samples.js +232 -10
  13. package/dist/contentTypeChecks.d.ts +9 -0
  14. package/dist/contentTypeChecks.js +27 -0
  15. package/dist/generateSchemaExample.d.ts +5 -6
  16. package/dist/generateSchemaExample.js +13 -8
  17. package/dist/json2xml.d.ts +4 -0
  18. package/dist/json2xml.js +7 -0
  19. package/dist/stringifyOpenAPI.d.ts +1 -1
  20. package/dist/stringifyOpenAPI.js +8 -2
  21. package/dist/tsconfig.build.tsbuildinfo +1 -1
  22. package/dist/types.d.ts +18 -2
  23. package/dist/util/server.d.ts +9 -0
  24. package/dist/{OpenAPIServerURL.jsx → util/server.js} +7 -28
  25. package/dist/utils.d.ts +27 -3
  26. package/dist/utils.js +75 -3
  27. package/package.json +3 -2
  28. package/src/OpenAPICodeSample.tsx +11 -7
  29. package/src/OpenAPIOperation.tsx +36 -11
  30. package/src/OpenAPIResponse.tsx +6 -12
  31. package/src/OpenAPIResponseExample.tsx +237 -69
  32. package/src/OpenAPISchema.tsx +81 -58
  33. package/src/OpenAPISchemaName.tsx +37 -5
  34. package/src/OpenAPISpec.tsx +2 -17
  35. package/src/OpenAPITabs.tsx +8 -2
  36. package/src/__snapshots__/json2xml.test.ts.snap +18 -0
  37. package/src/code-samples.test.ts +594 -2
  38. package/src/code-samples.ts +231 -10
  39. package/src/contentTypeChecks.ts +35 -0
  40. package/src/generateSchemaExample.ts +28 -22
  41. package/src/json2xml.test.ts +46 -0
  42. package/src/json2xml.ts +8 -0
  43. package/src/resolveOpenAPIOperation.test.ts +1 -1
  44. package/src/stringifyOpenAPI.ts +13 -2
  45. package/src/types.ts +12 -1
  46. package/src/util/server.test.ts +58 -0
  47. package/src/util/server.ts +48 -0
  48. package/src/utils.ts +86 -6
  49. package/dist/OpenAPIServerURL.d.ts +0 -11
  50. package/dist/OpenAPIServerURLVariable.d.ts +0 -8
  51. package/dist/OpenAPIServerURLVariable.jsx +0 -8
  52. package/src/OpenAPIServerURL.tsx +0 -73
  53. package/src/OpenAPIServerURLVariable.tsx +0 -14
@@ -1,3 +1,13 @@
1
+ import {
2
+ isFormData,
3
+ isPDF,
4
+ isFormUrlEncoded,
5
+ isText,
6
+ isXML,
7
+ isCSV,
8
+ isGraphQL,
9
+ isPlainObject,
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,18 +73,29 @@ 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`;
98
+ code += `});\n\n`;
65
99
  code += `const data = await response.json();`;
66
100
 
67
101
  return code;
@@ -73,15 +107,34 @@ 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';
136
+
137
+ code += ')\n\n';
85
138
  code += `data = response.json()`;
86
139
  return code;
87
140
  },
@@ -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(),
@@ -117,7 +176,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
117
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}
@@ -157,3 +216,165 @@ export function parseHostAndPath(url: string) {
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
+ }
@@ -3,32 +3,40 @@ import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
3
3
 
4
4
  type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
5
5
 
6
+ type ScalarGetExampleFromSchemaOptions = NonNullable<Parameters<typeof getExampleFromSchema>[1]>;
7
+ type GenerateSchemaExampleOptions = Pick<
8
+ ScalarGetExampleFromSchemaOptions,
9
+ 'xml' | 'omitEmptyAndOptionalProperties' | 'mode'
10
+ >;
11
+
6
12
  /**
7
13
  * Generate a JSON example from a schema
8
14
  */
9
15
  export function generateSchemaExample(
10
16
  schema: OpenAPIV3.SchemaObject,
11
- options: {
12
- onlyRequired?: boolean;
13
- } = {},
17
+ options?: GenerateSchemaExampleOptions,
14
18
  ): JSONValue | undefined {
15
- return getExampleFromSchema(schema, {
16
- emptyString: 'text',
17
- omitEmptyAndOptionalProperties: options.onlyRequired,
18
- variables: {
19
- 'date-time': new Date().toISOString(),
20
- date: new Date().toISOString().split('T')[0],
21
- email: 'name@gmail.com',
22
- hostname: 'example.com',
23
- ipv4: '0.0.0.0',
24
- ipv6: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
25
- uri: 'https://example.com',
26
- uuid: '123e4567-e89b-12d3-a456-426614174000',
27
- binary: 'binary',
28
- byte: 'Ynl0ZXM=',
29
- 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,
30
37
  },
31
- });
38
+ 3, // Max depth for circular references
39
+ );
32
40
  }
33
41
 
34
42
  /**
@@ -36,9 +44,7 @@ export function generateSchemaExample(
36
44
  */
37
45
  export function generateMediaTypeExample(
38
46
  mediaType: OpenAPIV3.MediaTypeObject,
39
- options: {
40
- onlyRequired?: boolean;
41
- } = {},
47
+ options?: GenerateSchemaExampleOptions,
42
48
  ): JSONValue | undefined {
43
49
  if (mediaType.example) {
44
50
  return mediaType.example;
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { json2xml } from './json2xml';
4
+
5
+ describe('getUrlFromServerState', () => {
6
+ it('transforms JSON to xml', () => {
7
+ const xml = json2xml({
8
+ foo: 'bar',
9
+ });
10
+
11
+ expect(xml).toBe('<?xml version="1.0"?>\n<foo>bar</foo>\n');
12
+ });
13
+
14
+ it('wraps array items', () => {
15
+ const xml = json2xml({
16
+ urls: {
17
+ url: ['https://example.com', 'https://example.com'],
18
+ },
19
+ });
20
+
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',
23
+ );
24
+ });
25
+
26
+ it('indents correctly', () => {
27
+ const xml = json2xml({
28
+ id: 10,
29
+ name: 'doggie',
30
+ category: {
31
+ id: 1,
32
+ name: 'Dogs',
33
+ },
34
+ photoUrls: ['string'],
35
+ tags: [
36
+ {
37
+ id: 0,
38
+ name: 'string',
39
+ },
40
+ ],
41
+ status: 'available',
42
+ });
43
+
44
+ expect(xml).toMatchSnapshot();
45
+ });
46
+ });
@@ -0,0 +1,8 @@
1
+ import { jsXml } from 'json-xml-parse';
2
+
3
+ /**
4
+ * This function converts an object to XML.
5
+ */
6
+ export function json2xml(data: Record<string, any>) {
7
+ return jsXml.toXmlString(data, { beautify: true });
8
+ }
@@ -9,7 +9,7 @@ async function fetchFilesystem(url: string) {
9
9
  const filesystem = await parseOpenAPI({ value: text, rootURL: url });
10
10
  const transformedFs = await traverse(filesystem, async (node) => {
11
11
  if ('description' in node && typeof node.description === 'string' && node.description) {
12
- node['x-description-html'] = node.description;
12
+ node['x-gitbook-description-html'] = node.description;
13
13
  }
14
14
  return node;
15
15
  });
@@ -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,7 +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
+ */
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;
9
20
 
10
21
  /** Spec url for the Scalar Api Client */
11
22
  specUrl: string;
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { interpolateServerURL, getDefaultServerURL } from './server';
3
+ import { OpenAPIV3 } from '@gitbook/openapi-parser';
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,48 @@
1
+ import { 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
+ } else {
27
+ return server.variables?.[part.name]?.default ?? `{${part.name}}`;
28
+ }
29
+ })
30
+ .join('');
31
+ }
32
+
33
+ function parseServerURL(url: string) {
34
+ const parts = url.split(/{([^}]+)}/g);
35
+ const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
36
+ for (let i = 0; i < parts.length; i++) {
37
+ const part = parts[i];
38
+ if (!part) {
39
+ continue;
40
+ }
41
+ if (i % 2 === 0) {
42
+ result.push({ kind: 'text', text: part });
43
+ } else {
44
+ result.push({ kind: 'variable', name: part });
45
+ }
46
+ }
47
+ return result;
48
+ }