@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.
- package/CHANGELOG.md +49 -0
- package/dist/InteractiveSection.d.ts +4 -8
- package/dist/InteractiveSection.jsx +60 -0
- package/dist/Markdown.d.ts +1 -2
- package/dist/Markdown.jsx +5 -0
- package/dist/OpenAPICodeSample.d.ts +2 -4
- package/dist/OpenAPICodeSample.jsx +141 -0
- package/dist/OpenAPIDisclosure.d.ts +12 -0
- package/dist/OpenAPIDisclosure.jsx +32 -0
- package/dist/OpenAPIDisclosureGroup.d.ts +19 -0
- package/dist/OpenAPIDisclosureGroup.jsx +81 -0
- package/dist/OpenAPIOperation.d.ts +2 -4
- package/dist/OpenAPIOperation.jsx +51 -0
- package/dist/OpenAPIOperationContext.d.ts +16 -0
- package/dist/OpenAPIOperationContext.jsx +26 -0
- package/dist/OpenAPIPath.d.ts +8 -0
- package/dist/OpenAPIPath.jsx +54 -0
- package/dist/OpenAPIRequestBody.d.ts +4 -5
- package/dist/OpenAPIRequestBody.jsx +22 -0
- package/dist/OpenAPIResponse.d.ts +4 -4
- package/dist/OpenAPIResponse.jsx +39 -0
- package/dist/OpenAPIResponseExample.d.ts +2 -4
- package/dist/OpenAPIResponseExample.jsx +108 -0
- package/dist/OpenAPIResponses.d.ts +3 -4
- package/dist/OpenAPIResponses.jsx +35 -0
- package/dist/OpenAPISchema.d.ts +11 -8
- package/dist/OpenAPISchema.jsx +285 -0
- package/dist/OpenAPISchemaName.d.ts +12 -0
- package/dist/OpenAPISchemaName.jsx +15 -0
- package/dist/OpenAPISecurities.d.ts +2 -4
- package/dist/OpenAPISecurities.jsx +55 -0
- package/dist/OpenAPIServerURL.d.ts +2 -3
- package/dist/OpenAPIServerURL.jsx +67 -0
- package/dist/OpenAPIServerURLVariable.d.ts +2 -3
- package/dist/OpenAPIServerURLVariable.jsx +8 -0
- package/dist/OpenAPISpec.d.ts +3 -4
- package/dist/OpenAPISpec.jsx +91 -0
- package/dist/OpenAPITabs.d.ts +26 -0
- package/dist/OpenAPITabs.jsx +103 -0
- package/dist/ScalarApiButton.d.ts +3 -3
- package/dist/ScalarApiButton.jsx +51 -0
- package/dist/code-samples.d.ts +4 -0
- package/dist/code-samples.js +103 -38
- package/dist/generateSchemaExample.d.ts +2 -2
- package/dist/generateSchemaExample.js +29 -102
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/resolveOpenAPIOperation.d.ts +11 -0
- package/dist/resolveOpenAPIOperation.js +194 -0
- package/dist/stringifyOpenAPI.d.ts +4 -0
- package/dist/stringifyOpenAPI.js +6 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +11 -12
- package/dist/useSyncedTabsGlobalState.d.ts +1 -0
- package/dist/useSyncedTabsGlobalState.js +16 -0
- package/dist/utils.d.ts +6 -2
- package/dist/utils.js +13 -6
- package/package.json +12 -10
- package/src/InteractiveSection.tsx +90 -86
- package/src/Markdown.tsx +2 -3
- package/src/OpenAPICodeSample.tsx +43 -31
- package/src/OpenAPIDisclosure.tsx +50 -0
- package/src/OpenAPIDisclosureGroup.tsx +136 -0
- package/src/OpenAPIOperation.tsx +36 -42
- package/src/OpenAPIOperationContext.tsx +45 -0
- package/src/OpenAPIPath.tsx +65 -0
- package/src/OpenAPIRequestBody.tsx +10 -17
- package/src/OpenAPIResponse.tsx +27 -45
- package/src/OpenAPIResponseExample.tsx +89 -31
- package/src/OpenAPIResponses.tsx +48 -17
- package/src/OpenAPISchema.test.ts +1 -1
- package/src/OpenAPISchema.tsx +129 -108
- package/src/OpenAPISchemaName.tsx +27 -0
- package/src/OpenAPISecurities.tsx +45 -24
- package/src/OpenAPIServerURL.tsx +17 -10
- package/src/OpenAPIServerURLVariable.tsx +2 -4
- package/src/OpenAPISpec.tsx +58 -58
- package/src/OpenAPITabs.tsx +153 -0
- package/src/ScalarApiButton.tsx +84 -7
- package/src/code-samples.test.ts +51 -0
- package/src/code-samples.ts +95 -31
- package/src/generateSchemaExample.ts +26 -153
- package/src/index.ts +3 -2
- package/src/resolveOpenAPIOperation.test.ts +177 -0
- package/src/resolveOpenAPIOperation.ts +164 -0
- package/src/stringifyOpenAPI.ts +6 -0
- package/src/types.ts +17 -10
- package/src/useSyncedTabsGlobalState.ts +23 -0
- package/src/utils.ts +14 -7
- package/dist/InteractiveSection.js +0 -47
- package/dist/Markdown.js +0 -6
- package/dist/OpenAPICodeSample.js +0 -110
- package/dist/OpenAPIOperation.js +0 -38
- package/dist/OpenAPIRequestBody.js +0 -18
- package/dist/OpenAPIResponse.js +0 -32
- package/dist/OpenAPIResponseExample.js +0 -54
- package/dist/OpenAPIResponses.js +0 -18
- package/dist/OpenAPISchema.js +0 -235
- package/dist/OpenAPISchema.test.d.ts +0 -1
- package/dist/OpenAPISchema.test.js +0 -91
- package/dist/OpenAPISecurities.js +0 -42
- package/dist/OpenAPIServerURL.js +0 -51
- package/dist/OpenAPIServerURLVariable.js +0 -10
- package/dist/OpenAPISpec.js +0 -70
- package/dist/ScalarApiButton.js +0 -14
- package/dist/fetchOpenAPIOperation.d.ts +0 -72
- package/dist/fetchOpenAPIOperation.js +0 -124
- package/dist/fetchOpenAPIOperation.test.d.ts +0 -1
- package/dist/fetchOpenAPIOperation.test.js +0 -152
- package/dist/resolveOpenAPIPath.d.ts +0 -7
- package/dist/resolveOpenAPIPath.js +0 -112
- package/dist/resolveOpenAPIPath.test.d.ts +0 -1
- package/dist/resolveOpenAPIPath.test.js +0 -39
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/fetchOpenAPIOperation.test.ts +0 -185
- package/src/fetchOpenAPIOperation.ts +0 -230
- package/src/resolveOpenAPIPath.test.ts +0 -60
- package/src/resolveOpenAPIPath.ts +0 -145
package/src/OpenAPISpec.tsx
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
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 {
|
|
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: {
|
|
22
|
-
const {
|
|
19
|
+
export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAPIClientContext }) {
|
|
20
|
+
const { data, context } = props;
|
|
23
21
|
|
|
24
|
-
const
|
|
25
|
-
const { operation, securities } = parsedData;
|
|
22
|
+
const { operation, securities } = data;
|
|
26
23
|
|
|
27
|
-
const
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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={
|
|
67
|
+
<OpenAPIResponses responses={operation.responses} context={context} />
|
|
70
68
|
) : null}
|
|
71
69
|
</>
|
|
72
70
|
);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
function groupParameters(parameters:
|
|
73
|
+
function groupParameters(parameters: OpenAPI.Parameters): Array<{
|
|
76
74
|
key: string;
|
|
77
75
|
label: string;
|
|
78
|
-
parameters:
|
|
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:
|
|
83
|
+
parameters: OpenAPI.Parameters;
|
|
86
84
|
}> = [];
|
|
87
85
|
|
|
88
|
-
parameters
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
group.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
package/src/ScalarApiButton.tsx
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useApiClientModal } from '@scalar/api-client-react';
|
|
4
|
-
import
|
|
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({
|
|
10
|
-
|
|
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={() =>
|
|
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
|
+
});
|
package/src/code-samples.ts
CHANGED
|
@@ -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: '
|
|
18
|
-
label: '
|
|
19
|
-
syntax: '
|
|
19
|
+
id: 'curl',
|
|
20
|
+
label: 'cURL',
|
|
21
|
+
syntax: 'bash',
|
|
20
22
|
generate: ({ method, url, headers, body }) => {
|
|
21
|
-
|
|
23
|
+
const separator = ' \\\n';
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
method: '${method.toUpperCase()}',\n`;
|
|
25
|
+
const lines: string[] = ['curl -L'];
|
|
25
26
|
|
|
26
|
-
if (
|
|
27
|
-
|
|
27
|
+
if (method.toUpperCase() !== 'GET') {
|
|
28
|
+
lines.push(`--request ${method.toUpperCase()}`);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
39
|
+
if (body && Object.keys(body).length > 0) {
|
|
40
|
+
lines.push(`--data '${stringifyOpenAPI(body)}'`);
|
|
41
|
+
}
|
|
36
42
|
|
|
37
|
-
return
|
|
43
|
+
return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator);
|
|
38
44
|
},
|
|
39
45
|
},
|
|
40
46
|
{
|
|
41
|
-
id: '
|
|
42
|
-
label: '
|
|
43
|
-
syntax: '
|
|
47
|
+
id: 'javascript',
|
|
48
|
+
label: 'JavaScript',
|
|
49
|
+
syntax: 'javascript',
|
|
44
50
|
generate: ({ method, url, headers, body }) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const lines: string[] = ['curl -L'];
|
|
51
|
+
let code = '';
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
53
|
+
code += `const response = await fetch('${url}', {
|
|
54
|
+
method: '${method.toUpperCase()}',\n`;
|
|
52
55
|
|
|
53
56
|
if (headers) {
|
|
54
|
-
|
|
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
|
-
|
|
61
|
+
code += indent(`body: JSON.stringify(${stringifyOpenAPI(body, null, 2)}),\n`, 4);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
|
|
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=${
|
|
79
|
+
code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4);
|
|
78
80
|
}
|
|
79
81
|
if (body) {
|
|
80
|
-
code += indent(`json=${
|
|
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
|
+
}
|