@gitbook/react-openapi 1.0.4 → 1.1.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 +22 -0
- package/dist/InteractiveSection.jsx +10 -9
- package/dist/OpenAPICodeSample.jsx +3 -3
- package/dist/OpenAPIDisclosure.d.ts +5 -9
- package/dist/OpenAPIDisclosure.jsx +25 -27
- package/dist/OpenAPIDisclosureGroup.d.ts +2 -2
- package/dist/OpenAPIDisclosureGroup.jsx +6 -5
- package/dist/OpenAPIPath.jsx +5 -1
- package/dist/OpenAPIResponseExample.jsx +8 -8
- package/dist/OpenAPIResponses.jsx +3 -3
- package/dist/OpenAPISchema.d.ts +3 -26
- package/dist/OpenAPISchema.jsx +80 -131
- package/dist/OpenAPISpec.jsx +3 -4
- package/dist/OpenAPITabs.jsx +51 -47
- package/dist/ScalarApiButton.d.ts +3 -2
- package/dist/ScalarApiButton.jsx +22 -18
- package/dist/StaticSection.d.ts +10 -0
- package/dist/StaticSection.jsx +23 -0
- package/dist/dereference.d.ts +5 -0
- package/dist/dereference.js +68 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/models/OpenAPIModels.d.ts +9 -0
- package/dist/models/OpenAPIModels.jsx +62 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.js +2 -0
- package/dist/models/resolveOpenAPIModels.d.ts +7 -0
- package/dist/models/resolveOpenAPIModels.js +73 -0
- package/dist/resolveOpenAPIOperation.d.ts +2 -2
- package/dist/resolveOpenAPIOperation.js +3 -34
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/useSyncedTabsGlobalState.d.ts +10 -1
- package/dist/useSyncedTabsGlobalState.js +19 -15
- package/dist/utils.js +42 -3
- package/package.json +3 -3
- package/src/InteractiveSection.tsx +10 -18
- package/src/OpenAPICodeSample.tsx +3 -3
- package/src/OpenAPIDisclosure.tsx +35 -42
- package/src/OpenAPIDisclosureGroup.tsx +13 -11
- package/src/OpenAPIPath.tsx +7 -1
- package/src/OpenAPIResponseExample.tsx +8 -15
- package/src/OpenAPIResponses.tsx +3 -3
- package/src/OpenAPISchema.test.ts +26 -35
- package/src/OpenAPISchema.tsx +138 -227
- package/src/OpenAPISpec.tsx +3 -5
- package/src/OpenAPITabs.tsx +52 -63
- package/src/ScalarApiButton.tsx +26 -28
- package/src/StaticSection.tsx +59 -0
- package/src/dereference.ts +29 -0
- package/src/index.ts +3 -2
- package/src/models/OpenAPIModels.tsx +89 -0
- package/src/models/index.ts +2 -0
- package/src/models/resolveOpenAPIModels.ts +35 -0
- package/src/resolveOpenAPIOperation.ts +8 -36
- package/src/types.ts +10 -0
- package/src/useSyncedTabsGlobalState.ts +33 -21
- package/src/utils.ts +51 -3
package/src/OpenAPITabs.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
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
|
-
import {
|
|
5
|
+
import { useEventCallback } from 'usehooks-ts';
|
|
6
6
|
import { Markdown } from './Markdown';
|
|
7
|
-
import {
|
|
7
|
+
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
|
8
8
|
|
|
9
9
|
export type TabItem = {
|
|
10
10
|
key: Key;
|
|
@@ -15,7 +15,7 @@ export type TabItem = {
|
|
|
15
15
|
|
|
16
16
|
type OpenAPITabsContextData = {
|
|
17
17
|
items: TabItem[];
|
|
18
|
-
selectedTab: TabItem;
|
|
18
|
+
selectedTab: TabItem | null;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
|
|
@@ -35,68 +35,54 @@ export function OpenAPITabs(
|
|
|
35
35
|
props: React.PropsWithChildren<TabsProps & { items: TabItem[]; stateKey?: string }>
|
|
36
36
|
) {
|
|
37
37
|
const { children, items, stateKey } = props;
|
|
38
|
-
const [
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const [selectedTabKey, setSelectedTabKey] = useState(() => {
|
|
46
|
-
if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
|
|
47
|
-
const tabFromState = syncedTabs.get(stateKey);
|
|
48
|
-
return tabFromState?.key ?? items[0]?.key;
|
|
38
|
+
const [tabKey, setTabKey] = useState<Key | null>(() => {
|
|
39
|
+
if (stateKey && typeof window !== 'undefined') {
|
|
40
|
+
const store = getOrCreateTabStoreByKey(stateKey);
|
|
41
|
+
const tabKey = store.getState().tabKey;
|
|
42
|
+
if (tabKey) {
|
|
43
|
+
return tabKey;
|
|
44
|
+
}
|
|
49
45
|
}
|
|
50
|
-
return items[0]?.key;
|
|
46
|
+
return items[0]?.key ?? null;
|
|
51
47
|
});
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
setSelectedTabKey(key);
|
|
56
|
-
if (stateKey) {
|
|
57
|
-
const tab = items.find((item) => item.key === key);
|
|
58
|
-
|
|
59
|
-
if (!tab) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
setSyncedTabs((state) => {
|
|
64
|
-
const newState = new Map(state);
|
|
65
|
-
newState.set(stateKey, tab);
|
|
66
|
-
return newState;
|
|
67
|
-
});
|
|
48
|
+
const selectTab = useEventCallback((key: Key | null) => {
|
|
49
|
+
if (!key || key === tabKey) {
|
|
50
|
+
return;
|
|
68
51
|
}
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
const tab = items.find((item) => item.key === key);
|
|
53
|
+
if (!tab) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setTabKey(key);
|
|
57
|
+
});
|
|
58
|
+
const selectedTab = items.find((item) => item.key === tabKey) ?? items[0] ?? null;
|
|
59
|
+
const cancelDeferRef = useRef<(() => void) | null>(null);
|
|
71
60
|
useEffect(() => {
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!items.some((item) => item.key === tabFromState?.key)) {
|
|
76
|
-
return setSelectedTab(defaultTab);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (tabFromState && tabFromState?.key !== selectedTab?.key) {
|
|
80
|
-
const tabFromItems = items.find((item) => item.key === tabFromState.key);
|
|
81
|
-
|
|
82
|
-
if (!tabFromItems) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
setSelectedTab(tabFromItems);
|
|
87
|
-
}
|
|
61
|
+
if (!stateKey) {
|
|
62
|
+
return undefined;
|
|
88
63
|
}
|
|
89
|
-
|
|
90
|
-
|
|
64
|
+
const store = getOrCreateTabStoreByKey(stateKey);
|
|
65
|
+
return store.subscribe((state) => {
|
|
66
|
+
cancelDeferRef.current?.();
|
|
67
|
+
cancelDeferRef.current = defer(() => selectTab(state.tabKey));
|
|
68
|
+
});
|
|
69
|
+
}, [stateKey, selectTab]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
return () => cancelDeferRef.current?.();
|
|
72
|
+
}, []);
|
|
91
73
|
const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]);
|
|
92
|
-
|
|
93
74
|
return (
|
|
94
75
|
<OpenAPITabsContext.Provider value={contextValue}>
|
|
95
76
|
<Tabs
|
|
96
|
-
ref={ref}
|
|
97
77
|
className="openapi-tabs"
|
|
98
|
-
onSelectionChange={
|
|
99
|
-
|
|
78
|
+
onSelectionChange={(tabKey) => {
|
|
79
|
+
selectTab(tabKey);
|
|
80
|
+
if (stateKey) {
|
|
81
|
+
const store = getOrCreateTabStoreByKey(stateKey);
|
|
82
|
+
store.setState({ tabKey });
|
|
83
|
+
}
|
|
84
|
+
}}
|
|
85
|
+
selectedKey={tabKey}
|
|
100
86
|
>
|
|
101
87
|
{children}
|
|
102
88
|
</Tabs>
|
|
@@ -104,6 +90,11 @@ export function OpenAPITabs(
|
|
|
104
90
|
);
|
|
105
91
|
}
|
|
106
92
|
|
|
93
|
+
const defer = (fn: () => void) => {
|
|
94
|
+
const id = setTimeout(fn, 0);
|
|
95
|
+
return () => clearTimeout(id);
|
|
96
|
+
};
|
|
97
|
+
|
|
107
98
|
/**
|
|
108
99
|
* The OpenAPI Tabs list component.
|
|
109
100
|
* This component should be used as a child of the OpenAPITabs component.
|
|
@@ -116,14 +107,14 @@ export function OpenAPITabsList() {
|
|
|
116
107
|
<TabList className="openapi-tabs-list">
|
|
117
108
|
{items.map((tab) => (
|
|
118
109
|
<Tab
|
|
110
|
+
key={tab.key}
|
|
111
|
+
id={tab.key}
|
|
119
112
|
style={({ isFocusVisible }) => ({
|
|
120
113
|
outline: isFocusVisible
|
|
121
114
|
? '2px solid rgb(var(--primary-color-500)/0.4)'
|
|
122
115
|
: 'none',
|
|
123
116
|
})}
|
|
124
117
|
className="openapi-tabs-tab"
|
|
125
|
-
key={`Tab-${tab.key}`}
|
|
126
|
-
id={tab.key}
|
|
127
118
|
>
|
|
128
119
|
{tab.label}
|
|
129
120
|
</Tab>
|
|
@@ -144,12 +135,10 @@ export function OpenAPITabsPanels() {
|
|
|
144
135
|
return null;
|
|
145
136
|
}
|
|
146
137
|
|
|
138
|
+
const key = selectedTab.key.toString();
|
|
139
|
+
|
|
147
140
|
return (
|
|
148
|
-
<TabPanel
|
|
149
|
-
key={`TabPanel-${selectedTab.key}`}
|
|
150
|
-
id={selectedTab.key.toString()}
|
|
151
|
-
className="openapi-tabs-panel"
|
|
152
|
-
>
|
|
141
|
+
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
|
153
142
|
{selectedTab.body}
|
|
154
143
|
{selectedTab.description ? (
|
|
155
144
|
<Markdown source={selectedTab.description} className="openapi-tabs-footer" />
|
package/src/ScalarApiButton.tsx
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ApiClientModalProvider, useApiClientModal } from '@scalar/api-client-react';
|
|
4
|
-
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
4
|
+
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
|
8
8
|
import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Button which launches the Scalar API Client
|
|
12
12
|
*/
|
|
13
|
-
export function ScalarApiButton({
|
|
14
|
-
method
|
|
15
|
-
path,
|
|
16
|
-
specUrl,
|
|
17
|
-
}: {
|
|
18
|
-
method: string;
|
|
13
|
+
export function ScalarApiButton(props: {
|
|
14
|
+
method: OpenAPIV3_1.HttpMethods;
|
|
19
15
|
path: string;
|
|
20
16
|
specUrl: string;
|
|
21
17
|
}) {
|
|
18
|
+
const { method, path, specUrl } = props;
|
|
22
19
|
const [isOpen, setIsOpen] = useState(false);
|
|
23
20
|
const controllerRef = useRef<ScalarModalControllerRef>(null);
|
|
24
21
|
return (
|
|
@@ -55,21 +52,18 @@ export function ScalarApiButton({
|
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
function ScalarModal(props: {
|
|
58
|
-
method:
|
|
55
|
+
method: OpenAPIV3_1.HttpMethods;
|
|
59
56
|
path: string;
|
|
60
57
|
specUrl: string;
|
|
61
58
|
controllerRef: React.Ref<ScalarModalControllerRef>;
|
|
62
59
|
}) {
|
|
60
|
+
const { method, path, specUrl, controllerRef } = props;
|
|
63
61
|
return (
|
|
64
62
|
<ApiClientModalProvider
|
|
65
|
-
configuration={{ spec: { url:
|
|
66
|
-
initialRequest={{
|
|
63
|
+
configuration={{ spec: { url: specUrl } }}
|
|
64
|
+
initialRequest={{ method, path }}
|
|
67
65
|
>
|
|
68
|
-
<ScalarModalController
|
|
69
|
-
method={props.method}
|
|
70
|
-
path={props.path}
|
|
71
|
-
controllerRef={props.controllerRef}
|
|
72
|
-
/>
|
|
66
|
+
<ScalarModalController method={method} path={path} controllerRef={controllerRef} />
|
|
73
67
|
</ApiClientModalProvider>
|
|
74
68
|
);
|
|
75
69
|
}
|
|
@@ -79,28 +73,32 @@ type ScalarModalControllerRef = {
|
|
|
79
73
|
};
|
|
80
74
|
|
|
81
75
|
function ScalarModalController(props: {
|
|
82
|
-
method:
|
|
76
|
+
method: OpenAPIV3_1.HttpMethods;
|
|
83
77
|
path: string;
|
|
84
78
|
controllerRef: React.Ref<ScalarModalControllerRef>;
|
|
85
79
|
}) {
|
|
80
|
+
const { method, path, controllerRef } = props;
|
|
86
81
|
const client = useApiClientModal();
|
|
87
|
-
const
|
|
82
|
+
const openScalarClient = client?.open;
|
|
83
|
+
const { onOpenClient: trackClientOpening } = useOpenAPIOperationContext();
|
|
84
|
+
const openClient = useMemo(() => {
|
|
85
|
+
if (openScalarClient) {
|
|
86
|
+
return () => {
|
|
87
|
+
openScalarClient({ method, path, _source: 'gitbook' });
|
|
88
|
+
trackClientOpening({ method, path });
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}, [openScalarClient, method, path, trackClientOpening]);
|
|
88
93
|
useImperativeHandle(
|
|
89
|
-
|
|
94
|
+
controllerRef,
|
|
90
95
|
() => ({ openClient: openClient ? () => openClient() : undefined }),
|
|
91
96
|
[openClient]
|
|
92
97
|
);
|
|
93
98
|
|
|
94
|
-
// Open
|
|
95
|
-
const { onOpenClient } = useOpenAPIOperationContext();
|
|
96
|
-
const trackOpening = useEventCallback(() => {
|
|
97
|
-
onOpenClient({ method: props.method, path: props.path });
|
|
98
|
-
});
|
|
99
|
+
// Open at mount
|
|
99
100
|
useEffect(() => {
|
|
100
|
-
|
|
101
|
-
openClient();
|
|
102
|
-
trackOpening();
|
|
103
|
-
}
|
|
101
|
+
openClient?.();
|
|
104
102
|
}, [openClient]);
|
|
105
103
|
return null;
|
|
106
104
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { type ComponentPropsWithoutRef, forwardRef } from 'react';
|
|
3
|
+
|
|
4
|
+
export function Section(props: ComponentPropsWithoutRef<'div'>) {
|
|
5
|
+
return <div {...props} className={clsx('openapi-section', props.className)} />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
{...props}
|
|
12
|
+
className={clsx(
|
|
13
|
+
'openapi-section-header',
|
|
14
|
+
props.className && `${props.className}-header`
|
|
15
|
+
)}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SectionHeaderContent(props: ComponentPropsWithoutRef<'div'>) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
{...props}
|
|
24
|
+
className={clsx(
|
|
25
|
+
'openapi-section-header-content',
|
|
26
|
+
props.className && `${props.className}-header-content`
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SectionBody = forwardRef(function SectionBody(
|
|
33
|
+
props: ComponentPropsWithoutRef<'div'>,
|
|
34
|
+
ref: React.ForwardedRef<HTMLDivElement>
|
|
35
|
+
) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
{...props}
|
|
40
|
+
className={clsx('openapi-section-body', props.className && `${props.className}-body`)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export function StaticSection(props: {
|
|
46
|
+
className: string;
|
|
47
|
+
header: React.ReactNode;
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
}) {
|
|
50
|
+
const { className, header, children } = props;
|
|
51
|
+
return (
|
|
52
|
+
<Section className={className}>
|
|
53
|
+
<SectionHeader className={className}>
|
|
54
|
+
<SectionHeaderContent className={className}>{header}</SectionHeaderContent>
|
|
55
|
+
</SectionHeader>
|
|
56
|
+
<SectionBody className={className}>{children}</SectionBody>
|
|
57
|
+
</Section>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Filesystem, type OpenAPIV3xDocument, dereference } from '@gitbook/openapi-parser';
|
|
2
|
+
|
|
3
|
+
const dereferenceCache = new WeakMap<Filesystem, Promise<OpenAPIV3xDocument>>();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Memoized version of `dereferenceSchema`.
|
|
7
|
+
*/
|
|
8
|
+
export function dereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
|
9
|
+
if (dereferenceCache.has(filesystem)) {
|
|
10
|
+
return dereferenceCache.get(filesystem) as Promise<OpenAPIV3xDocument>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const promise = baseDereferenceFilesystem(filesystem);
|
|
14
|
+
dereferenceCache.set(filesystem, promise);
|
|
15
|
+
return promise;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Dereference an OpenAPI schema.
|
|
20
|
+
*/
|
|
21
|
+
async function baseDereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
|
22
|
+
const result = await dereference(filesystem);
|
|
23
|
+
|
|
24
|
+
if (!result.schema) {
|
|
25
|
+
throw new Error('Failed to dereference OpenAPI document');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result.schema as OpenAPIV3xDocument;
|
|
29
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './models';
|
|
2
2
|
export * from './OpenAPIOperation';
|
|
3
3
|
export * from './OpenAPIOperationContext';
|
|
4
|
-
export
|
|
4
|
+
export * from './resolveOpenAPIOperation';
|
|
5
|
+
export type { OpenAPIModelsData, OpenAPIOperationData } from './types';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
|
|
3
|
+
import { OpenAPIRootSchema } from '../OpenAPISchema';
|
|
4
|
+
import { Section, SectionBody } from '../StaticSection';
|
|
5
|
+
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIModelsData } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Display OpenAPI Models.
|
|
9
|
+
*/
|
|
10
|
+
export function OpenAPIModels(props: {
|
|
11
|
+
className?: string;
|
|
12
|
+
data: OpenAPIModelsData;
|
|
13
|
+
context: OpenAPIContextProps;
|
|
14
|
+
}) {
|
|
15
|
+
const { className, data, context } = props;
|
|
16
|
+
const { models } = data;
|
|
17
|
+
|
|
18
|
+
const clientContext: OpenAPIClientContext = {
|
|
19
|
+
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
|
20
|
+
icons: context.icons,
|
|
21
|
+
blockKey: context.blockKey,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (!models.length) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={clsx('openapi-models', className)}>
|
|
30
|
+
<OpenAPIRootModelsSchema models={models} context={clientContext} />
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Root schema for OpenAPI models.
|
|
37
|
+
* It displays a single model or a disclosure group for multiple models.
|
|
38
|
+
*/
|
|
39
|
+
function OpenAPIRootModelsSchema(props: {
|
|
40
|
+
models: OpenAPIModelsData['models'];
|
|
41
|
+
context: OpenAPIClientContext;
|
|
42
|
+
}) {
|
|
43
|
+
const { models, context } = props;
|
|
44
|
+
|
|
45
|
+
// If there is only one model, we show it directly.
|
|
46
|
+
if (models.length === 1) {
|
|
47
|
+
const schema = models?.[0]?.schema;
|
|
48
|
+
|
|
49
|
+
if (!schema) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Section>
|
|
55
|
+
<SectionBody>
|
|
56
|
+
<OpenAPIRootSchema schema={schema} context={context} />
|
|
57
|
+
</SectionBody>
|
|
58
|
+
</Section>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If there are multiple models, we use a disclosure group to show them all.
|
|
63
|
+
return (
|
|
64
|
+
<OpenAPIDisclosureGroup
|
|
65
|
+
allowsMultipleExpanded
|
|
66
|
+
icon={context.icons.chevronRight}
|
|
67
|
+
groups={models.map(({ name, schema }) => ({
|
|
68
|
+
id: name,
|
|
69
|
+
label: (
|
|
70
|
+
<div className="openapi-response-tab-content" key={`model-${name}`}>
|
|
71
|
+
<span className="openapi-response-statuscode">{name}</span>
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
tabs: [
|
|
75
|
+
{
|
|
76
|
+
id: 'model',
|
|
77
|
+
body: (
|
|
78
|
+
<Section className="openapi-section-models">
|
|
79
|
+
<SectionBody>
|
|
80
|
+
<OpenAPIRootSchema schema={schema} context={context} />
|
|
81
|
+
</SectionBody>
|
|
82
|
+
</Section>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
}))}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Filesystem,
|
|
3
|
+
type OpenAPIV3,
|
|
4
|
+
type OpenAPIV3_1,
|
|
5
|
+
type OpenAPIV3xDocument,
|
|
6
|
+
shouldIgnoreEntity,
|
|
7
|
+
} from '@gitbook/openapi-parser';
|
|
8
|
+
import { dereferenceFilesystem } from '../dereference';
|
|
9
|
+
import type { OpenAPIModel, OpenAPIModelsData } from '../types';
|
|
10
|
+
|
|
11
|
+
//!!TODO: We should return only the models that are used in the block. Still a WIP awaiting future work.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve an OpenAPI models from a file and compile it to a more usable format.
|
|
15
|
+
* Models are extracted from the OpenAPI components.schemas
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveOpenAPIModels(
|
|
18
|
+
filesystem: Filesystem<OpenAPIV3xDocument>
|
|
19
|
+
): Promise<OpenAPIModelsData | null> {
|
|
20
|
+
const schema = await dereferenceFilesystem(filesystem);
|
|
21
|
+
|
|
22
|
+
const models = getOpenAPIComponents(schema);
|
|
23
|
+
|
|
24
|
+
return { models };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get OpenAPI components.schemas that are not ignored.
|
|
29
|
+
*/
|
|
30
|
+
function getOpenAPIComponents(schema: OpenAPIV3.Document | OpenAPIV3_1.Document): OpenAPIModel[] {
|
|
31
|
+
const schemas = schema.components?.schemas ?? {};
|
|
32
|
+
return Object.entries(schemas)
|
|
33
|
+
.filter(([, schema]) => !shouldIgnoreEntity(schema))
|
|
34
|
+
.map(([key, schema]) => ({ name: key, schema }));
|
|
35
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { fromJSON, toJSON } from 'flatted';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
dereference,
|
|
3
|
+
import type {
|
|
4
|
+
Filesystem,
|
|
5
|
+
OpenAPIV3,
|
|
6
|
+
OpenAPIV3_1,
|
|
7
|
+
OpenAPIV3xDocument,
|
|
9
8
|
} from '@gitbook/openapi-parser';
|
|
9
|
+
import { dereferenceFilesystem } from './dereference';
|
|
10
10
|
import type { OpenAPIOperationData } from './types';
|
|
11
11
|
import { checkIsReference } from './utils';
|
|
12
12
|
|
|
13
|
-
export {
|
|
13
|
+
export { fromJSON, toJSON };
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Resolve an OpenAPI operation in a file and compile it to a more usable format.
|
|
@@ -23,7 +23,7 @@ export async function resolveOpenAPIOperation(
|
|
|
23
23
|
}
|
|
24
24
|
): Promise<OpenAPIOperationData | null> {
|
|
25
25
|
const { path, method } = operationDescriptor;
|
|
26
|
-
const schema = await
|
|
26
|
+
const schema = await dereferenceFilesystem(filesystem);
|
|
27
27
|
let operation = getOperationByPathAndMethod(schema, path, method);
|
|
28
28
|
|
|
29
29
|
if (!operation) {
|
|
@@ -69,34 +69,6 @@ export async function resolveOpenAPIOperation(
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const dereferenceCache = new WeakMap<Filesystem, Promise<OpenAPIV3xDocument>>();
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Memoized version of `dereferenceSchema`.
|
|
76
|
-
*/
|
|
77
|
-
function memoDereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
|
78
|
-
if (dereferenceCache.has(filesystem)) {
|
|
79
|
-
return dereferenceCache.get(filesystem) as Promise<OpenAPIV3xDocument>;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const promise = dereferenceFilesystem(filesystem);
|
|
83
|
-
dereferenceCache.set(filesystem, promise);
|
|
84
|
-
return promise;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Dereference an OpenAPI schema.
|
|
89
|
-
*/
|
|
90
|
-
async function dereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
|
91
|
-
const result = await dereference(filesystem);
|
|
92
|
-
|
|
93
|
-
if (!result.schema) {
|
|
94
|
-
throw new Error('Failed to dereference OpenAPI document');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return result.schema as OpenAPIV3xDocument;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
72
|
/**
|
|
101
73
|
* Get a path object from its path.
|
|
102
74
|
*/
|
package/src/types.ts
CHANGED
|
@@ -55,3 +55,13 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
|
|
55
55
|
/** Securities that should be used for this operation */
|
|
56
56
|
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
export type OpenAPIModel = {
|
|
60
|
+
name: string;
|
|
61
|
+
schema: OpenAPIV3.SchemaObject;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface OpenAPIModelsData {
|
|
65
|
+
/** Components schemas to be used for models */
|
|
66
|
+
models: OpenAPIModel[];
|
|
67
|
+
}
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
3
|
+
import { createStore } from 'zustand';
|
|
4
|
+
|
|
5
|
+
type Key = string | number;
|
|
6
|
+
|
|
7
|
+
type TabState = {
|
|
8
|
+
tabKey: Key | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TabActions = { setTabKey: (tab: Key | null) => void };
|
|
12
|
+
|
|
13
|
+
type TabStore = TabState & TabActions;
|
|
14
|
+
|
|
15
|
+
const createTabStore = (initialTab?: Key) => {
|
|
16
|
+
return createStore<TabStore>()((set) => ({
|
|
17
|
+
tabKey: initialTab ?? null,
|
|
18
|
+
setTabKey: (tabKey) => {
|
|
19
|
+
set(() => ({ tabKey }));
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const defaultTabStores = new Map<string, ReturnType<typeof createTabStore>>();
|
|
25
|
+
|
|
26
|
+
const createTabStoreFactory = (stores: typeof defaultTabStores) => {
|
|
27
|
+
return (storeKey: string, initialKey?: Key) => {
|
|
28
|
+
if (!stores.has(storeKey)) {
|
|
29
|
+
stores.set(storeKey, createTabStore(initialKey));
|
|
30
|
+
}
|
|
31
|
+
return stores.get(storeKey)!;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getOrCreateTabStoreByKey = createTabStoreFactory(defaultTabStores);
|