Skip to content

Commit 883d10a

Browse files
authored
Support for OpenAPI references (#2813)
1 parent 3e5e458 commit 883d10a

12 files changed

+165
-119
lines changed

.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

packages/react-openapi/src/OpenAPICodeSample.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { generateMediaTypeExample, generateSchemaExample } from './generateSchem
66
import { InteractiveSection } from './InteractiveSection';
77
import { getServersURL } from './OpenAPIServerURL';
88
import { OpenAPIContextProps } from './types';
9-
import { noReference } from './utils';
109
import { stringifyOpenAPI } from './stringifyOpenAPI';
1110
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
11+
import { OpenAPIV3 } from '@scalar/openapi-types';
12+
import { checkIsReference } from './utils';
1213

1314
/**
1415
* Display code samples to execute the operation.
@@ -23,24 +24,19 @@ export function OpenAPICodeSample(props: {
2324
const searchParams = new URLSearchParams();
2425
const headersObject: { [k: string]: string } = {};
2526

26-
data.operation.parameters?.forEach((rawParam) => {
27-
const param = noReference(rawParam);
27+
data.operation.parameters?.forEach((param) => {
2828
if (!param) {
2929
return;
3030
}
3131

3232
if (param.in === 'header' && param.required) {
33-
const example = param.schema
34-
? generateSchemaExample(noReference(param.schema))
35-
: undefined;
33+
const example = param.schema ? generateSchemaExample(param.schema) : undefined;
3634
if (example !== undefined && param.name) {
3735
headersObject[param.name] =
3836
typeof example !== 'string' ? stringifyOpenAPI(example) : example;
3937
}
4038
} else if (param.in === 'query' && param.required) {
41-
const example = param.schema
42-
? generateSchemaExample(noReference(param.schema))
43-
: undefined;
39+
const example = param.schema ? generateSchemaExample(param.schema) : undefined;
4440
if (example !== undefined && param.name) {
4541
searchParams.append(
4642
param.name,
@@ -50,7 +46,9 @@ export function OpenAPICodeSample(props: {
5046
}
5147
});
5248

53-
const requestBody = noReference(data.operation.requestBody);
49+
const requestBody = !checkIsReference(data.operation.requestBody)
50+
? data.operation.requestBody
51+
: undefined;
5452
const requestBodyContentEntries = requestBody?.content
5553
? Object.entries(requestBody.content)
5654
: undefined;

packages/react-openapi/src/OpenAPIRequestBody.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@ import * as React from 'react';
22

33
import { OpenAPIV3 } from '@scalar/openapi-types';
44
import { OpenAPIRootSchema } from './OpenAPISchema';
5-
import { noReference } from './utils';
65
import { OpenAPIClientContext } from './types';
76
import { InteractiveSection } from './InteractiveSection';
7+
import { checkIsReference } from './utils';
88

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

18+
if (checkIsReference(requestBody)) {
19+
return null;
20+
}
21+
1822
return (
1923
<InteractiveSection
2024
header="Body"
@@ -26,7 +30,7 @@ export function OpenAPIRequestBody(props: {
2630
label: contentType,
2731
body: (
2832
<OpenAPIRootSchema
29-
schema={noReference(mediaTypeObject.schema) ?? {}}
33+
schema={mediaTypeObject.schema ?? {}}
3034
context={context}
3135
/>
3236
),
Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import classNames from 'classnames';
22
import { OpenAPIV3 } from '@scalar/openapi-types';
33
import { OpenAPISchemaProperties } from './OpenAPISchema';
4-
import { checkIsReference, noReference } from './utils';
54
import { OpenAPIClientContext } from './types';
65
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
76

@@ -15,7 +14,7 @@ export function OpenAPIResponse(props: {
1514
}) {
1615
const { response, context, mediaType } = props;
1716
const headers = Object.entries(response.headers ?? {}).map(
18-
([name, header]) => [name, noReference(header) ?? {}] as const,
17+
([name, header]) => [name, header ?? {}] as const,
1918
);
2019
const content = Object.entries(mediaType.schema ?? {});
2120

@@ -30,7 +29,7 @@ export function OpenAPIResponse(props: {
3029
<OpenAPISchemaProperties
3130
properties={headers.map(([name, header]) => ({
3231
propertyName: name,
33-
schema: noReference(header.schema) ?? {},
32+
schema: header.schema ?? {},
3433
required: header.required,
3534
}))}
3635
context={context}
@@ -42,7 +41,7 @@ export function OpenAPIResponse(props: {
4241
id={`response-${context.blockKey}`}
4342
properties={[
4443
{
45-
schema: handleUnresolvedReference(mediaType.schema) ?? {},
44+
schema: mediaType.schema ?? {},
4645
},
4746
]}
4847
context={context}
@@ -51,17 +50,3 @@ export function OpenAPIResponse(props: {
5150
</div>
5251
);
5352
}
54-
55-
function handleUnresolvedReference(
56-
input: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
57-
): OpenAPIV3.SchemaObject {
58-
const isReference = checkIsReference(input);
59-
60-
if (isReference || input === undefined) {
61-
// If we find a reference that wasn't resolved or needed to be resolved externally, do not try to render it.
62-
// Instead we render `any`
63-
return {};
64-
}
65-
66-
return input;
67-
}

packages/react-openapi/src/OpenAPIResponseExample.tsx

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
22
import { generateSchemaExample } from './generateSchemaExample';
33
import { OpenAPIContextProps } from './types';
4-
import { checkIsReference, noReference } from './utils';
54
import { stringifyOpenAPI } from './stringifyOpenAPI';
65
import { OpenAPIV3 } from '@scalar/openapi-types';
76
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
@@ -41,7 +40,7 @@ export function OpenAPIResponseExample(props: {
4140

4241
const examples = responses
4342
.map(([key, value]) => {
44-
const responseObject = noReference(value);
43+
const responseObject = value;
4544
const mediaTypeObject = (() => {
4645
if (!responseObject.content) {
4746
return null;
@@ -61,30 +60,28 @@ export function OpenAPIResponseExample(props: {
6160
};
6261
}
6362

64-
const example = handleUnresolvedReference(
65-
(() => {
66-
const { examples, example } = mediaTypeObject;
67-
if (examples) {
68-
const firstKey = Object.keys(examples)[0];
69-
// @TODO handle multiple examples
70-
const firstExample = noReference(examples[firstKey]);
71-
if (firstExample) {
72-
return firstExample;
73-
}
63+
const example: OpenAPIV3.ExampleObject | null = (() => {
64+
const { examples, example } = mediaTypeObject;
65+
if (examples) {
66+
const firstKey = Object.keys(examples)[0];
67+
// @TODO handle multiple examples
68+
const firstExample = examples[firstKey];
69+
if (firstExample) {
70+
return firstExample;
7471
}
72+
}
7573

76-
if (example) {
77-
return { value: example };
78-
}
74+
if (example) {
75+
return { value: example };
76+
}
7977

80-
const schema = noReference(mediaTypeObject.schema);
81-
if (!schema) {
82-
return null;
83-
}
78+
const schema = mediaTypeObject.schema;
79+
if (!schema) {
80+
return null;
81+
}
8482

85-
return { value: generateSchemaExample(schema) };
86-
})(),
87-
);
83+
return { value: generateSchemaExample(schema) };
84+
})();
8885

8986
return {
9087
key: key,
@@ -128,16 +125,3 @@ function OpenAPIEmptyResponseExample() {
128125
</pre>
129126
);
130127
}
131-
132-
function handleUnresolvedReference(
133-
input: OpenAPIV3.ExampleObject | null,
134-
): OpenAPIV3.ExampleObject | null {
135-
const isReference = checkIsReference(input?.value);
136-
137-
if (isReference) {
138-
// If we find a reference that wasn't resolved or needed to be resolved externally, render out the URL
139-
return { value: input.value.$ref };
140-
}
141-
142-
return input;
143-
}

packages/react-openapi/src/OpenAPIResponses.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import * as React from 'react';
22
import classNames from 'classnames';
3-
import { createStateKey, noReference } from './utils';
3+
import { createStateKey } from './utils';
44
import { OpenAPIResponse } from './OpenAPIResponse';
55
import { OpenAPIClientContext } from './types';
66
import { InteractiveSection } from './InteractiveSection';
77
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
88
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
99
import { Markdown } from './Markdown';
10-
import { OpenAPIRootSchema, OpenAPISchemaProperties, OpenAPISchemaProperty } from './OpenAPISchema';
11-
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
1210

1311
/**
1412
* Display an interactive response body.

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import React, { useId } from 'react';
55
import { InteractiveSection } from './InteractiveSection';
66
import { Markdown } from './Markdown';
77
import { OpenAPIClientContext } from './types';
8-
import { checkIsReference, noReference } from './utils';
98
import { stringifyOpenAPI } from './stringifyOpenAPI';
109
import { OpenAPISchemaName } from './OpenAPISchemaName';
1110
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
11+
import { checkIsReference } from './utils';
1212

1313
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
1414

@@ -275,9 +275,9 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
275275
function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
276276
if (schema.allOf) {
277277
return schema.allOf.reduce((acc, subSchema) => {
278-
const properties = getSchemaProperties(noReference(subSchema)) ?? [
278+
const properties = getSchemaProperties(subSchema) ?? [
279279
{
280-
schema: noReference(subSchema),
280+
schema: subSchema,
281281
},
282282
];
283283
return [...acc, ...properties];
@@ -286,7 +286,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
286286

287287
// check array AND schema.items as this is sometimes null despite what the type indicates
288288
if (schema.type === 'array' && !!schema.items) {
289-
const items = noReference(schema.items);
289+
const items = schema.items;
290290
const itemProperties = getSchemaProperties(items);
291291
if (itemProperties) {
292292
return itemProperties;
@@ -304,12 +304,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
304304
const result: OpenAPISchemaPropertyEntry[] = [];
305305

306306
if (schema.properties) {
307-
Object.entries(schema.properties).forEach(([propertyName, rawPropertySchema]) => {
308-
const isReference = checkIsReference(rawPropertySchema);
309-
const propertySchema: OpenAPIV3.SchemaObject = isReference
310-
? { propertyName }
311-
: rawPropertySchema;
312-
307+
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
313308
result.push({
314309
propertyName,
315310
required: Array.isArray(schema.required)
@@ -321,7 +316,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
321316
}
322317

323318
if (schema.additionalProperties) {
324-
const additionalProperties = noReference(schema.additionalProperties);
319+
const additionalProperties = schema.additionalProperties;
325320

326321
result.push({
327322
propertyName: 'Other properties',
@@ -345,17 +340,11 @@ export function getSchemaAlternatives(
345340
const downAncestors = new Set(ancestors).add(schema);
346341

347342
if (schema.anyOf) {
348-
return [
349-
flattenAlternatives('anyOf', schema.anyOf.map(noReference), downAncestors),
350-
noReference(schema.discriminator),
351-
];
343+
return [flattenAlternatives('anyOf', schema.anyOf, downAncestors), schema.discriminator];
352344
}
353345

354346
if (schema.oneOf) {
355-
return [
356-
flattenAlternatives('oneOf', schema.oneOf.map(noReference), downAncestors),
357-
noReference(schema.discriminator),
358-
];
347+
return [flattenAlternatives('oneOf', schema.oneOf, downAncestors), schema.discriminator];
359348
}
360349

361350
if (schema.allOf) {
@@ -393,8 +382,8 @@ export function getSchemaTitle(
393382

394383
// Try using the discriminator
395384
if (discriminator?.propertyName && schema.properties) {
396-
const discriminatorProperty = noReference(schema.properties[discriminator.propertyName]);
397-
if (discriminatorProperty) {
385+
const discriminatorProperty = schema.properties[discriminator.propertyName];
386+
if (discriminatorProperty && !checkIsReference(discriminatorProperty)) {
398387
if (discriminatorProperty.enum) {
399388
return discriminatorProperty.enum.map((value) => value.toString()).join(' | ');
400389
}
@@ -408,7 +397,7 @@ export function getSchemaTitle(
408397
type = 'enum';
409398
// check array AND schema.items as this is sometimes null despite what the type indicates
410399
} else if (schema.type === 'array' && !!schema.items) {
411-
type = `${getSchemaTitle(noReference(schema.items))}[]`;
400+
type = `${getSchemaTitle(schema.items)}[]`;
412401
} else if (Array.isArray(schema.type)) {
413402
type = schema.type.join(' | ');
414403
} else if (schema.type || schema.properties) {

packages/react-openapi/src/OpenAPISpec.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
import { OpenAPI } from '@scalar/openapi-types';
44

5-
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
5+
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
66
import { InteractiveSection } from './InteractiveSection';
77
import { OpenAPIRequestBody } from './OpenAPIRequestBody';
88
import { OpenAPIResponses } from './OpenAPIResponses';
99
import { OpenAPISchemaProperties } from './OpenAPISchema';
1010
import { OpenAPISecurities } from './OpenAPISecurities';
1111
import { OpenAPIClientContext } from './types';
12-
import { noReference } from './utils';
1312

1413
/**
1514
* Client component to render the spec for the request and response.
@@ -47,7 +46,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
4746
example: parameter.example,
4847
// Deprecated can be defined at the parameter level
4948
deprecated: parameter.deprecated,
50-
...(noReference(parameter.schema) ?? {}),
49+
...(parameter.schema ?? {}),
5150
},
5251
required: parameter.required,
5352
}))}
@@ -57,13 +56,10 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
5756
))}
5857

5958
{operation.requestBody ? (
60-
<OpenAPIRequestBody
61-
requestBody={noReference(operation.requestBody)}
62-
context={context}
63-
/>
59+
<OpenAPIRequestBody requestBody={operation.requestBody} context={context} />
6460
) : null}
6561
{operation.responses ? (
66-
<OpenAPIResponses responses={noReference(operation.responses)} context={context} />
62+
<OpenAPIResponses responses={operation.responses} context={context} />
6763
) : null}
6864
</>
6965
);

0 commit comments

Comments
 (0)