Skip to content

Commit f8d4c76

Browse files
authored
Fix schema validation error (#2820)
1 parent dddb4ec commit f8d4c76

18 files changed

+169
-170
lines changed

.changeset/fifty-donkeys-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
---
4+
5+
Sync tabs across all OpenAPI blocks

.changeset/six-trainers-bathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
---
4+
5+
Support for OpenAPI references

bun.lock

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
},
3232
"packages/gitbook": {
3333
"name": "gitbook",
34-
"version": "0.5.0",
34+
"version": "0.6.0",
3535
"dependencies": {
3636
"@gitbook/api": "^0.93.0",
3737
"@gitbook/cache-do": "workspace:*",
@@ -118,7 +118,7 @@
118118
},
119119
"packages/gitbook-v2": {
120120
"name": "gitbook-v2",
121-
"version": "0.0.0",
121+
"version": "0.1.0",
122122
"dependencies": {
123123
"@gitbook/api": "^0.93.0",
124124
"next": "canary",
@@ -151,7 +151,7 @@
151151
},
152152
"packages/openapi-parser": {
153153
"name": "@gitbook/openapi-parser",
154-
"version": "0.0.0",
154+
"version": "1.0.0",
155155
"dependencies": {
156156
"@scalar/openapi-parser": "^0.10.4",
157157
"@scalar/openapi-types": "^0.1.6",
@@ -175,7 +175,7 @@
175175
},
176176
"packages/react-contentkit": {
177177
"name": "@gitbook/react-contentkit",
178-
"version": "0.5.1",
178+
"version": "0.6.0",
179179
"dependencies": {
180180
"@gitbook/api": "^0.93.0",
181181
"assert-never": "^1.2.1",
@@ -207,7 +207,7 @@
207207
},
208208
"packages/react-openapi": {
209209
"name": "@gitbook/react-openapi",
210-
"version": "0.7.1",
210+
"version": "1.0.0",
211211
"dependencies": {
212212
"@gitbook/openapi-parser": "workspace:*",
213213
"@scalar/api-client-react": "1.0.87",
@@ -217,6 +217,7 @@
217217
"react-aria": "^3.37.0",
218218
"react-aria-components": "^1.6.0",
219219
"usehooks-ts": "^3.1.0",
220+
"zustand": "^5.0.3",
220221
},
221222
"devDependencies": {
222223
"bun-types": "^1.1.20",
@@ -3425,6 +3426,8 @@
34253426

34263427
"zod": ["[email protected]", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
34273428

3429+
"zustand": ["[email protected]", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="],
3430+
34283431
"zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
34293432

34303433
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],

packages/openapi-parser/src/filesystem.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ export async function createFileSystem(input: {
2020
const { filesystem } = await load(input.value, {
2121
plugins: [fetchURLs({ rootURL: input.rootURL })],
2222
});
23+
2324
return filesystem;
2425
}

packages/react-openapi/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"flatted": "^3.2.9",
1919
"react-aria-components": "^1.6.0",
2020
"react-aria": "^3.37.0",
21-
"usehooks-ts": "^3.1.0"
21+
"usehooks-ts": "^3.1.0",
22+
"zustand": "^5.0.3"
2223
},
2324
"devDependencies": {
2425
"bun-types": "^1.1.20",

packages/react-openapi/src/InteractiveSection.tsx

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import clsx from 'clsx';
4-
import { useCallback, useRef, useState, useSyncExternalStore } from 'react';
4+
import { useRef, useState } from 'react';
55
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
66
import { useDisclosureState } from 'react-stately';
77

@@ -11,30 +11,6 @@ interface InteractiveSectionTab {
1111
body: React.ReactNode;
1212
}
1313

14-
let globalState: Record<string, string> = {};
15-
const listeners = new Set<() => void>();
16-
17-
function useSyncedTabsGlobalState() {
18-
const subscribe = useCallback((callback: () => void) => {
19-
listeners.add(callback);
20-
return () => listeners.delete(callback);
21-
}, []);
22-
23-
const getSnapshot = useCallback(() => globalState, []);
24-
25-
const setSyncedTabs = useCallback(
26-
(updater: (tabs: Record<string, string>) => Record<string, string>) => {
27-
globalState = updater(globalState);
28-
listeners.forEach((listener) => listener());
29-
},
30-
[],
31-
);
32-
33-
const tabs = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
34-
35-
return [tabs, setSyncedTabs] as const;
36-
}
37-
3814
/**
3915
* To optimize rendering, most of the components are server-components,
4016
* and the interactiveness is mainly handled by a few key components like this one.
@@ -59,8 +35,6 @@ export function InteractiveSection(props: {
5935
children?: React.ReactNode;
6036
/** Children to display within the container */
6137
overlay?: React.ReactNode;
62-
/** An optional key referencing a value in global state */
63-
stateKey?: string;
6438
}) {
6539
const {
6640
id,
@@ -73,16 +47,11 @@ export function InteractiveSection(props: {
7347
children,
7448
overlay,
7549
toggleIcon = '▶',
76-
stateKey,
7750
} = props;
78-
const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState();
79-
const tabFromState =
80-
stateKey && stateKey in syncedTabs
81-
? tabs.find((tab) => tab.key === syncedTabs[stateKey])
82-
: undefined;
83-
const [selectedTabKey, setSelectedTab] = useState(tabFromState?.key ?? defaultTab);
51+
52+
const [selectedTabKey, setSelectedTab] = useState(defaultTab);
8453
const selectedTab: InteractiveSectionTab | undefined =
85-
tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
54+
tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
8655

8756
const state = useDisclosureState({
8857
defaultExpanded: defaultOpened,
@@ -153,12 +122,6 @@ export function InteractiveSection(props: {
153122
value={selectedTab?.key ?? ''}
154123
onChange={(event) => {
155124
setSelectedTab(event.target.value);
156-
if (stateKey) {
157-
setSyncedTabs((state) => ({
158-
...state,
159-
[stateKey]: event.target.value,
160-
}));
161-
}
162125
state.expand();
163126
}}
164127
>

packages/react-openapi/src/OpenAPICodeSample.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { generateMediaTypeExample, generateSchemaExample } from './generateSchem
33
import { InteractiveSection } from './InteractiveSection';
44
import { getServersURL } from './OpenAPIServerURL';
55
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
6-
import { noReference } from './utils';
6+
import { createStateKey } from './utils';
77
import { stringifyOpenAPI } from './stringifyOpenAPI';
88
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
9+
import { checkIsReference } from './utils';
910

1011
/**
1112
* Display code samples to execute the operation.
@@ -20,24 +21,19 @@ export function OpenAPICodeSample(props: {
2021
const searchParams = new URLSearchParams();
2122
const headersObject: { [k: string]: string } = {};
2223

23-
data.operation.parameters?.forEach((rawParam) => {
24-
const param = noReference(rawParam);
24+
data.operation.parameters?.forEach((param) => {
2525
if (!param) {
2626
return;
2727
}
2828

2929
if (param.in === 'header' && param.required) {
30-
const example = param.schema
31-
? generateSchemaExample(noReference(param.schema))
32-
: undefined;
30+
const example = param.schema ? generateSchemaExample(param.schema) : undefined;
3331
if (example !== undefined && param.name) {
3432
headersObject[param.name] =
3533
typeof example !== 'string' ? stringifyOpenAPI(example) : example;
3634
}
3735
} else if (param.in === 'query' && param.required) {
38-
const example = param.schema
39-
? generateSchemaExample(noReference(param.schema))
40-
: undefined;
36+
const example = param.schema ? generateSchemaExample(param.schema) : undefined;
4137
if (example !== undefined && param.name) {
4238
searchParams.append(
4339
param.name,
@@ -47,7 +43,9 @@ export function OpenAPICodeSample(props: {
4743
}
4844
});
4945

50-
const requestBody = noReference(data.operation.requestBody);
46+
const requestBody = !checkIsReference(data.operation.requestBody)
47+
? data.operation.requestBody
48+
: undefined;
5149
const requestBodyContentEntries = requestBody?.content
5250
? Object.entries(requestBody.content)
5351
: undefined;
@@ -115,7 +113,7 @@ export function OpenAPICodeSample(props: {
115113
}
116114

117115
return (
118-
<OpenAPITabs items={samples}>
116+
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
119117
<InteractiveSection header={<OpenAPITabsList />} className="openapi-codesample">
120118
<OpenAPITabsPanels />
121119
</InteractiveSection>

packages/react-openapi/src/OpenAPIRequestBody.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
22
import { OpenAPIRootSchema } from './OpenAPISchema';
3-
import { noReference } from './utils';
43
import type { OpenAPIClientContext } from './types';
54
import { InteractiveSection } from './InteractiveSection';
5+
import { checkIsReference } from './utils';
66

77
/**
88
* Display an interactive request body.
99
*/
1010
export function OpenAPIRequestBody(props: {
11-
requestBody: OpenAPIV3.RequestBodyObject;
11+
requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
1212
context: OpenAPIClientContext;
1313
}) {
1414
const { requestBody, context } = props;
1515

16+
if (checkIsReference(requestBody)) {
17+
return null;
18+
}
19+
1620
return (
1721
<InteractiveSection
1822
header="Body"
@@ -24,7 +28,7 @@ export function OpenAPIRequestBody(props: {
2428
label: contentType,
2529
body: (
2630
<OpenAPIRootSchema
27-
schema={noReference(mediaTypeObject.schema) ?? {}}
31+
schema={mediaTypeObject.schema ?? {}}
2832
context={context}
2933
/>
3034
),

packages/react-openapi/src/OpenAPIResponse.tsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
22
import { OpenAPISchemaProperties } from './OpenAPISchema';
3-
import { checkIsReference, noReference, resolveDescription } from './utils';
3+
import { resolveDescription } from './utils';
44
import type { OpenAPIClientContext } from './types';
55
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
66

@@ -14,7 +14,7 @@ export function OpenAPIResponse(props: {
1414
}) {
1515
const { response, context, mediaType } = props;
1616
const headers = Object.entries(response.headers ?? {}).map(
17-
([name, header]) => [name, noReference(header) ?? {}] as const,
17+
([name, header]) => [name, header ?? {}] as const,
1818
);
1919
const content = Object.entries(mediaType.schema ?? {});
2020

@@ -31,7 +31,7 @@ export function OpenAPIResponse(props: {
3131
<OpenAPISchemaProperties
3232
properties={headers.map(([name, header]) => ({
3333
propertyName: name,
34-
schema: noReference(header.schema) ?? {},
34+
schema: header.schema ?? {},
3535
required: header.required,
3636
}))}
3737
context={context}
@@ -43,7 +43,7 @@ export function OpenAPIResponse(props: {
4343
id={`response-${context.blockKey}`}
4444
properties={[
4545
{
46-
schema: handleUnresolvedReference(mediaType.schema) ?? {},
46+
schema: mediaType.schema ?? {},
4747
},
4848
]}
4949
context={context}
@@ -52,17 +52,3 @@ export function OpenAPIResponse(props: {
5252
</div>
5353
);
5454
}
55-
56-
function handleUnresolvedReference(
57-
input: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
58-
): OpenAPIV3.SchemaObject {
59-
const isReference = checkIsReference(input);
60-
61-
if (isReference || input === undefined) {
62-
// If we find a reference that wasn't resolved or needed to be resolved externally, do not try to render it.
63-
// Instead we render `any`
64-
return {};
65-
}
66-
67-
return input;
68-
}

packages/react-openapi/src/OpenAPIResponseExample.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
22
import { generateSchemaExample } from './generateSchemaExample';
33
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
4-
import { checkIsReference, noReference, resolveDescription } from './utils';
4+
import { checkIsReference, createStateKey, resolveDescription } from './utils';
55
import { stringifyOpenAPI } from './stringifyOpenAPI';
66
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
77
import { InteractiveSection } from './InteractiveSection';
@@ -40,7 +40,7 @@ export function OpenAPIResponseExample(props: {
4040

4141
const examples = responses
4242
.map(([key, value]) => {
43-
const responseObject = noReference(value);
43+
const responseObject = value;
4444
const mediaTypeObject = (() => {
4545
if (!responseObject.content) {
4646
return null;
@@ -68,7 +68,7 @@ export function OpenAPIResponseExample(props: {
6868
const key = Object.keys(examples)[0];
6969
if (key) {
7070
// @TODO handle multiple examples
71-
const firstExample = noReference(examples[key]);
71+
const firstExample = examples[key];
7272
if (firstExample) {
7373
return firstExample;
7474
}
@@ -79,7 +79,7 @@ export function OpenAPIResponseExample(props: {
7979
return { value: example };
8080
}
8181

82-
const schema = noReference(mediaTypeObject.schema);
82+
const schema = mediaTypeObject.schema;
8383
if (!schema) {
8484
return null;
8585
}
@@ -115,7 +115,7 @@ export function OpenAPIResponseExample(props: {
115115
}
116116

117117
return (
118-
<OpenAPITabs items={examples}>
118+
<OpenAPITabs stateKey={createStateKey('response-example')} items={examples}>
119119
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
120120
<OpenAPITabsPanels />
121121
</InteractiveSection>

0 commit comments

Comments
 (0)