@gitbook/react-openapi 1.1.3 → 1.1.5
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 +105 -91
- package/dist/OpenAPICodeSample.jsx +27 -2
- package/dist/OpenAPICopyButton.d.ts +4 -0
- package/dist/OpenAPICopyButton.jsx +32 -0
- package/dist/OpenAPIOperation.jsx +1 -1
- package/dist/OpenAPIPath.d.ts +1 -2
- package/dist/OpenAPIPath.jsx +32 -30
- package/dist/OpenAPIResponseExample.jsx +5 -8
- package/dist/OpenAPISchemaName.jsx +1 -0
- package/dist/OpenAPITabs.d.ts +1 -1
- package/dist/OpenAPITabs.jsx +9 -2
- package/dist/ScalarApiButton.jsx +2 -2
- package/dist/schemas/OpenAPISchemas.d.ts +3 -1
- package/dist/schemas/resolveOpenAPISchemas.d.ts +9 -3
- package/dist/schemas/resolveOpenAPISchemas.js +24 -15
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/OpenAPICodeSample.tsx +38 -2
- package/src/OpenAPICopyButton.tsx +54 -0
- package/src/OpenAPIOperation.tsx +1 -1
- package/src/OpenAPIPath.tsx +40 -42
- package/src/OpenAPIResponseExample.tsx +30 -33
- package/src/OpenAPISchemaName.tsx +1 -0
- package/src/OpenAPITabs.tsx +13 -4
- package/src/ScalarApiButton.tsx +2 -2
- package/src/schemas/OpenAPISchemas.tsx +6 -1
- package/src/schemas/resolveOpenAPISchemas.test.ts +174 -0
- package/src/schemas/resolveOpenAPISchemas.ts +31 -9
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
1
2
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
3
|
+
import { ScalarApiButton } from './ScalarApiButton';
|
|
2
4
|
import { StaticSection } from './StaticSection';
|
|
3
5
|
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
|
|
4
6
|
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
|
@@ -26,13 +28,17 @@ export function OpenAPICodeSample(props: {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
if (param.in === 'header' && param.required) {
|
|
29
|
-
const example = param.schema
|
|
31
|
+
const example = param.schema
|
|
32
|
+
? generateSchemaExample(param.schema, { mode: 'write' })
|
|
33
|
+
: undefined;
|
|
30
34
|
if (example !== undefined && param.name) {
|
|
31
35
|
headersObject[param.name] =
|
|
32
36
|
typeof example !== 'string' ? stringifyOpenAPI(example) : example;
|
|
33
37
|
}
|
|
34
38
|
} else if (param.in === 'query' && param.required) {
|
|
35
|
-
const example = param.schema
|
|
39
|
+
const example = param.schema
|
|
40
|
+
? generateSchemaExample(param.schema, { mode: 'write' })
|
|
41
|
+
: undefined;
|
|
36
42
|
if (example !== undefined && param.name) {
|
|
37
43
|
searchParams.append(
|
|
38
44
|
param.name,
|
|
@@ -75,6 +81,7 @@ export function OpenAPICodeSample(props: {
|
|
|
75
81
|
code: generator.generate(input),
|
|
76
82
|
syntax: generator.syntax,
|
|
77
83
|
}),
|
|
84
|
+
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
|
78
85
|
}));
|
|
79
86
|
|
|
80
87
|
// Use custom samples if defined
|
|
@@ -101,6 +108,7 @@ export function OpenAPICodeSample(props: {
|
|
|
101
108
|
code: sample.source,
|
|
102
109
|
syntax: sample.lang,
|
|
103
110
|
}),
|
|
111
|
+
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
|
104
112
|
}));
|
|
105
113
|
}
|
|
106
114
|
});
|
|
@@ -124,6 +132,30 @@ export function OpenAPICodeSample(props: {
|
|
|
124
132
|
);
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
function OpenAPICodeSampleFooter(props: {
|
|
136
|
+
data: OpenAPIOperationData;
|
|
137
|
+
context: OpenAPIContextProps;
|
|
138
|
+
}) {
|
|
139
|
+
const { data, context } = props;
|
|
140
|
+
const { method, path } = data;
|
|
141
|
+
const { specUrl } = context;
|
|
142
|
+
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
|
143
|
+
|
|
144
|
+
if (hideTryItPanel) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!validateHttpMethod(method)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="openapi-codesample-footer">
|
|
154
|
+
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
127
159
|
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
128
160
|
[key: string]: string;
|
|
129
161
|
} {
|
|
@@ -165,3 +197,7 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
|
165
197
|
}
|
|
166
198
|
}
|
|
167
199
|
}
|
|
200
|
+
|
|
201
|
+
function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
|
|
202
|
+
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
|
|
203
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
|
|
5
|
+
|
|
6
|
+
export function OpenAPICopyButton(
|
|
7
|
+
props: ButtonProps & {
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
) {
|
|
11
|
+
const { value } = props;
|
|
12
|
+
const { children, onPress, className } = props;
|
|
13
|
+
const [copied, setCopied] = useState(false);
|
|
14
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleCopy = () => {
|
|
17
|
+
if (!value) return;
|
|
18
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
19
|
+
setIsOpen(true);
|
|
20
|
+
setCopied(true);
|
|
21
|
+
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
setCopied(false);
|
|
24
|
+
}, 2000);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen} closeDelay={200} delay={200}>
|
|
30
|
+
<Button
|
|
31
|
+
type="button"
|
|
32
|
+
preventFocusOnPress
|
|
33
|
+
onPress={(e) => {
|
|
34
|
+
handleCopy();
|
|
35
|
+
onPress?.(e);
|
|
36
|
+
}}
|
|
37
|
+
className={`openapi-copy-button ${className}`}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</Button>
|
|
42
|
+
|
|
43
|
+
<Tooltip
|
|
44
|
+
isOpen={isOpen}
|
|
45
|
+
onOpenChange={setIsOpen}
|
|
46
|
+
placement="top"
|
|
47
|
+
offset={4}
|
|
48
|
+
className="openapi-tooltip"
|
|
49
|
+
>
|
|
50
|
+
{copied ? 'Copied' : 'Copy to clipboard'}{' '}
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</TooltipTrigger>
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/OpenAPIOperation.tsx
CHANGED
|
@@ -35,6 +35,7 @@ export function OpenAPIOperation(props: {
|
|
|
35
35
|
title: operation.summary,
|
|
36
36
|
})
|
|
37
37
|
: null}
|
|
38
|
+
<OpenAPIPath data={data} context={context} />
|
|
38
39
|
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
|
39
40
|
</div>
|
|
40
41
|
<div className="openapi-columns">
|
|
@@ -49,7 +50,6 @@ export function OpenAPIOperation(props: {
|
|
|
49
50
|
</div>
|
|
50
51
|
) : null}
|
|
51
52
|
<OpenAPIOperationDescription operation={operation} context={context} />
|
|
52
|
-
<OpenAPIPath data={data} context={context} />
|
|
53
53
|
<OpenAPISpec data={data} context={clientContext} />
|
|
54
54
|
</div>
|
|
55
55
|
<div className="openapi-column-preview">
|
package/src/OpenAPIPath.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type React from 'react';
|
|
3
|
-
import { ScalarApiButton } from './ScalarApiButton';
|
|
1
|
+
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
|
4
2
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
|
3
|
+
import { getDefaultServerURL } from './util/server';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Display the path of an operation.
|
|
@@ -10,63 +9,62 @@ export function OpenAPIPath(props: {
|
|
|
10
9
|
data: OpenAPIOperationData;
|
|
11
10
|
context: OpenAPIContextProps;
|
|
12
11
|
}) {
|
|
13
|
-
const { data
|
|
14
|
-
const { method, path } = data;
|
|
15
|
-
|
|
16
|
-
const
|
|
12
|
+
const { data } = props;
|
|
13
|
+
const { method, path, operation } = data;
|
|
14
|
+
|
|
15
|
+
const server = getDefaultServerURL(data.servers);
|
|
16
|
+
const formattedPath = formatPath(path);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<div className="openapi-path">
|
|
20
20
|
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
|
|
22
|
+
<OpenAPICopyButton
|
|
23
|
+
value={server + path}
|
|
24
|
+
className="openapi-path-title"
|
|
25
|
+
data-deprecated={operation.deprecated}
|
|
26
|
+
>
|
|
27
|
+
<span className="openapi-path-server">{server}</span>
|
|
28
|
+
{formattedPath}
|
|
29
|
+
</OpenAPICopyButton>
|
|
27
30
|
</div>
|
|
28
31
|
);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Format the path to highlight placeholders
|
|
34
|
+
/**
|
|
35
|
+
* Format the path by wrapping placeholders in <span> tags.
|
|
36
|
+
*/
|
|
36
37
|
function formatPath(path: string) {
|
|
37
38
|
// Matches placeholders like {id}, {userId}, etc.
|
|
38
|
-
const regex = /\{(\w+)\}
|
|
39
|
+
const regex = /\{\s*(\w+)\s*\}|:\w+/g;
|
|
39
40
|
|
|
40
41
|
const parts: (string | React.JSX.Element)[] = [];
|
|
41
42
|
let lastIndex = 0;
|
|
42
43
|
|
|
43
|
-
//
|
|
44
|
-
path.replace(regex, (match,
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
//Wrap the variables in <span> tags and maintain either {variable} or :variable
|
|
45
|
+
path.replace(regex, (match, _, offset) => {
|
|
46
|
+
if (offset > lastIndex) {
|
|
47
|
+
parts.push(path.slice(lastIndex, offset));
|
|
48
|
+
}
|
|
49
|
+
parts.push(
|
|
50
|
+
<span key={offset} className="openapi-path-variable">
|
|
51
|
+
{match}
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
47
54
|
lastIndex = offset + match.length;
|
|
48
55
|
return match;
|
|
49
56
|
});
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Join parts with separators wrapped in <span>
|
|
55
|
-
const formattedPath = parts.reduce(
|
|
56
|
-
(acc, part, index) => {
|
|
57
|
-
if (typeof part === 'string' && index > 0 && part === '/') {
|
|
58
|
-
acc.push(
|
|
59
|
-
<span className="openapi-path-separator" key={`sep-${index}`}>
|
|
60
|
-
/
|
|
61
|
-
</span>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
58
|
+
if (lastIndex < path.length) {
|
|
59
|
+
parts.push(path.slice(lastIndex));
|
|
60
|
+
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
const formattedPath = parts.map((part, index) => {
|
|
63
|
+
if (typeof part === 'string') {
|
|
64
|
+
return <span key={index}>{part}</span>;
|
|
65
|
+
}
|
|
66
|
+
return part;
|
|
67
|
+
});
|
|
70
68
|
|
|
71
|
-
return
|
|
69
|
+
return formattedPath;
|
|
72
70
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
|
+
import { Markdown } from './Markdown';
|
|
2
3
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
3
4
|
import { StaticSection } from './StaticSection';
|
|
4
5
|
import { generateSchemaExample } from './generateSchemaExample';
|
|
@@ -39,44 +40,40 @@ export function OpenAPIResponseExample(props: {
|
|
|
39
40
|
return Number(a) - Number(b);
|
|
40
41
|
});
|
|
41
42
|
|
|
42
|
-
const tabs = responses
|
|
43
|
-
|
|
44
|
-
const description = resolveDescription(responseObject);
|
|
45
|
-
|
|
46
|
-
if (checkIsReference(responseObject)) {
|
|
47
|
-
return {
|
|
48
|
-
key: key,
|
|
49
|
-
label: key,
|
|
50
|
-
description,
|
|
51
|
-
body: (
|
|
52
|
-
<OpenAPIExample
|
|
53
|
-
example={getExampleFromReference(responseObject)}
|
|
54
|
-
context={context}
|
|
55
|
-
syntax="json"
|
|
56
|
-
/>
|
|
57
|
-
),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
|
62
|
-
return {
|
|
63
|
-
key: key,
|
|
64
|
-
label: key,
|
|
65
|
-
description,
|
|
66
|
-
body: <OpenAPIEmptyResponseExample />,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
43
|
+
const tabs = responses.map(([key, responseObject]) => {
|
|
44
|
+
const description = resolveDescription(responseObject);
|
|
69
45
|
|
|
46
|
+
if (checkIsReference(responseObject)) {
|
|
70
47
|
return {
|
|
71
48
|
key: key,
|
|
72
49
|
label: key,
|
|
73
|
-
|
|
74
|
-
|
|
50
|
+
body: (
|
|
51
|
+
<OpenAPIExample
|
|
52
|
+
example={getExampleFromReference(responseObject)}
|
|
53
|
+
context={context}
|
|
54
|
+
syntax="json"
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
footer: description ? <Markdown source={description} /> : undefined,
|
|
75
58
|
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
key: key,
|
|
64
|
+
label: key,
|
|
65
|
+
body: <OpenAPIEmptyResponseExample />,
|
|
66
|
+
footer: description ? <Markdown source={description} /> : undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
key: key,
|
|
72
|
+
label: key,
|
|
73
|
+
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
|
74
|
+
footer: description ? <Markdown source={description} /> : undefined,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
80
77
|
|
|
81
78
|
if (tabs.length === 0) {
|
|
82
79
|
return null;
|
|
@@ -30,6 +30,7 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
|
|
|
30
30
|
<span className="openapi-schema-type">{additionalItems}</span>
|
|
31
31
|
) : null}
|
|
32
32
|
</span>
|
|
33
|
+
{schema?.readOnly ? <span className="openapi-schema-readonly">read-only</span> : null}
|
|
33
34
|
{required ? <span className="openapi-schema-required">required</span> : null}
|
|
34
35
|
{schema?.deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
|
|
35
36
|
</div>
|
package/src/OpenAPITabs.tsx
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
|
5
5
|
import { useEventCallback } from 'usehooks-ts';
|
|
6
|
-
import { Markdown } from './Markdown';
|
|
7
6
|
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
|
8
7
|
|
|
9
8
|
export type TabItem = {
|
|
10
9
|
key: Key;
|
|
11
10
|
label: string;
|
|
12
11
|
body: React.ReactNode;
|
|
13
|
-
|
|
12
|
+
footer?: React.ReactNode;
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
type OpenAPITabsContextData = {
|
|
@@ -140,9 +139,19 @@ export function OpenAPITabsPanels() {
|
|
|
140
139
|
return (
|
|
141
140
|
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
|
142
141
|
{selectedTab.body}
|
|
143
|
-
{selectedTab.
|
|
144
|
-
<
|
|
142
|
+
{selectedTab.footer ? (
|
|
143
|
+
<OpenAPITabsPanelFooter>{selectedTab.footer}</OpenAPITabsPanelFooter>
|
|
145
144
|
) : null}
|
|
146
145
|
</TabPanel>
|
|
147
146
|
);
|
|
148
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The OpenAPI Tabs panel footer component.
|
|
151
|
+
* This component should be used as a child of the OpenAPITabs component.
|
|
152
|
+
*/
|
|
153
|
+
function OpenAPITabsPanelFooter(props: { children: React.ReactNode }) {
|
|
154
|
+
const { children } = props;
|
|
155
|
+
|
|
156
|
+
return <div className="openapi-tabs-footer">{children}</div>;
|
|
157
|
+
}
|
package/src/ScalarApiButton.tsx
CHANGED
|
@@ -27,14 +27,14 @@ export function ScalarApiButton(props: {
|
|
|
27
27
|
setIsOpen(true);
|
|
28
28
|
}}
|
|
29
29
|
>
|
|
30
|
-
|
|
30
|
+
Test it
|
|
31
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 12" fill="currentColor">
|
|
31
32
|
<path
|
|
32
33
|
stroke="currentColor"
|
|
33
34
|
strokeWidth="1.5"
|
|
34
35
|
d="M1 10.05V1.43c0-.2.2-.31.37-.22l7.26 4.08c.17.1.17.33.01.43l-7.26 4.54a.25.25 0 0 1-.38-.21Z"
|
|
35
36
|
/>
|
|
36
37
|
</svg>
|
|
37
|
-
Test it
|
|
38
38
|
</button>
|
|
39
39
|
|
|
40
40
|
{isOpen &&
|
|
@@ -4,13 +4,18 @@ import { OpenAPIRootSchema } from '../OpenAPISchema';
|
|
|
4
4
|
import { Section, SectionBody } from '../StaticSection';
|
|
5
5
|
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPISchemasData } from '../types';
|
|
6
6
|
|
|
7
|
+
type OpenAPISchemasContextProps = Omit<
|
|
8
|
+
OpenAPIContextProps,
|
|
9
|
+
'renderCodeBlock' | 'renderHeading' | 'renderDocument'
|
|
10
|
+
>;
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Display OpenAPI Schemas.
|
|
9
14
|
*/
|
|
10
15
|
export function OpenAPISchemas(props: {
|
|
11
16
|
className?: string;
|
|
12
17
|
data: OpenAPISchemasData;
|
|
13
|
-
context:
|
|
18
|
+
context: OpenAPISchemasContextProps;
|
|
14
19
|
}) {
|
|
15
20
|
const { className, data, context } = props;
|
|
16
21
|
const { schemas } = data;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
|
|
4
|
+
import { resolveOpenAPISchemas } from './resolveOpenAPISchemas';
|
|
5
|
+
|
|
6
|
+
async function fetchFilesystem(url: string) {
|
|
7
|
+
const response = await fetch(url);
|
|
8
|
+
const text = await response.text();
|
|
9
|
+
const filesystem = await parseOpenAPI({ value: text, rootURL: url });
|
|
10
|
+
const transformedFs = await traverse(filesystem, async (node) => {
|
|
11
|
+
if ('description' in node && typeof node.description === 'string' && node.description) {
|
|
12
|
+
node['x-gitbook-description-html'] = node.description;
|
|
13
|
+
}
|
|
14
|
+
return node;
|
|
15
|
+
});
|
|
16
|
+
return transformedFs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('#resolveOpenAPISchemas', () => {
|
|
20
|
+
it('should resolve refs', async () => {
|
|
21
|
+
const filesystem = await fetchFilesystem(
|
|
22
|
+
'https://petstore3.swagger.io/api/v3/openapi.json'
|
|
23
|
+
);
|
|
24
|
+
const resolved = await resolveOpenAPISchemas(filesystem, { schemas: ['Pet', 'Tag'] });
|
|
25
|
+
|
|
26
|
+
expect(resolved).toMatchObject({
|
|
27
|
+
schemas: [
|
|
28
|
+
{
|
|
29
|
+
name: 'Pet',
|
|
30
|
+
schema: {
|
|
31
|
+
properties: {
|
|
32
|
+
tags: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
id: {
|
|
38
|
+
format: 'int64',
|
|
39
|
+
type: 'integer',
|
|
40
|
+
},
|
|
41
|
+
name: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'Tag',
|
|
52
|
+
schema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
id: {
|
|
56
|
+
type: 'integer',
|
|
57
|
+
format: 'int64',
|
|
58
|
+
},
|
|
59
|
+
name: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
xml: {
|
|
64
|
+
name: 'tag',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should support yaml', async () => {
|
|
73
|
+
const filesystem = await fetchFilesystem(
|
|
74
|
+
'https://petstore3.swagger.io/api/v3/openapi.yaml'
|
|
75
|
+
);
|
|
76
|
+
const resolved = await resolveOpenAPISchemas(filesystem, { schemas: ['Pet'] });
|
|
77
|
+
|
|
78
|
+
expect(resolved).toMatchObject({
|
|
79
|
+
schemas: [
|
|
80
|
+
{
|
|
81
|
+
name: 'Pet',
|
|
82
|
+
schema: {
|
|
83
|
+
properties: {
|
|
84
|
+
tags: {
|
|
85
|
+
type: 'array',
|
|
86
|
+
items: {
|
|
87
|
+
properties: {
|
|
88
|
+
id: {
|
|
89
|
+
format: 'int64',
|
|
90
|
+
type: 'integer',
|
|
91
|
+
},
|
|
92
|
+
name: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
type: 'object',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should resolve circular refs', async () => {
|
|
107
|
+
const filesystem = await fetchFilesystem('https://api.gitbook.com/openapi.json');
|
|
108
|
+
const resolved = await resolveOpenAPISchemas(filesystem, {
|
|
109
|
+
schemas: ['DocumentBlockTabs'],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(resolved).toMatchObject({
|
|
113
|
+
schemas: [
|
|
114
|
+
{
|
|
115
|
+
name: 'DocumentBlockTabs',
|
|
116
|
+
schema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
object: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
enum: ['block'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should resolve to null if the schema does not exist', async () => {
|
|
131
|
+
const filesystem = await fetchFilesystem(
|
|
132
|
+
'https://petstore3.swagger.io/api/v3/openapi.json'
|
|
133
|
+
);
|
|
134
|
+
const resolved = await resolveOpenAPISchemas(filesystem, {
|
|
135
|
+
schemas: ['NonExistentSchema'],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(resolved).toBe(null);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should parse Swagger 2.0', async () => {
|
|
142
|
+
const filesystem = await fetchFilesystem('https://petstore.swagger.io/v2/swagger.json');
|
|
143
|
+
const resolved = await resolveOpenAPISchemas(filesystem, {
|
|
144
|
+
schemas: ['Pet'],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(resolved).toMatchObject({
|
|
148
|
+
schemas: [
|
|
149
|
+
{
|
|
150
|
+
name: 'Pet',
|
|
151
|
+
schema: {
|
|
152
|
+
properties: {
|
|
153
|
+
tags: {
|
|
154
|
+
type: 'array',
|
|
155
|
+
items: {
|
|
156
|
+
properties: {
|
|
157
|
+
id: {
|
|
158
|
+
format: 'int64',
|
|
159
|
+
type: 'integer',
|
|
160
|
+
},
|
|
161
|
+
name: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
type: 'object',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -15,21 +15,43 @@ import type { OpenAPISchema, OpenAPISchemasData } from '../types';
|
|
|
15
15
|
* Schemas are extracted from the OpenAPI components.schemas
|
|
16
16
|
*/
|
|
17
17
|
export async function resolveOpenAPISchemas(
|
|
18
|
-
filesystem: Filesystem<OpenAPIV3xDocument
|
|
18
|
+
filesystem: Filesystem<OpenAPIV3xDocument>,
|
|
19
|
+
options: {
|
|
20
|
+
schemas: string[];
|
|
21
|
+
}
|
|
19
22
|
): Promise<OpenAPISchemasData | null> {
|
|
23
|
+
const { schemas: selectedSchemas } = options;
|
|
24
|
+
|
|
20
25
|
const schema = await dereferenceFilesystem(filesystem);
|
|
21
26
|
|
|
22
|
-
const schemas =
|
|
27
|
+
const schemas = filterSelectedOpenAPISchemas(schema, selectedSchemas);
|
|
28
|
+
|
|
29
|
+
if (schemas.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
return { schemas };
|
|
25
34
|
}
|
|
26
|
-
|
|
27
35
|
/**
|
|
28
|
-
*
|
|
36
|
+
* Extract selected schemas from the OpenAPI document.
|
|
29
37
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
export function filterSelectedOpenAPISchemas(
|
|
39
|
+
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
|
|
40
|
+
selectedSchemas: string[]
|
|
41
|
+
): OpenAPISchema[] {
|
|
42
|
+
const componentsSchemas = schema.components?.schemas ?? {};
|
|
43
|
+
|
|
44
|
+
// Preserve the order of the selected schemas
|
|
45
|
+
return selectedSchemas
|
|
46
|
+
.map((name) => {
|
|
47
|
+
const schema = componentsSchemas[name];
|
|
48
|
+
if (schema && !shouldIgnoreEntity(schema)) {
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
schema,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
})
|
|
56
|
+
.filter((schema): schema is OpenAPISchema => !!schema);
|
|
35
57
|
}
|