Skip to content

Commit 2350baa

Browse files
authored
Support for OpenAPI Array request body (#3420)
1 parent bc1eca8 commit 2350baa

File tree

7 files changed

+97
-41
lines changed

7 files changed

+97
-41
lines changed

.changeset/witty-forks-sell.md

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

packages/gitbook/src/components/DocumentView/OpenAPI/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@
358358

359359
.openapi-requestbody-header-content {
360360
/* unstyled */
361+
@apply flex flex-row items-center gap-2.5;
362+
}
363+
364+
.openapi-requestbody-header-type {
365+
@apply text-tint select-text text-[0.813rem] font-mono font-normal [word-spacing:-0.25rem];
361366
}
362367

363368
.openapi-requestbody-description.openapi-markdown {

packages/react-openapi/src/OpenAPIRequestBody.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
22
import { InteractiveSection } from './InteractiveSection';
3+
import { OpenAPIRequestBodyHeaderType } from './OpenAPIRequestBodyHeaderType';
34
import { OpenAPIRootSchema } from './OpenAPISchemaServer';
45
import type { OpenAPIClientContext } from './context';
56
import { t } from './translate';
@@ -20,11 +21,18 @@ export function OpenAPIRequestBody(props: {
2021
return null;
2122
}
2223

24+
const stateKey = createStateKey('request-body-media-type', context.blockKey);
25+
2326
return (
2427
<InteractiveSection
25-
header={t(context.translation, 'name' in data ? 'payload' : 'body')}
28+
header={
29+
<>
30+
<span>{t(context.translation, 'name' in data ? 'payload' : 'body')}</span>
31+
<OpenAPIRequestBodyHeaderType requestBody={requestBody} stateKey={stateKey} />
32+
</>
33+
}
2634
className="openapi-requestbody"
27-
stateKey={createStateKey('request-body-media-type', context.blockKey)}
35+
stateKey={stateKey}
2836
selectIcon={context.icons.chevronDown}
2937
tabs={Object.entries(requestBody.content ?? {}).map(
3038
([contentType, mediaTypeObject]) => {
@@ -35,6 +43,7 @@ export function OpenAPIRequestBody(props: {
3543
<OpenAPIRootSchema
3644
schema={mediaTypeObject.schema ?? {}}
3745
context={context}
46+
key={contentType}
3847
/>
3948
),
4049
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
4+
import { useSelectState } from './OpenAPISelect';
5+
import { getSchemaTitle } from './utils';
6+
7+
/**
8+
* Display the type of a request body. It only displays the type if the selected content is an array.
9+
*/
10+
export function OpenAPIRequestBodyHeaderType(props: {
11+
requestBody: OpenAPIV3.RequestBodyObject;
12+
stateKey: string;
13+
}) {
14+
const { requestBody, stateKey } = props;
15+
const content = requestBody.content ?? {};
16+
const state = useSelectState(stateKey, Object.keys(content)[0]);
17+
18+
const selectedContentMediaType = Object.entries(content).find(
19+
([contentType]) => contentType === state.key
20+
)?.[1];
21+
22+
// If the selected content is not an array, we don't display the type
23+
if (
24+
!selectedContentMediaType ||
25+
!selectedContentMediaType.schema?.type ||
26+
selectedContentMediaType.schema.type !== 'array'
27+
) {
28+
return null;
29+
}
30+
31+
return (
32+
<span className="openapi-requestbody-header-type">
33+
{`${getSchemaTitle(selectedContentMediaType.schema)}`}
34+
</span>
35+
);
36+
}

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { retrocycle } from './decycle';
1616
import { getDisclosureLabel } from './getDisclosureLabel';
1717
import { stringifyOpenAPI } from './stringifyOpenAPI';
1818
import { tString } from './translate';
19-
import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
19+
import { checkIsReference, getSchemaTitle, resolveDescription, resolveFirstExample } from './utils';
2020

2121
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
2222

@@ -652,40 +652,3 @@ function mergeRequiredFields(
652652
new Set([...(latestAncestor?.required || []), ...(schemaOrRef.required || [])])
653653
);
654654
}
655-
656-
function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
657-
// Otherwise try to infer a nice title
658-
let type = 'any';
659-
660-
if (schema.enum || schema['x-enumDescriptions'] || schema['x-gitbook-enum']) {
661-
type = `${schema.type} · enum`;
662-
// check array AND schema.items as this is sometimes null despite what the type indicates
663-
} else if (schema.type === 'array' && !!schema.items) {
664-
type = `${getSchemaTitle(schema.items)}[]`;
665-
} else if (Array.isArray(schema.type)) {
666-
type = schema.type.join(' | ');
667-
} else if (schema.type || schema.properties) {
668-
type = schema.type ?? 'object';
669-
670-
if (schema.format) {
671-
type += ` · ${schema.format}`;
672-
}
673-
674-
// Only add the title if it's an object (no need for the title of a string, number, etc.)
675-
if (type === 'object' && schema.title) {
676-
type += ` · ${schema.title.replaceAll(' ', '')}`;
677-
}
678-
}
679-
680-
if ('anyOf' in schema) {
681-
type = 'any of';
682-
} else if ('oneOf' in schema) {
683-
type = 'one of';
684-
} else if ('allOf' in schema) {
685-
type = 'all of';
686-
} else if ('not' in schema) {
687-
type = 'not';
688-
}
689-
690-
return type;
691-
}

packages/react-openapi/src/OpenAPISelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ interface OpenAPISelectProps<T extends OpenAPISelectItem> extends Omit<SelectPro
3333
icon?: React.ReactNode;
3434
}
3535

36-
export function useSelectState(stateKey = 'select-state', initialKey?: Key) {
36+
export function useSelectState(stateKey = 'select-state', initialKey: Key = 'default') {
3737
const store = useStore(getOrCreateStoreByKey(stateKey, initialKey));
3838
return {
3939
key: store.key,

packages/react-openapi/src/utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,40 @@ function getStatusCodeCategory(statusCode: number | string): number | string {
216216

217217
return category;
218218
}
219+
220+
export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
221+
// Otherwise try to infer a nice title
222+
let type = 'any';
223+
224+
if (schema.enum || schema['x-enumDescriptions'] || schema['x-gitbook-enum']) {
225+
type = `${schema.type} · enum`;
226+
// check array AND schema.items as this is sometimes null despite what the type indicates
227+
} else if (schema.type === 'array' && !!schema.items) {
228+
type = `${getSchemaTitle(schema.items)}[]`;
229+
} else if (Array.isArray(schema.type)) {
230+
type = schema.type.join(' | ');
231+
} else if (schema.type || schema.properties) {
232+
type = schema.type ?? 'object';
233+
234+
if (schema.format) {
235+
type += ` · ${schema.format}`;
236+
}
237+
238+
// Only add the title if it's an object (no need for the title of a string, number, etc.)
239+
if (type === 'object' && schema.title) {
240+
type += ` · ${schema.title.replaceAll(' ', '')}`;
241+
}
242+
}
243+
244+
if ('anyOf' in schema) {
245+
type = 'any of';
246+
} else if ('oneOf' in schema) {
247+
type = 'one of';
248+
} else if ('allOf' in schema) {
249+
type = 'all of';
250+
} else if ('not' in schema) {
251+
type = 'not';
252+
}
253+
254+
return type;
255+
}

0 commit comments

Comments
 (0)