@gitbook/react-openapi 0.7.1 → 1.0.0

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 (115) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/InteractiveSection.d.ts +4 -6
  3. package/dist/InteractiveSection.jsx +96 -0
  4. package/dist/Markdown.d.ts +1 -2
  5. package/dist/Markdown.jsx +5 -0
  6. package/dist/OpenAPICodeSample.d.ts +2 -4
  7. package/dist/OpenAPICodeSample.jsx +143 -0
  8. package/dist/OpenAPIDisclosure.d.ts +12 -0
  9. package/dist/OpenAPIDisclosure.jsx +32 -0
  10. package/dist/OpenAPIDisclosureGroup.d.ts +19 -0
  11. package/dist/OpenAPIDisclosureGroup.jsx +81 -0
  12. package/dist/OpenAPIOperation.d.ts +2 -4
  13. package/dist/OpenAPIOperation.jsx +51 -0
  14. package/dist/OpenAPIOperationContext.d.ts +16 -0
  15. package/dist/OpenAPIOperationContext.jsx +26 -0
  16. package/dist/OpenAPIPath.d.ts +8 -0
  17. package/dist/OpenAPIPath.jsx +54 -0
  18. package/dist/OpenAPIRequestBody.d.ts +3 -4
  19. package/dist/OpenAPIRequestBody.jsx +19 -0
  20. package/dist/OpenAPIResponse.d.ts +4 -4
  21. package/dist/OpenAPIResponse.jsx +49 -0
  22. package/dist/OpenAPIResponseExample.d.ts +2 -4
  23. package/dist/OpenAPIResponseExample.jsx +108 -0
  24. package/dist/OpenAPIResponses.d.ts +3 -4
  25. package/dist/OpenAPIResponses.jsx +36 -0
  26. package/dist/OpenAPISchema.d.ts +11 -8
  27. package/dist/OpenAPISchema.jsx +295 -0
  28. package/dist/OpenAPISchemaName.d.ts +12 -0
  29. package/dist/OpenAPISchemaName.jsx +15 -0
  30. package/dist/OpenAPISecurities.d.ts +2 -4
  31. package/dist/OpenAPISecurities.jsx +55 -0
  32. package/dist/OpenAPIServerURL.d.ts +2 -3
  33. package/dist/OpenAPIServerURL.jsx +67 -0
  34. package/dist/OpenAPIServerURLVariable.d.ts +2 -3
  35. package/dist/OpenAPIServerURLVariable.jsx +8 -0
  36. package/dist/OpenAPISpec.d.ts +3 -4
  37. package/dist/OpenAPISpec.jsx +91 -0
  38. package/dist/OpenAPITabs.d.ts +25 -0
  39. package/dist/OpenAPITabs.jsx +67 -0
  40. package/dist/ScalarApiButton.d.ts +3 -3
  41. package/dist/ScalarApiButton.jsx +51 -0
  42. package/dist/code-samples.d.ts +4 -0
  43. package/dist/code-samples.js +103 -38
  44. package/dist/fetchOpenAPIOperation.d.ts +9 -54
  45. package/dist/fetchOpenAPIOperation.js +178 -107
  46. package/dist/generateSchemaExample.d.ts +2 -2
  47. package/dist/generateSchemaExample.js +28 -100
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.js +2 -1
  50. package/dist/resolveOpenAPIOperation.d.ts +11 -0
  51. package/dist/resolveOpenAPIOperation.js +194 -0
  52. package/dist/stringifyOpenAPI.d.ts +4 -0
  53. package/dist/stringifyOpenAPI.js +6 -0
  54. package/dist/tsconfig.build.tsbuildinfo +1 -0
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/dist/types.d.ts +11 -12
  57. package/dist/utils.d.ts +6 -1
  58. package/dist/utils.js +15 -2
  59. package/package.json +11 -10
  60. package/src/InteractiveSection.tsx +119 -78
  61. package/src/Markdown.tsx +2 -3
  62. package/src/OpenAPICodeSample.tsx +35 -21
  63. package/src/OpenAPIDisclosure.tsx +50 -0
  64. package/src/OpenAPIDisclosureGroup.tsx +136 -0
  65. package/src/OpenAPIOperation.tsx +36 -42
  66. package/src/OpenAPIOperationContext.tsx +45 -0
  67. package/src/OpenAPIPath.tsx +65 -0
  68. package/src/OpenAPIRequestBody.tsx +3 -14
  69. package/src/OpenAPIResponse.tsx +39 -43
  70. package/src/OpenAPIResponseExample.tsx +89 -31
  71. package/src/OpenAPIResponses.tsx +51 -15
  72. package/src/OpenAPISchema.test.ts +1 -1
  73. package/src/OpenAPISchema.tsx +124 -92
  74. package/src/OpenAPISchemaName.tsx +27 -0
  75. package/src/OpenAPISecurities.tsx +45 -24
  76. package/src/OpenAPIServerURL.tsx +17 -10
  77. package/src/OpenAPIServerURLVariable.tsx +2 -4
  78. package/src/OpenAPISpec.tsx +56 -53
  79. package/src/OpenAPITabs.tsx +113 -0
  80. package/src/ScalarApiButton.tsx +84 -7
  81. package/src/code-samples.test.ts +51 -0
  82. package/src/code-samples.ts +95 -31
  83. package/src/generateSchemaExample.ts +25 -151
  84. package/src/index.ts +3 -2
  85. package/src/resolveOpenAPIOperation.test.ts +177 -0
  86. package/src/resolveOpenAPIOperation.ts +163 -0
  87. package/src/stringifyOpenAPI.ts +6 -0
  88. package/src/types.ts +17 -10
  89. package/src/utils.ts +17 -2
  90. package/dist/InteractiveSection.js +0 -47
  91. package/dist/Markdown.js +0 -6
  92. package/dist/OpenAPICodeSample.js +0 -110
  93. package/dist/OpenAPIOperation.js +0 -38
  94. package/dist/OpenAPIRequestBody.js +0 -18
  95. package/dist/OpenAPIResponse.js +0 -32
  96. package/dist/OpenAPIResponseExample.js +0 -54
  97. package/dist/OpenAPIResponses.js +0 -18
  98. package/dist/OpenAPISchema.js +0 -235
  99. package/dist/OpenAPISchema.test.d.ts +0 -1
  100. package/dist/OpenAPISchema.test.js +0 -91
  101. package/dist/OpenAPISecurities.js +0 -42
  102. package/dist/OpenAPIServerURL.js +0 -51
  103. package/dist/OpenAPIServerURLVariable.js +0 -10
  104. package/dist/OpenAPISpec.js +0 -70
  105. package/dist/ScalarApiButton.js +0 -14
  106. package/dist/fetchOpenAPIOperation.test.d.ts +0 -1
  107. package/dist/fetchOpenAPIOperation.test.js +0 -152
  108. package/dist/resolveOpenAPIPath.d.ts +0 -7
  109. package/dist/resolveOpenAPIPath.js +0 -112
  110. package/dist/resolveOpenAPIPath.test.d.ts +0 -1
  111. package/dist/resolveOpenAPIPath.test.js +0 -39
  112. package/src/fetchOpenAPIOperation.test.ts +0 -185
  113. package/src/fetchOpenAPIOperation.ts +0 -230
  114. package/src/resolveOpenAPIPath.test.ts +0 -60
  115. package/src/resolveOpenAPIPath.ts +0 -145
