@gitbook/react-openapi 0.7.1 → 1.0.1

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 (118) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/InteractiveSection.d.ts +4 -8
  3. package/dist/InteractiveSection.jsx +60 -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 +141 -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 +4 -5
  19. package/dist/OpenAPIRequestBody.jsx +22 -0
  20. package/dist/OpenAPIResponse.d.ts +4 -4
  21. package/dist/OpenAPIResponse.jsx +39 -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 +35 -0
  26. package/dist/OpenAPISchema.d.ts +11 -8
  27. package/dist/OpenAPISchema.jsx +285 -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 +26 -0
  39. package/dist/OpenAPITabs.jsx +103 -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/generateSchemaExample.d.ts +2 -2
  45. package/dist/generateSchemaExample.js +29 -102
  46. package/dist/index.d.ts +3 -2
  47. package/dist/index.js +2 -1
  48. package/dist/resolveOpenAPIOperation.d.ts +11 -0
  49. package/dist/resolveOpenAPIOperation.js +194 -0
  50. package/dist/stringifyOpenAPI.d.ts +4 -0
  51. package/dist/stringifyOpenAPI.js +6 -0
  52. package/dist/tsconfig.build.tsbuildinfo +1 -0
  53. package/dist/types.d.ts +11 -12
  54. package/dist/useSyncedTabsGlobalState.d.ts +1 -0
  55. package/dist/useSyncedTabsGlobalState.js +16 -0
  56. package/dist/utils.d.ts +6 -2
  57. package/dist/utils.js +13 -6
  58. package/package.json +12 -10
  59. package/src/InteractiveSection.tsx +90 -86
  60. package/src/Markdown.tsx +2 -3
  61. package/src/OpenAPICodeSample.tsx +43 -31
  62. package/src/OpenAPIDisclosure.tsx +50 -0
  63. package/src/OpenAPIDisclosureGroup.tsx +136 -0
  64. package/src/OpenAPIOperation.tsx +36 -42
  65. package/src/OpenAPIOperationContext.tsx +45 -0
  66. package/src/OpenAPIPath.tsx +65 -0
  67. package/src/OpenAPIRequestBody.tsx +10 -17
  68. package/src/OpenAPIResponse.tsx +27 -45
  69. package/src/OpenAPIResponseExample.tsx +89 -31
  70. package/src/OpenAPIResponses.tsx +48 -17
  71. package/src/OpenAPISchema.test.ts +1 -1
  72. package/src/OpenAPISchema.tsx +129 -108
  73. package/src/OpenAPISchemaName.tsx +27 -0
  74. package/src/OpenAPISecurities.tsx +45 -24
  75. package/src/OpenAPIServerURL.tsx +17 -10
  76. package/src/OpenAPIServerURLVariable.tsx +2 -4
  77. package/src/OpenAPISpec.tsx +58 -58
  78. package/src/OpenAPITabs.tsx +153 -0
  79. package/src/ScalarApiButton.tsx +84 -7
  80. package/src/code-samples.test.ts +51 -0
  81. package/src/code-samples.ts +95 -31
  82. package/src/generateSchemaExample.ts +26 -153
  83. package/src/index.ts +3 -2
  84. package/src/resolveOpenAPIOperation.test.ts +177 -0
  85. package/src/resolveOpenAPIOperation.ts +164 -0
  86. package/src/stringifyOpenAPI.ts +6 -0
  87. package/src/types.ts +17 -10
  88. package/src/useSyncedTabsGlobalState.ts +23 -0
  89. package/src/utils.ts +14 -7
  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.d.ts +0 -72
  107. package/dist/fetchOpenAPIOperation.js +0 -124
  108. package/dist/fetchOpenAPIOperation.test.d.ts +0 -1
  109. package/dist/fetchOpenAPIOperation.test.js +0 -152
  110. package/dist/resolveOpenAPIPath.d.ts +0 -7
  111. package/dist/resolveOpenAPIPath.js +0 -112
  112. package/dist/resolveOpenAPIPath.test.d.ts +0 -1
  113. package/dist/resolveOpenAPIPath.test.js +0 -39
  114. package/dist/tsconfig.tsbuildinfo +0 -1
  115. package/src/fetchOpenAPIOperation.test.ts +0 -185
  116. package/src/fetchOpenAPIOperation.ts +0 -230
  117. package/src/resolveOpenAPIPath.test.ts +0 -60
  118. 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 { 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,73 +30,75 @@ 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
+ ...(parameter.schema ?? {}),
53
+ },
54
+ required: parameter.required,
55
+ };
56
+ })}
57
+ context={context}
58
+ />
59
+ </InteractiveSection>
60
+ );
61
+ })}
61
62
 
62
63
  {operation.requestBody ? (
63
- <OpenAPIRequestBody
64
- requestBody={noReference(operation.requestBody)}
65
- context={context}
66
- />
64
+ <OpenAPIRequestBody requestBody={operation.requestBody} context={context} />
67
65
  ) : null}
68
66
  {operation.responses ? (
69
- <OpenAPIResponses responses={noReference(operation.responses)} context={context} />
67
+ <OpenAPIResponses responses={operation.responses} context={context} />
70
68
  ) : null}
71
69
  </>
72
70
  );
73
71
  }
74
72
 
