Skip to content

Commit 8438cbb

Browse files
committed
Refactor OpenAPI examples code
1 parent 2c13f8b commit 8438cbb

File tree

4 files changed

+106
-61
lines changed

4 files changed

+106
-61
lines changed

packages/react-openapi/src/OpenAPICodeSample.tsx

+84-47
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,82 @@ import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
33
import { ScalarApiButton } from './ScalarApiButton';
44
import { StaticSection } from './StaticSection';
55
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
6-
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
6+
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
77
import { stringifyOpenAPI } from './stringifyOpenAPI';
88
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
99
import { getDefaultServerURL } from './util/server';
1010
import { checkIsReference, createStateKey } from './utils';
1111

12+
const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
13+
1214
/**
1315
* Display code samples to execute the operation.
1416
* It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/)
1517
*/
1618
export function OpenAPICodeSample(props: {
1719
data: OpenAPIOperationData;
1820
context: OpenAPIContextProps;
21+
}) {
22+
const { data } = props;
23+
24+
// If code samples are disabled at operation level, we don't display the code samples.
25+
if (data.operation['x-codeSamples'] === false) {
26+
return null;
27+
}
28+
29+
const customCodeSamples = getCustomCodeSamples(props);
30+
31+
// If code samples are disabled at the top-level and not custom code samples are defined,
32+
// we don't display the code samples.
33+
if (data['x-codeSamples'] === false && !customCodeSamples) {
34+
return null;
35+
}
36+
37+
const samples = customCodeSamples ?? generateCodeSamples(props);
38+
39+
if (samples.length === 0) {
40+
return null;
41+
}
42+
43+
return (
44+
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
45+
<StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
46+
<OpenAPITabsPanels />
47+
</StaticSection>
48+
</OpenAPITabs>
49+
);
50+
}
51+
52+
function OpenAPICodeSampleFooter(props: {
53+
data: OpenAPIOperationData;
54+
context: OpenAPIContextProps;
55+
}) {
56+
const { data, context } = props;
57+
const { method, path } = data;
58+
const { specUrl } = context;
59+
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
60+
61+
if (hideTryItPanel) {
62+
return null;
63+
}
64+
65+
if (!validateHttpMethod(method)) {
66+
return null;
67+
}
68+
69+
return (
70+
<div className="openapi-codesample-footer">
71+
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
72+
</div>
73+
);
74+
}
75+
76+
/**
77+
* Generate code samples for the operation.
78+
*/
79+
function generateCodeSamples(props: {
80+
data: OpenAPIOperationData;
81+
context: OpenAPIContextProps;
1982
}) {
2083
const { data, context } = props;
2184

@@ -56,13 +119,17 @@ export function OpenAPICodeSample(props: {
56119
: undefined;
57120
const requestBodyContent = requestBodyContentEntries?.[0];
58121

122+
const requestBodyExamples = requestBodyContent
123+
? generateMediaTypeExamples(requestBodyContent[1])
124+
: [];
125+
59126
const input: CodeSampleInput = {
60127
url:
61128
getDefaultServerURL(data.servers) +
62129
data.path +
63130
(searchParams.size ? `?${searchParams.toString()}` : ''),
64131
method: data.method,
65-
body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1]) : undefined,
132+
body: requestBodyExamples[0]?.value,
66133
headers: {
67134
...getSecurityHeaders(data.securities),
68135
...headersObject,
@@ -74,7 +141,7 @@ export function OpenAPICodeSample(props: {
74141
},
75142
};
76143

77-
const autoCodeSamples = codeSampleGenerators.map((generator) => ({
144+
return codeSampleGenerators.map((generator) => ({
78145
key: `default-${generator.id}`,
79146
label: generator.label,
80147
body: context.renderCodeBlock({
@@ -83,14 +150,24 @@ export function OpenAPICodeSample(props: {
83150
}),
84151
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
85152
}));
153+
}
154+
155+
/**
156+
* Get custom code samples for the operation.
157+
*/
158+
function getCustomCodeSamples(props: {
159+
data: OpenAPIOperationData;
160+
context: OpenAPIContextProps;
161+
}) {
162+
const { data, context } = props;
86163

87-
// Use custom samples if defined
88164
let customCodeSamples: null | Array<{
89165
key: string;
90166
label: string;
91167
body: React.ReactNode;
92168
}> = null;
93-
(['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const).forEach((key) => {
169+
170+
CUSTOM_CODE_SAMPLES_KEYS.forEach((key) => {
94171
const customSamples = data.operation[key];
95172
if (customSamples && Array.isArray(customSamples)) {
96173
customCodeSamples = customSamples
@@ -102,7 +179,7 @@ export function OpenAPICodeSample(props: {
102179
);
103180
})
104181
.map((sample, index) => ({
105-
key: `redocly-${sample.lang}-${index}`,
182+
key: `custom-sample-${sample.lang}-${index}`,
106183
label: sample.label,
107184
body: context.renderCodeBlock({
108185
code: sample.source,
@@ -113,47 +190,7 @@ export function OpenAPICodeSample(props: {
113190
}
114191
});
115192

116-
// Code samples can be disabled at the top-level or at the operation level
117-
// If code samples are defined at the operation level, it will override the top-level setting
118-
const codeSamplesDisabled =
119-
data['x-codeSamples'] === false || data.operation['x-codeSamples'] === false;
120-
const samples = customCodeSamples ?? (!codeSamplesDisabled ? autoCodeSamples : []);
121-
122-
if (samples.length === 0) {
123-
return null;
124-
}
125-
126-
return (
127-
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
128-
<StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
129-
<OpenAPITabsPanels />
130-
</StaticSection>
131-
</OpenAPITabs>
132-
);
133-
}
134-
135-
function OpenAPICodeSampleFooter(props: {
136-
data: OpenAPIOperationData;
137-
context: OpenAPIContextProps;
138-
}) {
139-
const { data, context } = props;
140-
const { method, path } = data;
141-
const { specUrl } = context;
142-
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
143-
144-
if (hideTryItPanel) {
145-
return null;
146-
}
147-
148-
if (!validateHttpMethod(method)) {
149-
return null;
150-
}
151-
152-
return (
153-
<div className="openapi-codesample-footer">
154-
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
155-
</div>
156-
);
193+
return customCodeSamples;
157194
}
158195

159196
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {

packages/react-openapi/src/OpenAPIResponseExample.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ function OpenAPIResponseMediaType(props: {
145145
}) {
146146
const { mediaTypeObject, mediaType } = props;
147147
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
148+
console.log(examples, mediaType);
148149
const syntax = getSyntaxFromMediaType(mediaType);
149150
const firstExample = examples[0];
150151

packages/react-openapi/src/generateSchemaExample.ts

+20-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
22
import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
3+
import { checkIsReference } from './utils';
34

45
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
56

@@ -42,27 +43,35 @@ export function generateSchemaExample(
4243
/**
4344
* Generate an example for a media type.
4445
*/
45-
export function generateMediaTypeExample(
46+
export function generateMediaTypeExamples(
4647
mediaType: OpenAPIV3.MediaTypeObject,
4748
options?: GenerateSchemaExampleOptions
48-
): JSONValue | undefined {
49+
): OpenAPIV3.ExampleObject[] {
4950
if (mediaType.example) {
50-
return mediaType.example;
51+
return [{ summary: 'default', value: mediaType.example }];
5152
}
5253

5354
if (mediaType.examples) {
54-
const key = Object.keys(mediaType.examples)[0];
55-
if (key) {
56-
const example = mediaType.examples[key];
57-
if (example) {
58-
return example.value;
59-
}
55+
const { examples } = mediaType;
56+
const keys = Object.keys(examples);
57+
if (keys.length > 0) {
58+
return keys.reduce<OpenAPIV3.ExampleObject[]>((result, key) => {
59+
const example = examples[key];
60+
if (!example || checkIsReference(example)) {
61+
return result;
62+
}
63+
result.push({
64+
summary: example.summary || key,
65+
value: example.value,
66+
});
67+
return result;
68+
}, []);
6069
}
6170
}
6271

6372
if (mediaType.schema) {
64-
return generateSchemaExample(mediaType.schema, options);
73+
return [{ summary: 'default', value: generateSchemaExample(mediaType.schema, options) }];
6574
}
6675

67-
return undefined;
76+
return [];
6877
}

packages/react-openapi/src/utils.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
22
import { stringifyOpenAPI } from './stringifyOpenAPI';
33

4-
export function checkIsReference(
5-
input: unknown
6-
): input is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
4+
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
75
return typeof input === 'object' && !!input && '$ref' in input;
86
}
97

0 commit comments

Comments
 (0)