Skip to content

Commit bb5c6a4

Browse files
authored
Support multiple examples and multiple responses example (#2856)
1 parent 445baaa commit bb5c6a4

File tree

9 files changed

+329
-77
lines changed

9 files changed

+329
-77
lines changed

.changeset/green-kings-fix.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 multiple response media types and examples

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@
218218
"@scalar/oas-utils": "^0.2.101",
219219
"clsx": "^2.1.1",
220220
"flatted": "^3.2.9",
221+
"json-xml-parse": "^1.3.0",
221222
"react-aria": "^3.37.0",
222223
"react-aria-components": "^1.6.0",
223224
"usehooks-ts": "^3.1.0",
@@ -2410,6 +2411,8 @@
24102411

24112412
"json-stringify-deterministic": ["[email protected]", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
24122413

2414+
"json-xml-parse": ["[email protected]", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
2415+
24132416
"json5": ["[email protected]", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
24142417

24152418
"jsonfile": ["[email protected]", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],

packages/react-openapi/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@scalar/oas-utils": "^0.2.101",
1717
"clsx": "^2.1.1",
1818
"flatted": "^3.2.9",
19+
"json-xml-parse": "^1.3.0",
1920
"react-aria-components": "^1.6.0",
2021
"react-aria": "^3.37.0",
2122
"usehooks-ts": "^3.1.0",

packages/react-openapi/src/OpenAPICodeSample.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ export function OpenAPICodeSample(props: {
5858
(searchParams.size ? `?${searchParams.toString()}` : ''),
5959
method: data.method,
6060
body: requestBodyContent
61-
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
61+
? generateMediaTypeExample(requestBodyContent[1], {
62+
omitEmptyAndOptionalProperties: true,
63+
})
6264
: undefined,
6365
headers: {
6466
...getSecurityHeaders(data.securities),

packages/react-openapi/src/OpenAPIResponseExample.tsx

Lines changed: 236 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
22
import { generateSchemaExample } from './generateSchemaExample';
33
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
44
import { checkIsReference, createStateKey, resolveDescription } from './utils';
5-
import { stringifyOpenAPI } from './stringifyOpenAPI';
65
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
76
import { InteractiveSection } from './InteractiveSection';
7+
import { json2xml } from './json2xml';
88

99
/**
1010
* Display an example of the response content.
@@ -38,91 +38,264 @@ export function OpenAPIResponseExample(props: {
3838
return Number(a) - Number(b);
3939
});
4040

41-
const examples = responses
42-
.map(([key, value]) => {
43-
const responseObject = value;
44-
const mediaTypeObject = (() => {
45-
if (!responseObject.content) {
46-
return null;
47-
}
48-
const key = Object.keys(responseObject.content)[0];
49-
return (
50-
responseObject.content['application/json'] ??
51-
(key ? responseObject.content[key] : null)
52-
);
53-
})();
54-
55-
if (!mediaTypeObject) {
41+
const tabs = responses
42+
.map(([key, responseObject]) => {
43+
const description = resolveDescription(responseObject);
44+
45+
if (checkIsReference(responseObject)) {
5646
return {
5747
key: key,
5848
label: key,
59-
description: resolveDescription(responseObject),
60-
body: <OpenAPIEmptyResponseExample />,
49+
description,
50+
body: (
51+
<OpenAPIExample
52+
example={getExampleFromReference(responseObject)}
53+
context={context}
54+
syntax="json"
55+
/>
56+
),
6157
};
6258
}
6359

64-
const example = handleUnresolvedReference(
65-
(() => {
66-
const { examples, example } = mediaTypeObject;
67-
if (examples) {
68-
const key = Object.keys(examples)[0];
69-
if (key) {
70-
// @TODO handle multiple examples
71-
const firstExample = examples[key];
72-
if (firstExample) {
73-
return firstExample;
74-
}
75-
}
76-
}
77-
78-
if (example) {
79-
return { value: example };
80-
}
81-
82-
const schema = mediaTypeObject.schema;
83-
if (!schema) {
84-
return null;
85-
}
86-
87-
return { value: generateSchemaExample(schema) };
88-
})(),
89-
);
60+
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
61+
return {
62+
key: key,
63+
label: key,
64+
description,
65+
body: <OpenAPIEmptyResponseExample />,
66+
};
67+
}
9068

9169
return {
9270
key: key,
9371
label: key,
9472
description: resolveDescription(responseObject),
95-
body: example?.value ? (
96-
<context.CodeBlock
97-
code={
98-
typeof example.value === 'string'
99-
? example.value
100-
: stringifyOpenAPI(example.value, null, 2)
101-
}
102-
syntax="json"
103-
/>
104-
) : (
105-
<OpenAPIEmptyResponseExample />
106-
),
73+
body: <OpenAPIResponse context={context} content={responseObject.content} />,
10774
};
10875
})
10976
.filter((val): val is { key: string; label: string; body: any; description: string } =>
11077
Boolean(val),
11178
);
11279

113-
if (examples.length === 0) {
80+
if (tabs.length === 0) {
11481
return null;
11582
}
11683

11784
return (
118-
<OpenAPITabs stateKey={createStateKey('response-example')} items={examples}>
85+
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
11986
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
12087
<OpenAPITabsPanels />
12188
</InteractiveSection>
12289
</OpenAPITabs>
12390
);
12491
}
12592

93+
function OpenAPIResponse(props: {
94+
context: OpenAPIContextProps;
95+
content: {
96+
[media: string]: OpenAPIV3.MediaTypeObject;
97+
};
98+
}) {
99+
const { context, content } = props;
100+
101+
const entries = Object.entries(content);
102+
const firstEntry = entries[0];
103+
104+
if (!firstEntry) {
105+
throw new Error('One media type is required');
106+
}
107+
108+
if (entries.length === 1) {
109+
const [mediaType, mediaTypeObject] = firstEntry;
110+
return (
111+
<OpenAPIResponseMediaType
112+
context={context}
113+
mediaType={mediaType}
114+
mediaTypeObject={mediaTypeObject}
115+
/>
116+
);
117+
}
118+
119+
const tabs = entries.map((entry) => {
120+
const [mediaType, mediaTypeObject] = entry;
121+
return {
122+
key: mediaType,
123+
label: mediaType,
124+
body: (
125+
<OpenAPIResponseMediaType
126+
context={context}
127+
mediaType={mediaType}
128+
mediaTypeObject={mediaTypeObject}
129+
/>
130+
),
131+
};
132+
});
133+
134+
return (
135+
<OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}>
136+
<InteractiveSection
137+
header={<OpenAPITabsList />}
138+
className="openapi-response-media-types"
139+
>
140+
<OpenAPITabsPanels />
141+
</InteractiveSection>
142+
</OpenAPITabs>
143+
);
144+
}
145+
146+
function OpenAPIResponseMediaType(props: {
147+
mediaTypeObject: OpenAPIV3.MediaTypeObject;
148+
mediaType: string;
149+
context: OpenAPIContextProps;
150+
}) {
151+
const { mediaTypeObject, mediaType } = props;
152+
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
153+
const syntax = getSyntaxFromMediaType(mediaType);
154+
const firstExample = examples[0];
155+
156+
if (!firstExample) {
157+
return <OpenAPIEmptyResponseExample />;
158+
}
159+
160+
if (examples.length === 1) {
161+
return (
162+
<OpenAPIExample
163+
example={firstExample.example}
164+
context={props.context}
165+
syntax={syntax}
166+
/>
167+
);
168+
}
169+
170+
const tabs = examples.map((example) => {
171+
return {
172+
key: example.key,
173+
label: example.example.summary || example.key,
174+
body: (
175+
<OpenAPIExample
176+
example={firstExample.example}
177+
context={props.context}
178+
syntax={syntax}
179+
/>
180+
),
181+
};
182+
});
183+
184+
return (
185+
<OpenAPITabs stateKey={createStateKey('response-media-type-examples')} items={tabs}>
186+
<InteractiveSection
187+
header={<OpenAPITabsList />}
188+
className="openapi-response-media-type-examples"
189+
>
190+
<OpenAPITabsPanels />
191+
</InteractiveSection>
192+
</OpenAPITabs>
193+
);
194+
}
195+
196+
/**
197+
* Display an example.
198+
*/
199+
function OpenAPIExample(props: {
200+
example: OpenAPIV3.ExampleObject;
201+
context: OpenAPIContextProps;
202+
syntax: string;
203+
}) {
204+
const { example, context, syntax } = props;
205+
const code = stringifyExample({ example, xml: syntax === 'xml' });
206+
207+
if (code === null) {
208+
return <OpenAPIEmptyResponseExample />;
209+
}
210+
211+
return <context.CodeBlock code={code} syntax={syntax} />;
212+
}
213+
214+
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
215+
const { example, xml } = args;
216+
217+
if (!example.value) {
218+
return null;
219+
}
220+
221+
if (typeof example.value === 'string') {
222+
return example.value;
223+
}
224+
225+
if (xml) {
226+
return json2xml(example.value);
227+
}
228+
229+
return JSON.stringify(example.value, null, 2);
230+
}
231+
232+
/**
233+
* Get the syntax from a media type.
234+
*/
235+
function getSyntaxFromMediaType(mediaType: string): string {
236+
if (mediaType.includes('json')) {
237+
return 'json';
238+
}
239+
240+
if (mediaType === 'application/xml') {
241+
return 'xml';
242+
}
243+
244+
return 'text';
245+
}
246+
247+
/**
248+
* Get examples from a media type object.
249+
*/
250+
function getExamplesFromMediaTypeObject(args: {
251+
mediaType: string;
252+
mediaTypeObject: OpenAPIV3.MediaTypeObject;
253+
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
254+
const { mediaTypeObject, mediaType } = args;
255+
if (mediaTypeObject.examples) {
256+
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
257+
return {
258+
key,
259+
example: checkIsReference(example) ? getExampleFromReference(example) : example,
260+
};
261+
});
262+
}
263+
264+
if (mediaTypeObject.example) {
265+
return [{ key: 'default', example: { value: mediaTypeObject.example } }];
266+
}
267+
268+
if (mediaTypeObject.schema) {
269+
if (mediaType === 'application/xml') {
270+
// @TODO normally we should use the name of the schema but we don't have it
271+
// fix it when we got the reference name
272+
const root = mediaTypeObject.schema.xml?.name ?? 'object';
273+
return [
274+
{
275+
key: 'default',
276+
example: {
277+
value: {
278+
[root]: generateSchemaExample(mediaTypeObject.schema, {
279+
xml: mediaType === 'application/xml',
280+
}),
281+
},
282+
},
283+
},
284+
];
285+
}
286+
return [
287+
{
288+
key: 'default',
289+
example: { value: generateSchemaExample(mediaTypeObject.schema) },
290+
},
291+
];
292+
}
293+
return [];
294+
}
295+
296+
/**
297+
* Empty response example.
298+
*/
126299
function OpenAPIEmptyResponseExample() {
127300
return (
128301
<pre className="openapi-response-example-empty">
@@ -131,15 +304,9 @@ function OpenAPIEmptyResponseExample() {
131304
);
132305
}
133306

134-
function handleUnresolvedReference(
135-
input: OpenAPIV3.ExampleObject | null,
136-
): OpenAPIV3.ExampleObject | null {
137-
const isReference = checkIsReference(input?.value);
138-
139-
if (isReference) {
140-
// If we find a reference that wasn't resolved or needed to be resolved externally, render out the URL
141-
return { value: input.value.$ref };
142-
}
143-
144-
return input;
307+
/**
308+
* Generate an example from a reference object.
309+
*/
310+
function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
311+
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
145312
}

0 commit comments

Comments
 (0)