75
- function groupParameters(parameters: OpenAPIV3.ParameterObject[]): Array<{
73
+ function groupParameters(parameters: OpenAPI.Parameters): Array<{
76
74
  key: string;
77
75
  label: string;
78
- parameters: OpenAPIV3.ParameterObject[];
76
+ parameters: OpenAPI.Parameters;
79
77
  }> {
80
78
  const sorted = ['path', 'query', 'header'];
81
79
 
82
80
  const groups: Array<{
83
81
  key: string;
84
82
  label: string;
85
- parameters: OpenAPIV3.ParameterObject[];
83
+ parameters: OpenAPI.Parameters;
86
84
  }> = [];
87
85
 
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
- });
86
+ parameters
87
+ .filter((parameter) => parameter.in)
88
+ .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
+ });
102
102
 
103
103
  groups.sort((a, b) => sorted.indexOf(a.key) - sorted.indexOf(b.key));
104
104
 
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
4
+ import { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components';
5
+ import { Markdown } from './Markdown';
6
+ import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState';
7
+ import { useIntersectionObserver } from 'usehooks-ts';
8
+
9
+ export type Tab = {
10
+ key: Key;
11
+ label: string;
12
+ body: React.ReactNode;
13
+ description?: string;
14
+ };
15
+
16
+ type OpenAPITabsContextData = {
17
+ items: Tab[];
18
+ selectedTab: Tab;
19
+ };
20
+
21
+ const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
22
+
23
+ function useOpenAPITabsContext() {
24
+ const context = useContext(OpenAPITabsContext);
25
+ if (!context) {
26
+ throw new Error('OpenAPITabsContext is missing');
27
+ }
28
+ return context;
29
+ }
30
+
31
+ /**
32
+ * The OpenAPI Tabs wrapper component.
33
+ */
34
+ export function OpenAPITabs(
35
+ props: React.PropsWithChildren<TabsProps & { items: Tab[]; stateKey?: string }>,
36
+ ) {
37
+ const { children, items, stateKey } = props;
38
+ const isVisible = stateKey
39
+ ? useIntersectionObserver({
40
+ threshold: 0.1,
41
+ rootMargin: '200px',
42
+ })
43
+ : true;
44
+ const defaultTab = items[0] as Tab;
45
+ const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState<Tab>();
46
+ const [selectedTabKey, setSelectedTabKey] = useState(() => {
47
+ if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
48
+ const tabFromState = syncedTabs.get(stateKey);
49
+ return tabFromState?.key ?? items[0]?.key;
50
+ }
51
+ return items[0]?.key;
52
+ });
53
+ const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
54
+
55
+ const handleSelectionChange = (key: Key) => {
56
+ setSelectedTabKey(key);
57
+ if (stateKey) {
58
+ const tab = items.find((item) => item.key === key);
59
+
60
+ if (!tab) {
61
+ return;
62
+ }
63
+
64
+ setSyncedTabs((state) => {
65
+ const newState = new Map(state);
66
+ newState.set(stateKey, tab);
67
+ return newState;
68
+ });
69
+ }
70
+ };
71
+
72
+ useEffect(() => {
73
+ if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
74
+ const tabFromState = syncedTabs.get(stateKey);
75
+
76
+ if (!items.some((item) => item.key === tabFromState?.key)) {
77
+ return;
78
+ }
79
+
80
+ if (tabFromState && tabFromState?.key !== selectedTab?.key) {
81
+ setSelectedTab(tabFromState);
82
+ }
83
+ }
84
+ }, [isVisible, stateKey, syncedTabs, selectedTabKey]);
85
+
86
+ const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]);
87
+
88
+ return (
89
+ <OpenAPITabsContext.Provider value={contextValue}>
90
+ <Tabs
91
+ className="openapi-tabs"
92
+ onSelectionChange={handleSelectionChange}
93
+ selectedKey={selectedTab?.key}
94
+ >
95
+ {children}
96
+ </Tabs>
97
+ </OpenAPITabsContext.Provider>
98
+ );
99
+ }
100
+
101
+ /**
102
+ * The OpenAPI Tabs list component.
103
+ * This component should be used as a child of the OpenAPITabs component.
104
+ * It renders the list of tabs.
105
+ */
106
+ export function OpenAPITabsList() {
107
+ const { items } = useOpenAPITabsContext();
108
+
109
+ return (
110
+ <TabList className="openapi-tabs-list">
111
+ {items.map((tab) => (
112
+ <Tab
113
+ style={({ isFocusVisible }) => ({
114
+ outline: isFocusVisible
115
+ ? '2px solid rgb(var(--primary-color-500)/0.4)'
116
+ : 'none',
117
+ })}
118
+ className="openapi-tabs-tab"
119
+ key={`Tab-${tab.key}`}
120
+ id={tab.key}
121
+ >
122
+ {tab.label}
123
+ </Tab>
124
+ ))}
125
+ </TabList>
126
+ );
127
+ }
128
+
129
+ /**
130
+ * The OpenAPI Tabs panels component.
131
+ * This component should be used as a child of the OpenAPITabs component.
132
+ * It renders the content of the selected tab.
133
+ */
134
+ export function OpenAPITabsPanels() {
135
+ const { selectedTab } = useOpenAPITabsContext();
136
+
137
+ if (!selectedTab) {
138
+ return null;
139
+ }
140
+
141
+ return (
142
+ <TabPanel
143
+ key={`TabPanel-${selectedTab.key}`}
144
+ id={selectedTab.key.toString()}
145
+ className="openapi-tabs-panel"
146
+ >
147
+ {selectedTab.body}
148
+ {selectedTab.description ? (
149
+ <Markdown source={selectedTab.description} className="openapi-tabs-footer" />
150
+ ) : null}
151
+ </TabPanel>
152
+ );
153
+ }
@@ -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
+ }