@@ -1,16 +1,14 @@
1
1
  'use client';
2
2
 
3
- import * as React from 'react';
4
- import { OpenAPIV3 } from 'openapi-types';
3
+ import type { OpenAPI } from '@gitbook/openapi-parser';
5
4
 
6
- import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
7
5
  import { InteractiveSection } from './InteractiveSection';
8
6
  import { OpenAPIRequestBody } from './OpenAPIRequestBody';
9
7
  import { OpenAPIResponses } from './OpenAPIResponses';
10
8
  import { OpenAPISchemaProperties } from './OpenAPISchema';
11
9
  import { OpenAPISecurities } from './OpenAPISecurities';
12
- import { OpenAPIClientContext } from './types';
13
- import { noReference } from './utils';
10
+ import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
11
+ import { noReference, resolveDescription } from './utils';
14
12
 
15
13
  /**
16
14
  * Client component to render the spec for the request and response.
@@ -18,13 +16,13 @@ import { noReference } from './utils';
18
16
  * We use a client component as rendering recursive JSON schema in the server is expensive
19
17
  * (the entire schema is rendered at once, while the client component only renders the visible part)
20
18
  */
21
- export function OpenAPISpec(props: { rawData: any; context: OpenAPIClientContext }) {
22
- const { rawData, context } = props;
19
+ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAPIClientContext }) {
20
+ const { data, context } = props;
23
21
 
24
- const parsedData = fromJSON(rawData) as OpenAPIOperationData;
25
- const { operation, securities } = parsedData;
22
+ const { operation, securities } = data;
26
23
 
27
- const parameterGroups = groupParameters((operation.parameters || []).map(noReference));
24
+ const parameters = operation.parameters ?? [];
25
+ const parameterGroups = groupParameters(parameters);
28
26
 
29
27
  return (
30
28
  <>
@@ -32,32 +30,35 @@ export function OpenAPISpec(props: { rawData: any; context: OpenAPIClientContext
32
30
  <OpenAPISecurities securities={securities} context={context} />
33
31
  ) : null}
34
32
 
35
- {parameterGroups.map((group) => (
36
- <InteractiveSection
37
- key={group.key}
38
- className="openapi-parameters"
39
- toggeable
40
- toggleOpenIcon={context.icons.chevronRight}
41
- toggleCloseIcon={context.icons.chevronDown}
42
- header={group.label}
43
- defaultOpened={group.key === 'path' || context.defaultInteractiveOpened}
44
- >
45
- <OpenAPISchemaProperties
46
- properties={group.parameters.map((parameter) => ({
47
- propertyName: parameter.name,
48
- schema: {
49
- // Description of the parameter is defined at the parameter level
50
- // we use display it if the schema doesn't override it
51
- description: parameter.description,
52
- example: parameter.example,
53
- ...(noReference(parameter.schema) ?? {}),
54
- },
55
- required: parameter.required,
56
- }))}
57
- context={context}
58
- />
59
- </InteractiveSection>
60
- ))}
33
+ {parameterGroups.map((group) => {
34
+ return (
35
+ <InteractiveSection
36
+ key={group.key}
37
+ className="openapi-parameters"
38
+ header={group.label}
39
+ >
40
+ <OpenAPISchemaProperties
41
+ properties={group.parameters.map((parameter) => {
42
+ const description = resolveDescription(parameter);
43
+ return {
44
+ propertyName: parameter.name,
45
+ schema: {
46
+ // Description of the parameter is defined at the parameter level
47
+ // we use display it if the schema doesn't override it
48
+ description: description,
49
+ example: parameter.example,
50
+ // Deprecated can be defined at the parameter level
51
+ deprecated: parameter.deprecated,
52
+ ...(noReference(parameter.schema) ?? {}),
53
+ },
54
+ required: parameter.required,
55
+ };
56
+ })}
57
+ context={context}
58
+ />
59
+ </InteractiveSection>
60
+ );
61
+ })}
61
62
 
62
63
  {operation.requestBody ? (
63
64
  <OpenAPIRequestBody
@@ -72,33 +73,35 @@ export function OpenAPISpec(props: { rawData: any; context: OpenAPIClientContext
72
73
  );
73
74
  }
74
75
 
75
- function groupParameters(parameters: OpenAPIV3.ParameterObject[]): Array<{
76
+ function groupParameters(parameters: OpenAPI.Parameters): Array<{
76
77
  key: string;
77
78
  label: string;
78
- parameters: OpenAPIV3.ParameterObject[];
79
+ parameters: OpenAPI.Parameters;
79
80
  }> {
80
81
  const sorted = ['path', 'query', 'header'];
81
82
 
82
83
  const groups: Array<{
83
84
  key: string;
84
85
  label: string;
85
- parameters: OpenAPIV3.ParameterObject[];
86
+ parameters: OpenAPI.Parameters;
86
87
  }> = [];
87
88
 
88
- parameters.forEach((parameter) => {
89
- const key = parameter.in;
90
- const label = getParameterGroupName(parameter.in);
91
- const group = groups.find((group) => group.key === key);
92
- if (group) {
93
- group.parameters.push(parameter);
94
- } else {
95
- groups.push({
96
- key,
97
- label,
98
- parameters: [parameter],
99
- });
100
- }
101
- });
89
+ parameters
90
+ .filter((parameter) => parameter.in)
91
+ .forEach((parameter) => {
92
+ const key = parameter.in;
93
+ const label = getParameterGroupName(parameter.in);
94
+ const group = groups.find((group) => group.key === key);
95
+ if (group) {
96
+ group.parameters.push(parameter);
97
+ } else {
98
+ groups.push({
99
+ key,
100
+ label,
101
+ parameters: [parameter],
102
+ });
103
+ }
104
+ });
102
105
 
103
106
  groups.sort((a, b) => sorted.indexOf(a.key) - sorted.indexOf(b.key));
104
107
 
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useMemo, useState } from 'react';
4
+ import { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components';
5
+ import { Markdown } from './Markdown';
6
+
7
+ export type Tab = {
8
+ key: Key;
9
+ label: string;
10
+ body: React.ReactNode;
11
+ description?: string;
12
+ };
13
+
14
+ type OpenAPITabsContextData = {
15
+ items: Tab[];
16
+ selectedKey: Key;
17
+ setSelectedKey: (key: Key) => void;
18
+ };
19
+
20
+ const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
21
+
22
+ function useOpenAPITabsContext() {
23
+ const context = useContext(OpenAPITabsContext);
24
+ if (!context) {
25
+ throw new Error('OpenAPITabsContext is missing');
26
+ }
27
+ return context;
28
+ }
29
+
30
+ /**
31
+ * The OpenAPI Tabs wrapper component.
32
+ */
33
+ export function OpenAPITabs(props: React.PropsWithChildren<TabsProps & { items: Tab[] }>) {
34
+ const { children, items } = props;
35
+
36
+ const [selectedKey, setSelectedKey] = useState(() => {
37
+ const firstItem = items[0];
38
+ if (!firstItem) {
39
+ throw new Error('OpenAPITabs: at least one tab is required');
40
+ }
41
+ return firstItem.key;
42
+ });
43
+
44
+ const contextValue = { items, selectedKey, setSelectedKey };
45
+
46
+ return (
47
+ <OpenAPITabsContext.Provider value={contextValue}>
48
+ <Tabs
49
+ className="openapi-tabs"
50
+ onSelectionChange={setSelectedKey}
51
+ selectedKey={selectedKey}
52
+ >
53
+ {children}
54
+ </Tabs>
55
+ </OpenAPITabsContext.Provider>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * The OpenAPI Tabs list component.
61
+ * This component should be used as a child of the OpenAPITabs component.
62
+ * It renders the list of tabs.
63
+ */
64
+ export function OpenAPITabsList() {
65
+ const { items } = useOpenAPITabsContext();
66
+
67
+ return (
68
+ <TabList className="openapi-tabs-list">
69
+ {items.map((tab) => (
70
+ <Tab
71
+ style={({ isFocusVisible }) => ({
72
+ outline: isFocusVisible
73
+ ? '2px solid rgb(var(--primary-color-500)/0.4)'
74
+ : 'none',
75
+ })}
76
+ className="openapi-tabs-tab"
77
+ key={`Tab-${tab.key}`}
78
+ id={tab.key}
79
+ >
80
+ {tab.label}
81
+ </Tab>
82
+ ))}
83
+ </TabList>
84
+ );
85
+ }
86
+
87
+ /**
88
+ * The OpenAPI Tabs panels component.
89
+ * This component should be used as a child of the OpenAPITabs component.
90
+ * It renders the content of the selected tab.
91
+ */
92
+ export function OpenAPITabsPanels() {
93
+ const { selectedKey, items } = useOpenAPITabsContext();
94
+
95
+ const tab = useMemo(() => items.find((tab) => tab.key === selectedKey), [items, selectedKey]);
96
+
97
+ if (!tab) {
98
+ return null;
99
+ }
100
+
101
+ return (
102
+ <TabPanel
103
+ key={`TabPanel-${tab.key}`}
104
+ id={tab.key.toString()}
105
+ className="openapi-tabs-panel"
106
+ >
107
+ {tab.body}
108
+ {tab.description ? (
109
+ <Markdown source={tab.description} className="openapi-tabs-footer" />
110
+ ) : null}
111
+ </TabPanel>
112
+ );
113
+ }
@@ -1,19 +1,34 @@
1
1
  'use client';
2
2
 
3
- import { useApiClientModal } from '@scalar/api-client-react';
4
- import React from 'react';
3
+ import { ApiClientModalProvider, useApiClientModal } from '@scalar/api-client-react';
4
+ import { useEffect, useImperativeHandle, useRef, useState } from 'react';
5
+ import { createPortal } from 'react-dom';
6
+
7
+ import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
8
+ import { useEventCallback } from 'usehooks-ts';
5
9
 
6
10
  /**
7
11
  * Button which launches the Scalar API Client
8
12
  */
9
- export function ScalarApiButton({ method, path }: { method: string; path: string }) {
10
- const client = useApiClientModal();
11
-
13
+ export function ScalarApiButton({
14
+ method,
15
+ path,
16
+ specUrl,
17
+ }: {
18
+ method: string;
19
+ path: string;
20
+ specUrl: string;
21
+ }) {
22
+ const [isOpen, setIsOpen] = useState(false);
23
+ const controllerRef = useRef<ScalarModalControllerRef>(null);
12
24
  return (
13
25
  <div className="scalar scalar-activate">
14
26
  <button
15
- className="scalar-activate-button"
16
- onClick={() => client?.open({ method, path })}
27
+ className="scalar-activate-button button"
28
+ onClick={() => {
29
+ controllerRef.current?.openClient?.();
30
+ setIsOpen(true);
31
+ }}
17
32
  >
18
33
  <svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
19
34
  <path
@@ -24,6 +39,68 @@ export function ScalarApiButton({ method, path }: { method: string; path: string
24
39
  </svg>
25
40
  Test it
26
41
  </button>
42
+
43
+ {isOpen &&
44
+ createPortal(
45
+ <ScalarModal
46
+ controllerRef={controllerRef}
47
+ method={method}
48
+ path={path}
49
+ specUrl={specUrl}
50
+ />,
51
+ document.body,
52
+ )}
27
53
  </div>
28
54
  );
29
55
  }
56
+
57
+ function ScalarModal(props: {
58
+ method: string;
59
+ path: string;
60
+ specUrl: string;
61
+ controllerRef: React.Ref<ScalarModalControllerRef>;
62
+ }) {
63
+ return (
64
+ <ApiClientModalProvider
65
+ configuration={{ spec: { url: props.specUrl } }}
66
+ initialRequest={{ path: props.path, method: props.method }}
67
+ >
68
+ <ScalarModalController
69
+ method={props.method}
70
+ path={props.path}
71
+ controllerRef={props.controllerRef}
72
+ />
73
+ </ApiClientModalProvider>
74
+ );
75
+ }
76
+
77
+ type ScalarModalControllerRef = {
78
+ openClient: (() => void) | undefined;
79
+ };
80
+
81
+ function ScalarModalController(props: {
82
+ method: string;
83
+ path: string;
84
+ controllerRef: React.Ref<ScalarModalControllerRef>;
85
+ }) {
86
+ const client = useApiClientModal();
87
+ const openClient = client?.open;
88
+ useImperativeHandle(
89
+ props.controllerRef,
90
+ () => ({ openClient: openClient ? () => openClient() : undefined }),
91
+ [openClient],
92
+ );
93
+
94
+ // Open the client when the component is mounted.
95
+ const { onOpenClient } = useOpenAPIOperationContext();
96
+ const trackOpening = useEventCallback(() => {
97
+ onOpenClient({ method: props.method, path: props.path });
98
+ });
99
+ useEffect(() => {
100
+ if (openClient) {
101
+ openClient();
102
+ trackOpening();
103
+ }
104
+ }, [openClient]);
105
+ return null;
106
+ }
@@ -0,0 +1,51 @@
1
+ import { it, expect } from 'bun:test';
2
+ import { parseHostAndPath } from './code-samples';
3
+
4
+ it('should parse host and path on url strings properly', () => {
5
+ const testUrls = [
6
+ '//example.com/path',
7
+ '//sub.example.com',
8
+ '//example:8080/v1/test',
9
+ 'ftp://domain.com',
10
+ '//example.com/com.example',
11
+ 'https://example.com/path.com/another.com',
12
+ 'example.com/firstPath/secondPath',
13
+ ];
14
+
15
+ expect(testUrls.map(parseHostAndPath)).toEqual([
16
+ {
17
+ host: 'example.com',
18
+ path: '/path',
19
+ },
20
+
21
+ {
22
+ host: 'sub.example.com',
23
+ path: '/',
24
+ },
25
+
26
+ {
27
+ host: 'example:8080',
28
+ path: '/v1/test',
29
+ },
30
+
31
+ {
32
+ host: 'domain.com',
33
+ path: '/',
34
+ },
35
+
36
+ {
37
+ host: 'example.com',
38
+ path: '/com.example',
39
+ },
40
+
41
+ {
42
+ host: 'example.com',
43
+ path: '/path.com/another.com',
44
+ },
45
+
46
+ {
47
+ host: 'example.com',
48
+ path: '/firstPath/secondPath',
49
+ },
50
+ ]);
51
+ });
@@ -1,3 +1,5 @@
1
+ import { stringifyOpenAPI } from './stringifyOpenAPI';
2
+
1
3
  export interface CodeSampleInput {
2
4
  method: string;
3
5
  url: string;
@@ -14,55 +16,55 @@ interface CodeSampleGenerator {
14
16
 
15
17
  export const codeSampleGenerators: CodeSampleGenerator[] = [
16
18
  {
17
- id: 'javascript',
18
- label: 'JavaScript',
19
- syntax: 'javascript',
19
+ id: 'curl',
20
+ label: 'cURL',
21
+ syntax: 'bash',
20
22
  generate: ({ method, url, headers, body }) => {
21
- let code = '';
23
+ const separator = ' \\\n';
22
24
 
23
- code += `const response = await fetch('${url}', {
24
- method: '${method.toUpperCase()}',\n`;
25
+ const lines: string[] = ['curl -L'];
25
26
 
26
- if (headers) {
27
- code += indent(`headers: ${JSON.stringify(headers, null, 2)},\n`, 4);
27
+ if (method.toUpperCase() !== 'GET') {
28
+ lines.push(`--request ${method.toUpperCase()}`);
28
29
  }
29
30
 
30
- if (body) {
31
- code += indent(`body: JSON.stringify(${JSON.stringify(body, null, 2)}),\n`, 4);
31
+ lines.push(`--url '${url}'`);
32
+
33
+ if (headers) {
34
+ Object.entries(headers).forEach(([key, value]) => {
35
+ lines.push(`--header '${key}: ${value}'`);
36
+ });
32
37
  }
33
38
 
34
- code += `});\n`;
35
- code += `const data = await response.json();`;
39
+ if (body && Object.keys(body).length > 0) {
40
+ lines.push(`--data '${stringifyOpenAPI(body)}'`);
41
+ }
36
42
 
37
- return code;
43
+ return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator);
38
44
  },
39
45
  },
40
46
  {
41
- id: 'curl',
42
- label: 'Curl',
43
- syntax: 'bash',
47
+ id: 'javascript',
48
+ label: 'JavaScript',
49
+ syntax: 'javascript',
44
50
  generate: ({ method, url, headers, body }) => {
45
- const separator = ' \\\n';
46
-
47
- const lines: string[] = ['curl -L'];
51
+ let code = '';
48
52
 
49
- if (method.toUpperCase() !== 'GET') {
50
- lines.push(`-X ${method.toUpperCase()}`);
51
- }
53
+ code += `const response = await fetch('${url}', {
54
+ method: '${method.toUpperCase()}',\n`;
52
55
 
53
56
  if (headers) {
54
- Object.entries(headers).forEach(([key, value]) => {
55
- lines.push(`-H '${key}: ${value}'`);
56
- });
57
+ code += indent(`headers: ${stringifyOpenAPI(headers, null, 2)},\n`, 4);
57
58
  }
58
59
 
59
- lines.push(`'${url}'`);
60
-
61
60
  if (body) {
62
- lines.push(`-d '${JSON.stringify(body)}'`);
61
+ code += indent(`body: JSON.stringify(${stringifyOpenAPI(body, null, 2)}),\n`, 4);
63
62
  }
64
63
 
65
- return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator);
64
+ code += `});\n`;
65
+ code += `const data = await response.json();`;
66
+
67
+ return code;
66
68
  },
67
69
  },
68
70
  {
@@ -74,16 +76,56 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
74
76
  code += `response = requests.${method.toLowerCase()}(\n`;
75
77
  code += indent(`"${url}",\n`, 4);
76
78
  if (headers) {
77
- code += indent(`headers=${JSON.stringify(headers)},\n`, 4);
79
+ code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4);
78
80
  }
79
81
  if (body) {
80
- code += indent(`json=${JSON.stringify(body)}\n`, 4);
82
+ code += indent(`json=${stringifyOpenAPI(body)}\n`, 4);
81
83
  }
82
84
  code += ')\n';
83
85
  code += `data = response.json()`;
84
86
  return code;
85
87
  },
86
88
  },
89
+ {
90
+ id: 'http',
91
+ label: 'HTTP',
92
+ syntax: 'bash',
93
+ generate: ({ method, url, headers = {}, body }: CodeSampleInput) => {
94
+ const { host, path } = parseHostAndPath(url);
95
+
96
+ if (body) {
97
+ // if we had a body add a content length header
98
+ const bodyContent = body ? stringifyOpenAPI(body) : '';
99
+ // handle unicode chars with a text encoder
100
+ const encoder = new TextEncoder();
101
+
102
+ headers = {
103
+ ...headers,
104
+ 'Content-Length': encoder.encode(bodyContent).length.toString(),
105
+ };
106
+ }
107
+
108
+ if (!headers.hasOwnProperty('Accept')) {
109
+ headers.Accept = '*/*';
110
+ }
111
+
112
+ const headerString = headers
113
+ ? Object.entries(headers)
114
+ .map(([key, value]) =>
115
+ key.toLowerCase() !== 'host' ? `${key}: ${value}` : ``,
116
+ )
117
+ .join('\n') + '\n'
118
+ : '';
119
+
120
+ const bodyString = body ? `\n${stringifyOpenAPI(body, null, 2)}` : '';
121
+
122
+ const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1
123
+ Host: ${host}
124
+ ${headerString}${bodyString}`;
125
+
126
+ return httpRequest;
127
+ },
128
+ },
87
129
  ];
88
130
 
89
131
  function indent(code: string, spaces: number) {
@@ -93,3 +135,25 @@ function indent(code: string, spaces: number) {
93
135
  .map((line) => (line ? indent + line : ''))
94
136
  .join('\n');
95
137
  }
138
+
139
+ export function parseHostAndPath(url: string) {
140
+ try {
141
+ const urlObj = new URL(url);
142
+ const path = urlObj.pathname || '/';
143
+ return { host: urlObj.host, path };
144
+ } catch (e) {
145
+ // If the URL was invalid do our best to parse the URL.
146
+ // Check for the protocol part and pull it off to grab the host
147
+ const splitted = url.split('//');
148
+ const fullUrl = splitted[1] ? splitted[1] : url;
149
+
150
+ // separate paths from the first element (host)
151
+ const parts = fullUrl.split('/');
152
+ // pull off the host (mutates)
153
+ const host = parts.shift();
154
+ // add a leading slash and join the paths again
155
+ const path = '/' + parts.join('/');
156
+
157
+ return { host, path };
158
+ }
159
+ }