Skip to content

Commit 46fb3b5

Browse files
committed
Support multiple examples and multiple responses example
1 parent 6157583 commit 46fb3b5

File tree

9 files changed

+296
-77
lines changed

9 files changed

+296
-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
@@ -210,6 +210,7 @@
210210
"@scalar/oas-utils": "^0.2.101",
211211
"clsx": "^2.1.1",
212212
"flatted": "^3.2.9",
213+
"json-xml-parse": "^1.3.0",
213214
"react-aria": "^3.37.0",
214215
"react-aria-components": "^1.6.0",
215216
"usehooks-ts": "^3.1.0",
@@ -2400,6 +2401,8 @@
24002401

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

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

24052408
"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: 203 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,231 @@ 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}>
86+
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
87+
<OpenAPITabsPanels />
88+
</InteractiveSection>
89+
</OpenAPITabs>
90+
);
91+
}
92+
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('media-type-examples')} items={tabs}>
136+
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
137+
<OpenAPITabsPanels />
138+
</InteractiveSection>
139+
</OpenAPITabs>
140+
);
141+
}
142+
143+
function OpenAPIResponseMediaType(props: {
144+
mediaTypeObject: OpenAPIV3.MediaTypeObject;
145+
mediaType: string;
146+
context: OpenAPIContextProps;
147+
}) {
148+
const { mediaTypeObject, mediaType } = props;
149+
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
150+
const syntax = getSyntaxFromMediaType(mediaType);
151+
const firstExample = examples[0];
152+
153+
if (!firstExample) {
154+
return <OpenAPIEmptyResponseExample />;
155+
}
156+
157+
if (examples.length === 1) {
158+
return <OpenAPIExample example={firstExample} context={props.context} syntax={syntax} />;
159+
}
160+
161+
const tabs = examples.map((example) => {
162+
return {
163+
key: example.summary || 'Example',
164+
label: example.summary || 'Example',
165+
body: <OpenAPIExample example={firstExample} context={props.context} syntax={syntax} />,
166+
};
167+
});
168+
169+
return (
170+
<OpenAPITabs stateKey={createStateKey('media-type-examples')} items={tabs}>
119171
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
120172
<OpenAPITabsPanels />
121173
</InteractiveSection>
122174
</OpenAPITabs>
123175
);
124176
}
125177

178+
/**
179+
* Display an example.
180+
*/
181+
function OpenAPIExample(props: {
182+
example: OpenAPIV3.ExampleObject;
183+
context: OpenAPIContextProps;
184+
syntax: string;
185+
}) {
186+
const { example, context, syntax } = props;
187+
const code = stringifyExample({ example, xml: syntax === 'xml' });
188+
189+
if (code === null) {
190+
return <OpenAPIEmptyResponseExample />;
191+
}
192+
193+
return <context.CodeBlock code={code} syntax={syntax} />;
194+
}
195+
196+
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
197+
const { example, xml } = args;
198+
199+
if (!example.value) {
200+
return null;
201+
}
202+
203+
if (typeof example.value === 'string') {
204+
return example.value;
205+
}
206+
207+
if (xml) {
208+
return json2xml(example.value);
209+
}
210+
211+
return JSON.stringify(example.value, null, 2);
212+
}
213+
214+
/**
215+
* Get the syntax from a media type.
216+
*/
217+
function getSyntaxFromMediaType(mediaType: string): string {
218+
if (mediaType.includes('json')) {
219+
return 'json';
220+
}
221+
222+
if (mediaType === 'application/xml') {
223+
return 'xml';
224+
}
225+
226+
return 'text';
227+
}
228+
229+
/**
230+
* Get examples from a media type object.
231+
*/
232+
function getExamplesFromMediaTypeObject(args: {
233+
mediaType: string;
234+
mediaTypeObject: OpenAPIV3.MediaTypeObject;
235+
}): OpenAPIV3.ExampleObject[] {
236+
const { mediaTypeObject, mediaType } = args;
237+
if (mediaTypeObject.examples) {
238+
return Object.values(mediaTypeObject.examples).map((example) => {
239+
return checkIsReference(example) ? getExampleFromReference(example) : example;
240+
});
241+
}
242+
243+
if (mediaTypeObject.example) {
244+
return [{ value: mediaTypeObject.example }];
245+
}
246+
247+
if (mediaTypeObject.schema) {
248+
// @TODO normally we should use the name of the schema but we don't have it
249+
const root = mediaTypeObject.schema.xml?.name ?? 'object';
250+
return [
251+
{
252+
value: {
253+
[root]: generateSchemaExample(mediaTypeObject.schema, {
254+
xml: mediaType === 'application/xml',
255+
}),
256+
},
257+
},
258+
];
259+
}
260+
return [];
261+
}
262+
263+
/**
264+
* Empty response example.
265+
*/
126266
function OpenAPIEmptyResponseExample() {
127267
return (
128268
<pre className="openapi-response-example-empty">
@@ -131,15 +271,9 @@ function OpenAPIEmptyResponseExample() {
131271
);
132272
}
133273

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;
274+
/**
275+
* Generate an example from a reference object.
276+
*/
277+
function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
278+
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
145279
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Bun Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`getUrlFromServerState indents correctly 1`] = `
4+
"<?xml version="1.0"?>
5+
<id>10</id>
6+
<name>doggie</name>
7+
<category>
8+
<id>1</id>
9+
<name>Dogs</name>
10+
</category>
11+
<photoUrls>string</photoUrls>
12+
<tags>
13+
<id>0</id>
14+
<name>string</name>
15+
</tags>
16+
<status>available</status>
17+
"
18+
`;

0 commit comments

Comments
 (0)