@gitbook/react-openapi 0.2.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.
- package/CHANGELOG.md +13 -0
- package/README.md +12 -0
- package/package.json +20 -0
- package/src/InteractiveSection.tsx +129 -0
- package/src/Markdown.tsx +12 -0
- package/src/OpenAPICodeSample.tsx +111 -0
- package/src/OpenAPIOperation.tsx +65 -0
- package/src/OpenAPIRequestBody.tsx +45 -0
- package/src/OpenAPIResponse.tsx +71 -0
- package/src/OpenAPIResponseExample.tsx +71 -0
- package/src/OpenAPIResponses.tsx +30 -0
- package/src/OpenAPISchema.test.ts +101 -0
- package/src/OpenAPISchema.tsx +401 -0
- package/src/OpenAPISecurities.tsx +71 -0
- package/src/OpenAPIServerURL.tsx +65 -0
- package/src/OpenAPIServerURLVariable.tsx +16 -0
- package/src/OpenAPISpec.tsx +118 -0
- package/src/ScalarApiButton.tsx +159 -0
- package/src/code-samples.ts +76 -0
- package/src/fetchOpenAPIOperation.test.ts +185 -0
- package/src/fetchOpenAPIOperation.ts +230 -0
- package/src/generateSchemaExample.ts +189 -0
- package/src/index.ts +3 -0
- package/src/resolveOpenAPIPath.test.ts +60 -0
- package/src/resolveOpenAPIPath.ts +145 -0
- package/src/types.ts +30 -0
- package/src/utils.ts +9 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# `@gitbook/react-openapi`
|
|
2
|
+
|
|
3
|
+
React components to render OpenAPI operations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Generate code samples for the request
|
|
8
|
+
- Support custom cde samples with `x-codeSamples` (Redocly syntax)
|
|
9
|
+
|
|
10
|
+
## TODO
|
|
11
|
+
|
|
12
|
+
- Support for trying out the request
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gitbook/react-openapi",
|
|
3
|
+
"exports": "./src/index.ts",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@scalar/api-client-react": "^0.3.7",
|
|
7
|
+
"@scalar/oas-utils": "0.1.6",
|
|
8
|
+
"classnames": "^2.5.1",
|
|
9
|
+
"flatted": "^3.2.9",
|
|
10
|
+
"openapi-types": "^12.1.3",
|
|
11
|
+
"yaml": "1.10.2",
|
|
12
|
+
"swagger2openapi": "^7.0.8"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/swagger2openapi": "^7.0.4"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"react": "*"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* To optimize rendering, most of the components are server-components,
|
|
8
|
+
* and the interactiveness is mainly handled by a few key components like this one.
|
|
9
|
+
*/
|
|
10
|
+
export function InteractiveSection(props: {
|
|
11
|
+
id?: string;
|
|
12
|
+
/** Class name to be set on the section, sub-elements will use it as prefix */
|
|
13
|
+
className: string;
|
|
14
|
+
/** If true, the content can be toggeable */
|
|
15
|
+
toggeable?: boolean;
|
|
16
|
+
/** Default state of the toggle */
|
|
17
|
+
defaultOpened?: boolean;
|
|
18
|
+
/** Icons to display for the toggle */
|
|
19
|
+
toggleOpenIcon?: React.ReactNode;
|
|
20
|
+
toggleCloseIcon?: React.ReactNode;
|
|
21
|
+
/** Tabs of content to display */
|
|
22
|
+
tabs?: Array<{
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
body: React.ReactNode;
|
|
26
|
+
}>;
|
|
27
|
+
/** Default tab to have opened */
|
|
28
|
+
defaultTab?: string;
|
|
29
|
+
/** Content of the header */
|
|
30
|
+
header: React.ReactNode;
|
|
31
|
+
/** Body of the section */
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
/** Children to display within the container */
|
|
34
|
+
overlay?: React.ReactNode;
|
|
35
|
+
}) {
|
|
36
|
+
const {
|
|
37
|
+
id,
|
|
38
|
+
className,
|
|
39
|
+
toggeable = false,
|
|
40
|
+
defaultOpened = true,
|
|
41
|
+
tabs = [],
|
|
42
|
+
defaultTab = tabs[0]?.key,
|
|
43
|
+
header,
|
|
44
|
+
children,
|
|
45
|
+
overlay,
|
|
46
|
+
toggleOpenIcon = '▶',
|
|
47
|
+
toggleCloseIcon = '▼',
|
|
48
|
+
} = props;
|
|
49
|
+
|
|
50
|
+
const [opened, setOpened] = React.useState(defaultOpened);
|
|
51
|
+
const [selectedTab, setSelectedTab] = React.useState(defaultTab);
|
|
52
|
+
|
|
53
|
+
const tabBody = tabs.find((tab) => tab.key === selectedTab)?.body;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
id={id}
|
|
58
|
+
className={classNames(
|
|
59
|
+
'openapi-section',
|
|
60
|
+
toggeable ? 'openapi-section-toggeable' : null,
|
|
61
|
+
className,
|
|
62
|
+
toggeable ? `${className}-${opened ? 'opened' : 'closed'}` : null,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
onClick={() => {
|
|
67
|
+
if (toggeable) {
|
|
68
|
+
setOpened(!opened);
|
|
69
|
+
}
|
|
70
|
+
}}
|
|
71
|
+
className={classNames('openapi-section-header', `${className}-header`)}
|
|
72
|
+
>
|
|
73
|
+
<div
|
|
74
|
+
className={classNames(
|
|
75
|
+
'openapi-section-header-content',
|
|
76
|
+
`${className}-header-content`,
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
{header}
|
|
80
|
+
</div>
|
|
81
|
+
<div
|
|
82
|
+
className={classNames(
|
|
83
|
+
'openapi-section-header-controls',
|
|
84
|
+
`${className}-header-controls`,
|
|
85
|
+
)}
|
|
86
|
+
onClick={(event) => {
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{tabs.length ? (
|
|
91
|
+
<select
|
|
92
|
+
className={classNames(
|
|
93
|
+
'openapi-section-select',
|
|
94
|
+
'openapi-select',
|
|
95
|
+
`${className}-tabs-select`,
|
|
96
|
+
)}
|
|
97
|
+
value={selectedTab}
|
|
98
|
+
onChange={(event) => {
|
|
99
|
+
setSelectedTab(event.target.value);
|
|
100
|
+
setOpened(true);
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{tabs.map((tab) => (
|
|
104
|
+
<option key={tab.key} value={tab.key}>
|
|
105
|
+
{tab.label}
|
|
106
|
+
</option>
|
|
107
|
+
))}
|
|
108
|
+
</select>
|
|
109
|
+
) : null}
|
|
110
|
+
{(children || tabBody) && toggeable ? (
|
|
111
|
+
<button
|
|
112
|
+
className={classNames('openapi-section-toggle', `${className}-toggle`)}
|
|
113
|
+
onClick={() => setOpened(!opened)}
|
|
114
|
+
>
|
|
115
|
+
{opened ? toggleCloseIcon : toggleOpenIcon}
|
|
116
|
+
</button>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
{(!toggeable || opened) && (children || tabBody) ? (
|
|
121
|
+
<div className={classNames('openapi-section-body', `${className}-body`)}>
|
|
122
|
+
{children}
|
|
123
|
+
{tabBody}
|
|
124
|
+
</div>
|
|
125
|
+
) : null}
|
|
126
|
+
{overlay}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
package/src/Markdown.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
|
|
3
|
+
export function Markdown(props: { source: string; className?: string }) {
|
|
4
|
+
const { source, className } = props;
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
className={classNames('openapi-markdown', className)}
|
|
9
|
+
dangerouslySetInnerHTML={{ __html: source }}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
|
|
3
|
+
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
|
4
|
+
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
|
5
|
+
import { generateMediaTypeExample } from './generateSchemaExample';
|
|
6
|
+
import { InteractiveSection } from './InteractiveSection';
|
|
7
|
+
import { getServersURL } from './OpenAPIServerURL';
|
|
8
|
+
import { ScalarApiButton } from './ScalarApiButton';
|
|
9
|
+
import { OpenAPIContextProps } from './types';
|
|
10
|
+
import { noReference } from './utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Display code samples to execute the operation.
|
|
14
|
+
* It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/)
|
|
15
|
+
*/
|
|
16
|
+
export function OpenAPICodeSample(props: {
|
|
17
|
+
data: OpenAPIOperationData;
|
|
18
|
+
context: OpenAPIContextProps;
|
|
19
|
+
}) {
|
|
20
|
+
const { data, context } = props;
|
|
21
|
+
|
|
22
|
+
const requestBody = noReference(data.operation.requestBody);
|
|
23
|
+
const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
|
|
24
|
+
|
|
25
|
+
const input: CodeSampleInput = {
|
|
26
|
+
url: getServersURL(data.servers) + data.path,
|
|
27
|
+
method: data.method,
|
|
28
|
+
body: requestBodyContent
|
|
29
|
+
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
|
|
30
|
+
: undefined,
|
|
31
|
+
headers: {
|
|
32
|
+
...getSecurityHeaders(data.securities),
|
|
33
|
+
...(requestBodyContent
|
|
34
|
+
? {
|
|
35
|
+
'Content-Type': requestBodyContent[0],
|
|
36
|
+
}
|
|
37
|
+
: undefined),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const autoCodeSamples = codeSampleGenerators.map((generator) => ({
|
|
42
|
+
key: `default-${generator.id}`,
|
|
43
|
+
label: generator.label,
|
|
44
|
+
body: <context.CodeBlock code={generator.generate(input)} syntax={generator.syntax} />,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Use custom samples if defined
|
|
48
|
+
let customCodeSamples: null | Array<{
|
|
49
|
+
key: string;
|
|
50
|
+
label: string;
|
|
51
|
+
body: React.ReactNode;
|
|
52
|
+
}> = null;
|
|
53
|
+
(['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const).forEach((key) => {
|
|
54
|
+
const customSamples = data.operation[key];
|
|
55
|
+
if (customSamples) {
|
|
56
|
+
customCodeSamples = customSamples.map((sample) => ({
|
|
57
|
+
key: `redocly-${sample.lang}`,
|
|
58
|
+
label: sample.label,
|
|
59
|
+
body: <context.CodeBlock code={sample.source} syntax={sample.lang} />,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const samples = customCodeSamples ?? (data['x-codeSamples'] !== false ? autoCodeSamples : []);
|
|
65
|
+
if (samples.length === 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchOperationData() {
|
|
70
|
+
'use server';
|
|
71
|
+
return toJSON(data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<InteractiveSection
|
|
76
|
+
header="Request"
|
|
77
|
+
className="openapi-codesample"
|
|
78
|
+
tabs={samples}
|
|
79
|
+
overlay={
|
|
80
|
+
data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
|
|
81
|
+
<ScalarApiButton fetchOperationData={fetchOperationData} />
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
89
|
+
[key: string]: string;
|
|
90
|
+
} {
|
|
91
|
+
const security = securities[0];
|
|
92
|
+
if (!security) {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
switch (security[1].type) {
|
|
97
|
+
case 'http': {
|
|
98
|
+
let scheme = security[1].scheme;
|
|
99
|
+
if (scheme === 'bearer') {
|
|
100
|
+
scheme = 'Bearer';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
Authorization: scheme + ' ' + (security[1].bearerFormat ?? '<token>'),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
default: {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
|
|
3
|
+
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
|
4
|
+
import { Markdown } from './Markdown';
|
|
5
|
+
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
|
6
|
+
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
|
7
|
+
import { OpenAPIServerURL } from './OpenAPIServerURL';
|
|
8
|
+
import { OpenAPISpec } from './OpenAPISpec';
|
|
9
|
+
import { ScalarApiClient } from './ScalarApiButton';
|
|
10
|
+
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Display an interactive OpenAPI operation.
|
|
14
|
+
*/
|
|
15
|
+
export function OpenAPIOperation(props: {
|
|
16
|
+
className?: string;
|
|
17
|
+
data: OpenAPIOperationData;
|
|
18
|
+
context: OpenAPIContextProps;
|
|
19
|
+
}) {
|
|
20
|
+
const { className, data, context } = props;
|
|
21
|
+
const { operation, servers, method, path } = data;
|
|
22
|
+
|
|
23
|
+
const clientContext: OpenAPIClientContext = {
|
|
24
|
+
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
|
25
|
+
icons: context.icons,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<ScalarApiClient>
|
|
30
|
+
<div className={classNames('openapi-operation', className)}>
|
|
31
|
+
<div className="openapi-intro">
|
|
32
|
+
<h2 className="openapi-summary">{operation.summary}</h2>
|
|
33
|
+
{operation.description ? (
|
|
34
|
+
<Markdown className="openapi-description" source={operation.description} />
|
|
35
|
+
) : null}
|
|
36
|
+
<div className="openapi-target">
|
|
37
|
+
<span
|
|
38
|
+
className={classNames(
|
|
39
|
+
'openapi-method',
|
|
40
|
+
`openapi-method-${method.toLowerCase()}`,
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{method.toUpperCase()}
|
|
44
|
+
</span>
|
|
45
|
+
<span className="openapi-url">
|
|
46
|
+
<OpenAPIServerURL servers={servers} />
|
|
47
|
+
{path}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div className={classNames('openapi-columns')}>
|
|
52
|
+
<div className={classNames('openapi-column-spec')}>
|
|
53
|
+
<OpenAPISpec rawData={toJSON(data)} context={clientContext} />
|
|
54
|
+
</div>
|
|
55
|
+
<div className={classNames('openapi-column-preview')}>
|
|
56
|
+
<div className={classNames('openapi-column-preview-body')}>
|
|
57
|
+
<OpenAPICodeSample {...props} />
|
|
58
|
+
<OpenAPIResponseExample {...props} />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</ScalarApiClient>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { OpenAPIRootSchema } from './OpenAPISchema';
|
|
3
|
+
import { noReference } from './utils';
|
|
4
|
+
import { OpenAPIClientContext } from './types';
|
|
5
|
+
import { InteractiveSection } from './InteractiveSection';
|
|
6
|
+
import { Markdown } from './Markdown';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Display an interactive request body.
|
|
10
|
+
*/
|
|
11
|
+
export function OpenAPIRequestBody(props: {
|
|
12
|
+
requestBody: OpenAPIV3.RequestBodyObject;
|
|
13
|
+
context: OpenAPIClientContext;
|
|
14
|
+
}) {
|
|
15
|
+
const { requestBody, context } = props;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<InteractiveSection
|
|
19
|
+
header="Body"
|
|
20
|
+
className="openapi-requestbody"
|
|
21
|
+
tabs={Object.entries(requestBody.content ?? {}).map(
|
|
22
|
+
([contentType, mediaTypeObject]) => {
|
|
23
|
+
return {
|
|
24
|
+
key: contentType,
|
|
25
|
+
label: contentType,
|
|
26
|
+
body: (
|
|
27
|
+
<OpenAPIRootSchema
|
|
28
|
+
schema={noReference(mediaTypeObject.schema) ?? {}}
|
|
29
|
+
context={context}
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
)}
|
|
35
|
+
defaultOpened={context.defaultInteractiveOpened}
|
|
36
|
+
>
|
|
37
|
+
{requestBody.description ? (
|
|
38
|
+
<Markdown
|
|
39
|
+
source={requestBody.description}
|
|
40
|
+
className="openapi-requestbody-description"
|
|
41
|
+
/>
|
|
42
|
+
) : null}
|
|
43
|
+
</InteractiveSection>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
import { OpenAPIRootSchema, OpenAPISchemaProperties } from './OpenAPISchema';
|
|
4
|
+
import { noReference } from './utils';
|
|
5
|
+
import { OpenAPIClientContext } from './types';
|
|
6
|
+
import { InteractiveSection } from './InteractiveSection';
|
|
7
|
+
import { Markdown } from './Markdown';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Display an interactive response body.
|
|
11
|
+
*/
|
|
12
|
+
export function OpenAPIResponse(props: {
|
|
13
|
+
response: OpenAPIV3.ResponseObject;
|
|
14
|
+
context: OpenAPIClientContext;
|
|
15
|
+
}) {
|
|
16
|
+
const { response, context } = props;
|
|
17
|
+
const content = Object.entries(response.content ?? {});
|
|
18
|
+
const headers = Object.entries(response.headers ?? {}).map(
|
|
19
|
+
([name, header]) => [name, noReference(header) ?? {}] as const,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (content.length === 0 && !response.description && headers.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{response.description ? (
|
|
29
|
+
<Markdown source={response.description} className="openapi-response-description" />
|
|
30
|
+
) : null}
|
|
31
|
+
|
|
32
|
+
{headers.length > 0 ? (
|
|
33
|
+
<InteractiveSection
|
|
34
|
+
toggeable
|
|
35
|
+
defaultOpened={!!context.defaultInteractiveOpened}
|
|
36
|
+
toggleCloseIcon={context.icons.chevronDown}
|
|
37
|
+
toggleOpenIcon={context.icons.chevronRight}
|
|
38
|
+
header="Headers"
|
|
39
|
+
className={classNames('openapi-responseheaders')}
|
|
40
|
+
>
|
|
41
|
+
<OpenAPISchemaProperties
|
|
42
|
+
properties={headers.map(([name, header]) => ({
|
|
43
|
+
propertyName: name,
|
|
44
|
+
schema: noReference(header.schema) ?? {},
|
|
45
|
+
required: header.required,
|
|
46
|
+
}))}
|
|
47
|
+
context={context}
|
|
48
|
+
/>
|
|
49
|
+
</InteractiveSection>
|
|
50
|
+
) : null}
|
|
51
|
+
{content.length > 0 ? (
|
|
52
|
+
<InteractiveSection
|
|
53
|
+
header="Body"
|
|
54
|
+
className={classNames('openapi-responsebody')}
|
|
55
|
+
tabs={content.map(([contentType, mediaType]) => {
|
|
56
|
+
return {
|
|
57
|
+
key: contentType,
|
|
58
|
+
label: contentType,
|
|
59
|
+
body: (
|
|
60
|
+
<OpenAPIRootSchema
|
|
61
|
+
schema={noReference(mediaType.schema) ?? {}}
|
|
62
|
+
context={context}
|
|
63
|
+
/>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
})}
|
|
67
|
+
/>
|
|
68
|
+
) : null}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { InteractiveSection } from './InteractiveSection';
|
|
2
|
+
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
|
3
|
+
import { generateSchemaExample } from './generateSchemaExample';
|
|
4
|
+
import { OpenAPIContextProps } from './types';
|
|
5
|
+
import { noReference } from './utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Display an example of the response content.
|
|
9
|
+
*/
|
|
10
|
+
export function OpenAPIResponseExample(props: {
|
|
11
|
+
data: OpenAPIOperationData;
|
|
12
|
+
context: OpenAPIContextProps;
|
|
13
|
+
}) {
|
|
14
|
+
const { data, context } = props;
|
|
15
|
+
|
|
16
|
+
// if there are no responses defined for the operation
|
|
17
|
+
if (!data.operation.responses) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const responses = Object.entries(data.operation.responses);
|
|
22
|
+
// Sort response to get 200, and 2xx first
|
|
23
|
+
responses.sort(([a], [b]) => {
|
|
24
|
+
if (a === 'default') {
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
if (b === 'default') {
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
30
|
+
if (a === '200') {
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
if (b === '200') {
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
return Number(a) - Number(b);
|
|
37
|
+
});
|
|
38
|
+
// Take the first one
|
|
39
|
+
const response = responses[0];
|
|
40
|
+
|
|
41
|
+
if (!response) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const responseObject = noReference(response[1]);
|
|
46
|
+
|
|
47
|
+
const schema = noReference(
|
|
48
|
+
(
|
|
49
|
+
responseObject.content?.['application/json'] ??
|
|
50
|
+
responseObject.content?.[Object.keys(responseObject.content)[0]]
|
|
51
|
+
)?.schema,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!schema) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const example = generateSchemaExample(schema);
|
|
59
|
+
if (example === undefined) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<InteractiveSection header="Response" className="openapi-response-example">
|
|
65
|
+
<context.CodeBlock
|
|
66
|
+
code={typeof example === 'string' ? example : JSON.stringify(example, null, 2)}
|
|
67
|
+
syntax="json"
|
|
68
|
+
/>
|
|
69
|
+
</InteractiveSection>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
import { noReference } from './utils';
|
|
4
|
+
import { OpenAPIResponse } from './OpenAPIResponse';
|
|
5
|
+
import { OpenAPIClientContext } from './types';
|
|
6
|
+
import { InteractiveSection } from './InteractiveSection';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Display an interactive response body.
|
|
10
|
+
*/
|
|
11
|
+
export function OpenAPIResponses(props: {
|
|
12
|
+
responses: OpenAPIV3.ResponsesObject;
|
|
13
|
+
context: OpenAPIClientContext;
|
|
14
|
+
}) {
|
|
15
|
+
const { responses, context } = props;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<InteractiveSection
|
|
19
|
+
header="Response"
|
|
20
|
+
className={classNames('openapi-responses')}
|
|
21
|
+
tabs={Object.entries(responses).map(([statusCode, response]) => {
|
|
22
|
+
return {
|
|
23
|
+
key: statusCode,
|
|
24
|
+
label: statusCode,
|
|
25
|
+
body: <OpenAPIResponse response={noReference(response)} context={context} />,
|
|
26
|
+
};
|
|
27
|
+
})}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { it, describe, expect } from 'bun:test';
|
|
2
|
+
import { getSchemaAlternatives } from './OpenAPISchema';
|
|
3
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
4
|
+
|
|
5
|
+
describe('getSchemaAlternatives', () => {
|
|
6
|
+
it('should flatten oneOf', () => {
|
|
7
|
+
expect(
|
|
8
|
+
getSchemaAlternatives({
|
|
9
|
+
oneOf: [
|
|
10
|
+
{
|
|
11
|
+
oneOf: [
|
|
12
|
+
{
|
|
13
|
+
type: 'number',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'string',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
25
|
+
).toEqual([
|
|
26
|
+
[
|
|
27
|
+
{
|
|
28
|
+
type: 'number',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'string',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
undefined,
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should not flatten oneOf and allOf', () => {
|
|
42
|
+
expect(
|
|
43
|
+
getSchemaAlternatives({
|
|
44
|
+
oneOf: [
|
|
45
|
+
{
|
|
46
|
+
allOf: [
|
|
47
|
+
{
|
|
48
|
+
type: 'number',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'string',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
).toEqual([
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
allOf: [
|
|
64
|
+
{
|
|
65
|
+
type: 'number',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'boolean',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: 'string',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
undefined,
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should stop at circular references', () => {
|
|
81
|
+
const a: OpenAPIV3.SchemaObject = {
|
|
82
|
+
anyOf: [
|
|
83
|
+
{
|
|
84
|
+
type: 'string',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
a.anyOf!.push(a);
|
|
90
|
+
|
|
91
|
+
expect(getSchemaAlternatives(a)).toEqual([
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
type: 'string',
|
|
95
|
+
},
|
|
96
|
+
a,
|
|
97
|
+
],
|
|
98
|
+
undefined,
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
});
|