Improve OpenAPI schemas block ungrouped style (#3092)

This commit is contained in:
Greg Bergé
2025-04-04 14:28:44 +02:00
committed by GitHub
parent 653920dec9
commit e59076a074
21 changed files with 479 additions and 328 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX.
@@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks';
export function PageClientLayout(props: { withSections?: boolean }) { export function PageClientLayout(props: { withSections?: boolean }) {
// We use this hook in the page layout to ensure the elements for the blocks // We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to a hash or to the top of the page // are rendered before we scroll to a hash or to the top of the page
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined }); useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
useStripFallbackQueryParam(); useStripFallbackQueryParam();
return null; return null;
@@ -20,7 +20,15 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
return ( return (
<Tag <Tag
id={id} id={id}
className={tcls(textStyle.textSize, 'heading', 'group', 'relative', 'grid', style)} className={tcls(
textStyle.textSize,
'heading',
'group',
'relative',
'grid',
'scroll-m-12',
style
)}
> >
<div <div
className={tcls( className={tcls(
@@ -1,18 +1,14 @@
import type { JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { OpenAPIOperation as BaseOpenAPIOperation } from '@gitbook/react-openapi'; import { OpenAPIOperation as BaseOpenAPIOperation } from '@gitbook/react-openapi';
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock'; import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
import { Heading } from '../Heading';
import './scalar.css'; import './scalar.css';
import './style.css'; import './style.css';
import type { AnyOpenAPIOperationsBlock } from '@/lib/openapi/types'; import type { AnyOpenAPIOperationsBlock } from '@/lib/openapi/types';
import { getOpenAPIContext } from './context';
/** /**
* Render an openapi block or an openapi-operation block. * Render an openapi block or an openapi-operation block.
@@ -55,56 +51,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return ( return (
<BaseOpenAPIOperation <BaseOpenAPIOperation
data={data} data={data}
context={{ context={getOpenAPIContext({ props, specUrl })}
specUrl,
icons: {
chevronDown: <Icon icon="chevron-down" />,
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
},
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
renderDocument: (documentProps) => (
<DocumentView
document={documentProps.document as JSONDocument}
context={props.context}
style="space-y-6"
blockStyle="max-w-full"
/>
),
renderHeading: (headingProps) => (
<Heading
document={props.document}
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={tcls([
headingProps.deprecated ? 'line-through' : undefined,
headingProps.deprecated || !!headingProps.stability
? '[&>div]:mt-0'
: undefined,
])}
block={{
object: 'block',
key: `${block.key}-heading`,
meta: block.meta,
data: {},
type: 'heading-2',
nodes: [
{
key: `${block.key}-heading-text`,
object: 'text',
leaves: [
{ text: headingProps.title, object: 'leaf', marks: [] },
],
},
],
}}
/>
),
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
}}
className="openapi-block" className="openapi-block"
/> />
); );
@@ -1,6 +1,5 @@
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock'; import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { OpenAPISchemas as BaseOpenAPISchemas } from '@gitbook/react-openapi'; import { OpenAPISchemas as BaseOpenAPISchemas } from '@gitbook/react-openapi';
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
@@ -8,6 +7,7 @@ import type { BlockProps } from '../Block';
import './scalar.css'; import './scalar.css';
import './style.css'; import './style.css';
import type { OpenAPISchemasBlock } from '@/lib/openapi/types'; import type { OpenAPISchemasBlock } from '@/lib/openapi/types';
import { getOpenAPIContext } from './context';
/** /**
* Render an openapi-schemas block. * Render an openapi-schemas block.
@@ -49,19 +49,9 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return ( return (
<BaseOpenAPISchemas <BaseOpenAPISchemas
data={data} schemas={data.schemas}
grouped={block.data.grouped} grouped={block.data.grouped}
context={{ context={getOpenAPIContext({ props, specUrl })}
specUrl,
icons: {
chevronDown: <Icon icon="chevron-down" />,
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
},
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
}}
className="openapi-block" className="openapi-block"
/> />
); );
@@ -0,0 +1,73 @@
import type { JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import type { OpenAPIContext } from '@gitbook/react-openapi';
import { tcls } from '@/lib/tailwind';
import type { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
import { Heading } from '../Heading';
import './scalar.css';
import './style.css';
import type { AnyOpenAPIOperationsBlock, OpenAPISchemasBlock } from '@/lib/openapi/types';
/**
* Get the OpenAPI context to render a block.
*/
export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock>;
specUrl: string;
}): OpenAPIContext {
const { props, specUrl } = args;
const { block } = props;
return {
specUrl,
icons: {
chevronDown: <Icon icon="chevron-down" />,
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
},
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
renderDocument: (documentProps) => (
<DocumentView
document={documentProps.document as JSONDocument}
context={props.context}
style="space-y-6"
blockStyle="max-w-full"
/>
),
renderHeading: (headingProps) => (
<Heading
document={props.document}
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={tcls([
headingProps.deprecated ? 'line-through' : undefined,
headingProps.deprecated || !!headingProps.stability
? '[&>div]:mt-0'
: undefined,
])}
block={{
object: 'block',
key: `${block.key}-heading`,
meta: block.meta,
data: {},
type: 'heading-2',
nodes: [
{
key: `${block.key}-heading-text`,
object: 'text',
leaves: [{ text: headingProps.title, object: 'leaf', marks: [] }],
},
],
}}
/>
),
defaultInteractiveOpened: props.context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
};
}
@@ -1,5 +1,6 @@
/* Layout Components */ /* Layout Components */
.openapi-operation { .openapi-operation,
.openapi-schemas {
@apply flex-1 flex flex-col gap-8 mb-14 min-w-0; @apply flex-1 flex flex-col gap-8 mb-14 min-w-0;
} }
@@ -17,7 +18,7 @@
} }
.openapi-summary { .openapi-summary {
@apply flex flex-col items-start justify-start gap-3; @apply flex flex-col items-start justify-start gap-3 scroll-m-12;
} }
.openapi-summary-tags { .openapi-summary-tags {
@@ -483,12 +484,30 @@
@apply flex flex-row items-center py-2 px-3 justify-end border-t border-tint-subtle; @apply flex flex-row items-center py-2 px-3 justify-end border-t border-tint-subtle;
} }
/* Response Example */ /* Panel */
.openapi-response-example { .openapi-panel {
@apply border rounded bg-tint border-tint-subtle; @apply border rounded bg-tint border-tint-subtle;
} }
.openapi-response-example-empty { .openapi-panel-heading {
@apply font-medium px-4 py-2 text-xs uppercase;
}
.openapi-panel-body {
@apply theme-gradient:bg-tint-12/1 relative;
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
}
.openapi-panel-footer {
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
}
.openapi-panel-footer .openapi-markdown {
@apply text-[0.813rem] text-tint;
}
/* Example */
.openapi-example-empty {
@apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center; @apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center;
} }
@@ -559,15 +578,6 @@
.openapi-tabs-panel { .openapi-tabs-panel {
@apply flex-1 text-sm relative focus-visible:outline-none; @apply flex-1 text-sm relative focus-visible:outline-none;
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
}
.openapi-tabs-footer {
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
}
.openapi-tabs-footer .openapi-markdown {
@apply text-[0.813rem] text-tint;
} }
/* Disclosure group */ /* Disclosure group */
@@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks';
export function PageClientLayout(props: { withSections?: boolean }) { export function PageClientLayout(props: { withSections?: boolean }) {
// We use this hook in the page layout to ensure the elements for the blocks // We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to a hash or to the top of the page // are rendered before we scroll to a hash or to the top of the page
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined }); useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
useStripFallbackQueryParam(); useStripFallbackQueryParam();
return null; return null;
@@ -3,6 +3,7 @@ import type { GitBookAnyContext } from '@v2/lib/context';
import { getNodeText } from './document'; import { getNodeText } from './document';
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock'; import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
import { resolveOpenAPISchemasBlock } from './openapi/resolveOpenAPISchemasBlock';
export interface DocumentSection { export interface DocumentSection {
id: string; id: string;
@@ -52,6 +53,26 @@ export async function getDocumentSections(
}); });
} }
} }
if (
block.type === 'openapi-schemas' &&
!block.data.grouped &&
block.meta?.id &&
block.data.schemas.length === 1
) {
const { data } = await resolveOpenAPISchemasBlock({
block,
context,
});
const schema = data?.schemas[0];
if (schema) {
sections.push({
id: block.meta.id,
title: `The ${schema.name} object`,
depth: 1,
});
}
}
} }
return sections; return sections;
@@ -1,5 +1,5 @@
import { OpenAPIParseError } from '@gitbook/openapi-parser'; import { OpenAPIParseError, type OpenAPISchema } from '@gitbook/openapi-parser';
import { type OpenAPISchemasData, resolveOpenAPISchemas } from '@gitbook/react-openapi'; import { resolveOpenAPISchemas } from '@gitbook/react-openapi';
import { fetchOpenAPIFilesystem } from './fetch'; import { fetchOpenAPIFilesystem } from './fetch';
import type { import type {
OpenAPISchemasBlock, OpenAPISchemasBlock,
@@ -7,7 +7,9 @@ import type {
ResolveOpenAPIBlockResult, ResolveOpenAPIBlockResult,
} from './types'; } from './types';
type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<OpenAPISchemasData>; type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<{
schemas: OpenAPISchema[];
}>;
const weakmap = new WeakMap<OpenAPISchemasBlock, Promise<ResolveOpenAPISchemasBlockResult>>(); const weakmap = new WeakMap<OpenAPISchemasBlock, Promise<ResolveOpenAPISchemasBlockResult>>();
@@ -9,7 +9,7 @@ import { StaticSection } from './StaticSection';
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples'; import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample'; import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
import { stringifyOpenAPI } from './stringifyOpenAPI'; import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server'; import { getDefaultServerURL } from './util/server';
import { checkIsReference, createStateKey } from './utils'; import { checkIsReference, createStateKey } from './utils';
@@ -21,7 +21,7 @@ const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-code
*/ */
export function OpenAPICodeSample(props: { export function OpenAPICodeSample(props: {
data: OpenAPIOperationData; data: OpenAPIOperationData;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { data } = props; const { data } = props;
@@ -58,7 +58,7 @@ export function OpenAPICodeSample(props: {
*/ */
function generateCodeSamples(props: { function generateCodeSamples(props: {
data: OpenAPIOperationData; data: OpenAPIOperationData;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { data, context } = props; const { data, context } = props;
@@ -189,7 +189,7 @@ export interface MediaTypeRenderer {
function OpenAPICodeSampleFooter(props: { function OpenAPICodeSampleFooter(props: {
data: OpenAPIOperationData; data: OpenAPIOperationData;
renderers: MediaTypeRenderer[]; renderers: MediaTypeRenderer[];
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { data, context, renderers } = props; const { data, context, renderers } = props;
const { method, path } = data; const { method, path } = data;
@@ -227,7 +227,7 @@ function OpenAPICodeSampleFooter(props: {
*/ */
function getCustomCodeSamples(props: { function getCustomCodeSamples(props: {
data: OpenAPIOperationData; data: OpenAPIOperationData;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { data, context } = props; const { data, context } = props;
@@ -0,0 +1,129 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { generateSchemaExample } from './generateSchemaExample';
import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIContext } from './types';
import { checkIsReference } from './utils';
/**
* Display an example.
*/
export function OpenAPIExample(props: {
example: OpenAPIV3.ExampleObject;
context: OpenAPIContext;
syntax: string;
}) {
const { example, context, syntax } = props;
const code = stringifyExample({ example, xml: syntax === 'xml' });
if (code === null) {
return <OpenAPIEmptyExample />;
}
return context.renderCodeBlock({ code, syntax });
}
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
const { example, xml } = args;
if (!example.value) {
return null;
}
if (typeof example.value === 'string') {
return example.value;
}
if (xml) {
return json2xml(example.value);
}
return stringifyOpenAPI(example.value, null, 2);
}
/**
* Empty response example.
*/
export function OpenAPIEmptyExample() {
return (
<pre className="openapi-example-empty">
<p>No Content</p>
</pre>
);
}
/**
* Generate an example from a reference object.
*/
export function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
}
/**
* Get examples from a media type object.
*/
export function getExamplesFromMediaTypeObject(args: {
mediaType: string;
mediaTypeObject: OpenAPIV3.MediaTypeObject;
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
const { mediaTypeObject, mediaType } = args;
if (mediaTypeObject.examples) {
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
return {
key,
example: checkIsReference(example) ? getExampleFromReference(example) : example,
};
});
}
if (mediaTypeObject.example) {
return [{ key: 'default', example: { value: mediaTypeObject.example } }];
}
if (mediaTypeObject.schema) {
if (mediaType === 'application/xml') {
// @TODO normally we should use the name of the schema but we don't have it
// fix it when we got the reference name
const root = mediaTypeObject.schema.xml?.name ?? 'object';
return [
{
key: 'default',
example: {
value: {
[root]: generateSchemaExample(mediaTypeObject.schema, {
xml: mediaType === 'application/xml',
mode: 'read',
}),
},
},
},
];
}
return [
{
key: 'default',
example: {
value: generateSchemaExample(mediaTypeObject.schema, {
mode: 'read',
}),
},
},
];
}
return [];
}
/**
* Get example from a schema object.
*/
export function getExampleFromSchema(args: {
schema: OpenAPIV3.SchemaObject;
}): OpenAPIV3.ExampleObject {
const { schema } = args;
if (schema.example) {
return { value: schema.example };
}
return { value: generateSchemaExample(schema, { mode: 'read' }) };
}
@@ -10,7 +10,8 @@ import { OpenAPICodeSample } from './OpenAPICodeSample';
import { OpenAPIPath } from './OpenAPIPath'; import { OpenAPIPath } from './OpenAPIPath';
import { OpenAPIResponseExample } from './OpenAPIResponseExample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample';
import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPISpec } from './OpenAPISpec';
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types'; import { getOpenAPIClientContext } from './context';
import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { resolveDescription } from './utils'; import { resolveDescription } from './utils';
/** /**
@@ -19,16 +20,12 @@ import { resolveDescription } from './utils';
export function OpenAPIOperation(props: { export function OpenAPIOperation(props: {
className?: string; className?: string;
data: OpenAPIOperationData; data: OpenAPIOperationData;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { className, data, context } = props; const { className, data, context } = props;
const { operation } = data; const { operation } = data;
const clientContext: OpenAPIClientContext = { const clientContext = getOpenAPIClientContext(context);
defaultInteractiveOpened: context.defaultInteractiveOpened,
icons: context.icons,
blockKey: context.blockKey,
};
return ( return (
<div className={clsx('openapi-operation', className)}> <div className={clsx('openapi-operation', className)}>
@@ -79,7 +76,7 @@ export function OpenAPIOperation(props: {
function OpenAPIOperationDescription(props: { function OpenAPIOperationDescription(props: {
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>; operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { operation } = props; const { operation } = props;
if (operation['x-gitbook-description-document']) { if (operation['x-gitbook-description-document']) {
+2 -2
View File
@@ -1,5 +1,5 @@
import { OpenAPICopyButton } from './OpenAPICopyButton'; import { OpenAPICopyButton } from './OpenAPICopyButton';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server'; import { getDefaultServerURL } from './util/server';
/** /**
@@ -7,7 +7,7 @@ import { getDefaultServerURL } from './util/server';
*/ */
export function OpenAPIPath(props: { export function OpenAPIPath(props: {
data: OpenAPIOperationData; data: OpenAPIOperationData;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { data } = props; const { data } = props;
const { method, path, operation } = data; const { method, path, operation } = data;
@@ -1,11 +1,14 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';
import {
OpenAPIEmptyExample,
OpenAPIExample,
getExampleFromReference,
getExamplesFromMediaTypeObject,
} from './OpenAPIExample';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { StaticSection } from './StaticSection'; import { StaticSection } from './StaticSection';
import { generateSchemaExample } from './generateSchemaExample'; import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import { checkIsReference, createStateKey, resolveDescription } from './utils'; import { checkIsReference, createStateKey, resolveDescription } from './utils';
/** /**
@@ -13,7 +16,7 @@ import { checkIsReference, createStateKey, resolveDescription } from './utils';
*/ */
export function OpenAPIResponseExample(props: { export function OpenAPIResponseExample(props: {
data: OpenAPIOperationData; data: OpenAPIOperationData;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { data, context } = props; const { data, context } = props;
@@ -62,7 +65,7 @@ export function OpenAPIResponseExample(props: {
return { return {
key: key, key: key,
label: key, label: key,
body: <OpenAPIEmptyResponseExample />, body: <OpenAPIEmptyExample />,
footer: description ? <Markdown source={description} /> : undefined, footer: description ? <Markdown source={description} /> : undefined,
}; };
} }
@@ -81,7 +84,7 @@ export function OpenAPIResponseExample(props: {
return ( return (
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}> <OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
<StaticSection header={<OpenAPITabsList />} className="openapi-response-example"> <StaticSection header={<OpenAPITabsList />} className="openapi-panel">
<OpenAPITabsPanels /> <OpenAPITabsPanels />
</StaticSection> </StaticSection>
</OpenAPITabs> </OpenAPITabs>
@@ -89,7 +92,7 @@ export function OpenAPIResponseExample(props: {
} }
function OpenAPIResponse(props: { function OpenAPIResponse(props: {
context: OpenAPIContextProps; context: OpenAPIContext;
content: { content: {
[media: string]: OpenAPIV3.MediaTypeObject; [media: string]: OpenAPIV3.MediaTypeObject;
}; };
@@ -141,7 +144,7 @@ function OpenAPIResponse(props: {
function OpenAPIResponseMediaType(props: { function OpenAPIResponseMediaType(props: {
mediaTypeObject: OpenAPIV3.MediaTypeObject; mediaTypeObject: OpenAPIV3.MediaTypeObject;
mediaType: string; mediaType: string;
context: OpenAPIContextProps; context: OpenAPIContext;
}) { }) {
const { mediaTypeObject, mediaType } = props; const { mediaTypeObject, mediaType } = props;
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType }); const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
@@ -149,7 +152,7 @@ function OpenAPIResponseMediaType(props: {
const firstExample = examples[0]; const firstExample = examples[0];
if (!firstExample) { if (!firstExample) {
return <OpenAPIEmptyResponseExample />; return <OpenAPIEmptyExample />;
} }
if (examples.length === 1) { if (examples.length === 1) {
@@ -184,42 +187,6 @@ function OpenAPIResponseMediaType(props: {
); );
} }
/**
* Display an example.
*/
function OpenAPIExample(props: {
example: OpenAPIV3.ExampleObject;
context: OpenAPIContextProps;
syntax: string;
}) {
const { example, context, syntax } = props;
const code = stringifyExample({ example, xml: syntax === 'xml' });
if (code === null) {
return <OpenAPIEmptyResponseExample />;
}
return context.renderCodeBlock({ code, syntax });
}
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
const { example, xml } = args;
if (!example.value) {
return null;
}
if (typeof example.value === 'string') {
return example.value;
}
if (xml) {
return json2xml(example.value);
}
return stringifyOpenAPI(example.value, null, 2);
}
/** /**
* Get the syntax from a media type. * Get the syntax from a media type.
*/ */
@@ -234,75 +201,3 @@ function getSyntaxFromMediaType(mediaType: string): string {
return 'text'; return 'text';
} }
/**
* Get examples from a media type object.
*/
function getExamplesFromMediaTypeObject(args: {
mediaType: string;
mediaTypeObject: OpenAPIV3.MediaTypeObject;
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
const { mediaTypeObject, mediaType } = args;
if (mediaTypeObject.examples) {
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
return {
key,
example: checkIsReference(example) ? getExampleFromReference(example) : example,
};
});
}
if (mediaTypeObject.example) {
return [{ key: 'default', example: { value: mediaTypeObject.example } }];
}
if (mediaTypeObject.schema) {
if (mediaType === 'application/xml') {
// @TODO normally we should use the name of the schema but we don't have it
// fix it when we got the reference name
const root = mediaTypeObject.schema.xml?.name ?? 'object';
return [
{
key: 'default',
example: {
value: {
[root]: generateSchemaExample(mediaTypeObject.schema, {
xml: mediaType === 'application/xml',
mode: 'read',
}),
},
},
},
];
}
return [
{
key: 'default',
example: {
value: generateSchemaExample(mediaTypeObject.schema, {
mode: 'read',
}),
},
},
];
}
return [];
}
/**
* Empty response example.
*/
function OpenAPIEmptyResponseExample() {
return (
<pre className="openapi-response-example-empty">
<p>No body</p>
</pre>
);
}
/**
* Generate an example from a reference object.
*/
function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
}
+2 -2
View File
@@ -138,9 +138,9 @@ export function OpenAPITabsPanels() {
return ( return (
<TabPanel id={key} className="openapi-tabs-panel"> <TabPanel id={key} className="openapi-tabs-panel">
<div className="openapi-tabs-body">{selectedTab.body}</div> <div className="openapi-panel-body">{selectedTab.body}</div>
{selectedTab.footer ? ( {selectedTab.footer ? (
<div className="openapi-tabs-footer">{selectedTab.footer}</div> <div className="openapi-panel-footer">{selectedTab.footer}</div>
) : null} ) : null}
</TabPanel> </TabPanel>
); );
+64
View File
@@ -0,0 +1,64 @@
export interface OpenAPIClientContext {
/**
* Icons used in the block.
*/
icons: {
chevronDown: React.ReactNode;
chevronRight: React.ReactNode;
plus: React.ReactNode;
};
/**
* Force all sections to be opened by default.
* @default false
*/
defaultInteractiveOpened?: boolean;
/**
* The key of the block
*/
blockKey?: string;
/**
* Optional id attached to the heading and used as an anchor.
*/
id?: string;
}
export interface OpenAPIContext extends OpenAPIClientContext {
/**
* Render a code block.
*/
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
/**
* Render the heading of the operation.
*/
renderHeading: (props: {
deprecated: boolean;
title: string;
stability?: string;
}) => React.ReactNode;
/**
* Render the document of the operation.
*/
renderDocument: (props: { document: object }) => React.ReactNode;
/**
* Specification URL.
*/
specUrl: string;
}
/**
* Get the client context from the OpenAPI context.
*/
export function getOpenAPIClientContext(context: OpenAPIContext): OpenAPIClientContext {
return {
icons: context.icons,
defaultInteractiveOpened: context.defaultInteractiveOpened,
blockKey: context.blockKey,
id: context.id,
};
}
+1 -1
View File
@@ -2,4 +2,4 @@ export * from './schemas';
export * from './OpenAPIOperation'; export * from './OpenAPIOperation';
export * from './OpenAPIOperationContext'; export * from './OpenAPIOperationContext';
export * from './resolveOpenAPIOperation'; export * from './resolveOpenAPIOperation';
export type { OpenAPISchemasData, OpenAPIOperationData } from './types'; export type { OpenAPIOperationData, OpenAPIContext } from './types';
@@ -1,99 +1,104 @@
import type { OpenAPISchema } from '@gitbook/openapi-parser';
import clsx from 'clsx'; import clsx from 'clsx';
import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup'; import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample';
import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
import { Section, SectionBody } from '../StaticSection'; import { Section, SectionBody, StaticSection } from '../StaticSection';
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPISchemasData } from '../types'; import { getOpenAPIClientContext } from '../context';
import type { OpenAPIContext } from '../types';
type OpenAPISchemasContextProps = Omit<
OpenAPIContextProps,
'renderCodeBlock' | 'renderHeading' | 'renderDocument'
>;
/** /**
* Display OpenAPI Schemas. * OpenAPI Schemas component.
*/ */
export function OpenAPISchemas(props: { export function OpenAPISchemas(props: {
className?: string; className?: string;
data: OpenAPISchemasData; schemas: OpenAPISchema[];
context: OpenAPISchemasContextProps; context: OpenAPIContext;
/** /**
* Whether to show the schema directly if there is only one. * Whether to show the schema directly if there is only one.
*/ */
grouped?: boolean; grouped?: boolean;
}) { }) {
const { className, data, context, grouped } = props; const { schemas, context, grouped, className } = props;
const { schemas } = data;
const clientContext: OpenAPIClientContext = { const firstSchema = schemas[0];
defaultInteractiveOpened: context.defaultInteractiveOpened,
icons: context.icons,
blockKey: context.blockKey,
};
if (!schemas.length) { if (!firstSchema) {
return null; return null;
} }
return ( const clientContext = getOpenAPIClientContext(context);
<div className={clsx('openapi-schemas', className)}>
<OpenAPIRootSchemasSchema grouped={grouped} schemas={schemas} context={clientContext} />
</div>
);
}
/**
* Root schema for OpenAPI schemas.
* It displays a single model or a disclosure group for multiple schemas.
*/
function OpenAPIRootSchemasSchema(props: {
schemas: OpenAPISchemasData['schemas'];
context: OpenAPIClientContext;
grouped?: boolean;
}) {
const { schemas, context, grouped } = props;
// If there is only one model and we are not grouping, we show it directly. // If there is only one model and we are not grouping, we show it directly.
if (schemas.length === 1 && !grouped) { if (schemas.length === 1 && !grouped) {
const schema = schemas?.[0]?.schema; const title = `The ${firstSchema.name} object`;
if (!schema) {
return null;
}
return ( return (
<Section> <div className={clsx('openapi-schemas', className)}>
<SectionBody> <div className="openapi-summary" id={context.id}>
<OpenAPIRootSchema schema={schema} context={context} /> {context.renderHeading({
</SectionBody> title,
</Section> })}
</div>
<div className="openapi-columns">
<div className="openapi-column-spec">
<StaticSection className="openapi-parameters" header="Attributes">
<OpenAPIRootSchema
schema={firstSchema.schema}
context={clientContext}
/>
</StaticSection>
</div>
<div className="openapi-column-preview">
<div className="openapi-column-preview-body">
<div className="openapi-panel">
<h4 className="openapi-panel-heading">{title}</h4>
<div className="openapi-panel-body">
<OpenAPIExample
example={getExampleFromSchema({
schema: firstSchema.schema,
})}
context={context}
syntax="json"
/>
</div>
</div>
</div>
</div>
</div>
</div>
); );
} }
// If there are multiple schemas, we use a disclosure group to show them all. // If there are multiple schemas, we use a disclosure group to show them all.
return ( return (
<OpenAPIDisclosureGroup <div className={clsx('openapi-schemas', className)}>
allowsMultipleExpanded <OpenAPIDisclosureGroup
icon={context.icons.chevronRight} allowsMultipleExpanded
groups={schemas.map(({ name, schema }) => ({ icon={context.icons.chevronRight}
id: name, groups={schemas.map(({ name, schema }) => ({
label: ( id: name,
<div className="openapi-response-tab-content" key={`model-${name}`}> label: (
<span className="openapi-response-statuscode">{name}</span> <div className="openapi-response-tab-content" key={`model-${name}`}>
</div> <span className="openapi-response-statuscode">{name}</span>
), </div>
tabs: [ ),
{ tabs: [
id: 'model', {
body: ( id: 'model',
<Section className="openapi-section-schemas"> body: (
<SectionBody> <Section className="openapi-section-schemas">
<OpenAPIRootSchema schema={schema} context={context} /> <SectionBody>
</SectionBody> <OpenAPIRootSchema
</Section> schema={schema}
), context={clientContext}
}, />
], </SectionBody>
}))} </Section>
/> ),
},
],
}))}
/>
</div>
); );
} }
@@ -1,9 +1,6 @@
import type { Filesystem, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; import type { Filesystem, OpenAPISchema, OpenAPIV3xDocument } from '@gitbook/openapi-parser';
import { filterSelectedOpenAPISchemas } from '@gitbook/openapi-parser'; import { filterSelectedOpenAPISchemas } from '@gitbook/openapi-parser';
import { dereferenceFilesystem } from '../dereference'; import { dereferenceFilesystem } from '../dereference';
import type { OpenAPISchemasData } from '../types';
//!!TODO: We should return only the schemas that are used in the block. Still a WIP awaiting future work.
/** /**
* Resolve an OpenAPI schemas from a file and compile it to a more usable format. * Resolve an OpenAPI schemas from a file and compile it to a more usable format.
@@ -14,7 +11,9 @@ export async function resolveOpenAPISchemas(
options: { options: {
schemas: string[]; schemas: string[];
} }
): Promise<OpenAPISchemasData | null> { ): Promise<{
schemas: OpenAPISchema[];
} | null> {
const { schemas: selectedSchemas } = options; const { schemas: selectedSchemas } = options;
const schema = await dereferenceFilesystem(filesystem); const schema = await dereferenceFilesystem(filesystem);
+34 -29
View File
@@ -1,33 +1,13 @@
import type { import type {
OpenAPICustomOperationProperties, OpenAPICustomOperationProperties,
OpenAPICustomSpecProperties, OpenAPICustomSpecProperties,
OpenAPISchema,
OpenAPIV3, OpenAPIV3,
} from '@gitbook/openapi-parser'; } from '@gitbook/openapi-parser';
export interface OpenAPIContextProps extends OpenAPIClientContext {
/**
* Render a code block.
*/
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
/**
* Render the heading of the operation.
*/
renderHeading: (props: {
deprecated: boolean;
title: string;
stability?: string;
}) => React.ReactNode;
/**
* Render the document of the operation.
*/
renderDocument: (props: { document: object }) => React.ReactNode;
/** Spec url for the Scalar Api Client */
specUrl: string;
}
export interface OpenAPIClientContext { export interface OpenAPIClientContext {
/**
* Icons used in the block.
*/
icons: { icons: {
chevronDown: React.ReactNode; chevronDown: React.ReactNode;
chevronRight: React.ReactNode; chevronRight: React.ReactNode;
@@ -39,14 +19,44 @@ export interface OpenAPIClientContext {
* @default false * @default false
*/ */
defaultInteractiveOpened?: boolean; defaultInteractiveOpened?: boolean;
/** /**
* The key of the block * The key of the block
*/ */
blockKey?: string; blockKey?: string;
/** Optional id attached to the OpenAPI Operation heading and used as an anchor */
/**
* Optional id attached to the heading and used as an anchor.
*/
id?: string; id?: string;
} }
export interface OpenAPIContext extends OpenAPIClientContext {
/**
* Render a code block.
*/
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
/**
* Render the heading of the operation.
*/
renderHeading: (props: {
deprecated?: boolean;
title: string;
stability?: string;
}) => React.ReactNode;
/**
* Render the document of the operation.
*/
renderDocument: (props: { document: object }) => React.ReactNode;
/**
* Specification URL.
*/
specUrl: string;
}
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties { export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
path: string; path: string;
method: string; method: string;
@@ -60,8 +70,3 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
/** Securities that should be used for this operation */ /** Securities that should be used for this operation */
securities: [string, OpenAPIV3.SecuritySchemeObject][]; securities: [string, OpenAPIV3.SecuritySchemeObject][];
} }
export interface OpenAPISchemasData {
/** Components schemas to be used for schemas */
schemas: OpenAPISchema[];
}