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 }) {
// 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
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
useStripFallbackQueryParam();
return null;
@@ -20,7 +20,15 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
return (
<Tag
id={id}
className={tcls(textStyle.textSize, 'heading', 'group', 'relative', 'grid', style)}
className={tcls(
textStyle.textSize,
'heading',
'group',
'relative',
'grid',
'scroll-m-12',
style
)}
>
<div
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 { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
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 } from '@/lib/openapi/types';
import { getOpenAPIContext } from './context';
/**
* Render an openapi block or an openapi-operation block.
@@ -55,56 +51,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return (
<BaseOpenAPIOperation
data={data}
context={{
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,
}}
context={getOpenAPIContext({ props, specUrl })}
className="openapi-block"
/>
);
@@ -1,6 +1,5 @@
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { OpenAPISchemas as BaseOpenAPISchemas } from '@gitbook/react-openapi';
import type { BlockProps } from '../Block';
@@ -8,6 +7,7 @@ import type { BlockProps } from '../Block';
import './scalar.css';
import './style.css';
import type { OpenAPISchemasBlock } from '@/lib/openapi/types';
import { getOpenAPIContext } from './context';
/**
* Render an openapi-schemas block.
@@ -49,19 +49,9 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return (
<BaseOpenAPISchemas
data={data}
schemas={data.schemas}
grouped={block.data.grouped}
context={{
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,
}}
context={getOpenAPIContext({ props, specUrl })}
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 */
.openapi-operation {
.openapi-operation,
.openapi-schemas {
@apply flex-1 flex flex-col gap-8 mb-14 min-w-0;
}
@@ -17,7 +18,7 @@
}
.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 {
@@ -483,12 +484,30 @@
@apply flex flex-row items-center py-2 px-3 justify-end border-t border-tint-subtle;
}
/* Response Example */
.openapi-response-example {
/* Panel */
.openapi-panel {
@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;
}
@@ -559,15 +578,6 @@
.openapi-tabs-panel {
@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 */
@@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks';
export function PageClientLayout(props: { withSections?: boolean }) {
// 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
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
useStripFallbackQueryParam();
return null;
@@ -3,6 +3,7 @@ import type { GitBookAnyContext } from '@v2/lib/context';
import { getNodeText } from './document';
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
import { resolveOpenAPISchemasBlock } from './openapi/resolveOpenAPISchemasBlock';
export interface DocumentSection {
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;
@@ -1,5 +1,5 @@
import { OpenAPIParseError } from '@gitbook/openapi-parser';
import { type OpenAPISchemasData, resolveOpenAPISchemas } from '@gitbook/react-openapi';
import { OpenAPIParseError, type OpenAPISchema } from '@gitbook/openapi-parser';
import { resolveOpenAPISchemas } from '@gitbook/react-openapi';
import { fetchOpenAPIFilesystem } from './fetch';
import type {
OpenAPISchemasBlock,
@@ -7,7 +7,9 @@ import type {
ResolveOpenAPIBlockResult,
} from './types';
type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<OpenAPISchemasData>;
type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<{
schemas: OpenAPISchema[];
}>;
const weakmap = new WeakMap<OpenAPISchemasBlock, Promise<ResolveOpenAPISchemasBlockResult>>();
@@ -9,7 +9,7 @@ import { StaticSection } from './StaticSection';
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server';
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: {
data: OpenAPIOperationData;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { data } = props;
@@ -58,7 +58,7 @@ export function OpenAPICodeSample(props: {
*/
function generateCodeSamples(props: {
data: OpenAPIOperationData;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { data, context } = props;
@@ -189,7 +189,7 @@ export interface MediaTypeRenderer {
function OpenAPICodeSampleFooter(props: {
data: OpenAPIOperationData;
renderers: MediaTypeRenderer[];
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { data, context, renderers } = props;
const { method, path } = data;
@@ -227,7 +227,7 @@ function OpenAPICodeSampleFooter(props: {
*/
function getCustomCodeSamples(props: {
data: OpenAPIOperationData;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
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 { OpenAPIResponseExample } from './OpenAPIResponseExample';
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';
/**
@@ -19,16 +20,12 @@ import { resolveDescription } from './utils';
export function OpenAPIOperation(props: {
className?: string;
data: OpenAPIOperationData;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { className, data, context } = props;
const { operation } = data;
const clientContext: OpenAPIClientContext = {
defaultInteractiveOpened: context.defaultInteractiveOpened,
icons: context.icons,
blockKey: context.blockKey,
};
const clientContext = getOpenAPIClientContext(context);
return (
<div className={clsx('openapi-operation', className)}>
@@ -79,7 +76,7 @@ export function OpenAPIOperation(props: {
function OpenAPIOperationDescription(props: {
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { operation } = props;
if (operation['x-gitbook-description-document']) {
+2 -2
View File
@@ -1,5 +1,5 @@
import { OpenAPICopyButton } from './OpenAPICopyButton';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server';
/**
@@ -7,7 +7,7 @@ import { getDefaultServerURL } from './util/server';
*/
export function OpenAPIPath(props: {
data: OpenAPIOperationData;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { data } = props;
const { method, path, operation } = data;
@@ -1,11 +1,14 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { Markdown } from './Markdown';
import {
OpenAPIEmptyExample,
OpenAPIExample,
getExampleFromReference,
getExamplesFromMediaTypeObject,
} from './OpenAPIExample';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { StaticSection } from './StaticSection';
import { generateSchemaExample } from './generateSchemaExample';
import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { checkIsReference, createStateKey, resolveDescription } from './utils';
/**
@@ -13,7 +16,7 @@ import { checkIsReference, createStateKey, resolveDescription } from './utils';
*/
export function OpenAPIResponseExample(props: {
data: OpenAPIOperationData;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { data, context } = props;
@@ -62,7 +65,7 @@ export function OpenAPIResponseExample(props: {
return {
key: key,
label: key,
body: <OpenAPIEmptyResponseExample />,
body: <OpenAPIEmptyExample />,
footer: description ? <Markdown source={description} /> : undefined,
};
}
@@ -81,7 +84,7 @@ export function OpenAPIResponseExample(props: {
return (
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
<StaticSection header={<OpenAPITabsList />} className="openapi-response-example">
<StaticSection header={<OpenAPITabsList />} className="openapi-panel">
<OpenAPITabsPanels />
</StaticSection>
</OpenAPITabs>
@@ -89,7 +92,7 @@ export function OpenAPIResponseExample(props: {
}
function OpenAPIResponse(props: {
context: OpenAPIContextProps;
context: OpenAPIContext;
content: {
[media: string]: OpenAPIV3.MediaTypeObject;
};
@@ -141,7 +144,7 @@ function OpenAPIResponse(props: {
function OpenAPIResponseMediaType(props: {
mediaTypeObject: OpenAPIV3.MediaTypeObject;
mediaType: string;
context: OpenAPIContextProps;
context: OpenAPIContext;
}) {
const { mediaTypeObject, mediaType } = props;
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
@@ -149,7 +152,7 @@ function OpenAPIResponseMediaType(props: {
const firstExample = examples[0];
if (!firstExample) {
return <OpenAPIEmptyResponseExample />;
return <OpenAPIEmptyExample />;
}
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.
*/
@@ -234,75 +201,3 @@ function getSyntaxFromMediaType(mediaType: string): string {
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 (
<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 ? (
<div className="openapi-tabs-footer">{selectedTab.footer}</div>
<div className="openapi-panel-footer">{selectedTab.footer}</div>
) : null}
</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 './OpenAPIOperationContext';
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 { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample';
import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
import { Section, SectionBody } from '../StaticSection';
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPISchemasData } from '../types';
type OpenAPISchemasContextProps = Omit<
OpenAPIContextProps,
'renderCodeBlock' | 'renderHeading' | 'renderDocument'
>;
import { Section, SectionBody, StaticSection } from '../StaticSection';
import { getOpenAPIClientContext } from '../context';
import type { OpenAPIContext } from '../types';
/**
* Display OpenAPI Schemas.
* OpenAPI Schemas component.
*/
export function OpenAPISchemas(props: {
className?: string;
data: OpenAPISchemasData;
context: OpenAPISchemasContextProps;
schemas: OpenAPISchema[];
context: OpenAPIContext;
/**
* Whether to show the schema directly if there is only one.
*/
grouped?: boolean;
}) {
const { className, data, context, grouped } = props;
const { schemas } = data;
const { schemas, context, grouped, className } = props;
const clientContext: OpenAPIClientContext = {
defaultInteractiveOpened: context.defaultInteractiveOpened,
icons: context.icons,
blockKey: context.blockKey,
};
const firstSchema = schemas[0];
if (!schemas.length) {
if (!firstSchema) {
return null;
}
return (
<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;
const clientContext = getOpenAPIClientContext(context);
// If there is only one model and we are not grouping, we show it directly.
if (schemas.length === 1 && !grouped) {
const schema = schemas?.[0]?.schema;
if (!schema) {
return null;
}
const title = `The ${firstSchema.name} object`;
return (
<Section>
<SectionBody>
<OpenAPIRootSchema schema={schema} context={context} />
</SectionBody>
</Section>
<div className={clsx('openapi-schemas', className)}>
<div className="openapi-summary" id={context.id}>
{context.renderHeading({
title,
})}
</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.
return (
<OpenAPIDisclosureGroup
allowsMultipleExpanded
icon={context.icons.chevronRight}
groups={schemas.map(({ name, schema }) => ({
id: name,
label: (
<div className="openapi-response-tab-content" key={`model-${name}`}>
<span className="openapi-response-statuscode">{name}</span>
</div>
),
tabs: [
{
id: 'model',
body: (
<Section className="openapi-section-schemas">
<SectionBody>
<OpenAPIRootSchema schema={schema} context={context} />
</SectionBody>
</Section>
),
},
],
}))}
/>
<div className={clsx('openapi-schemas', className)}>
<OpenAPIDisclosureGroup
allowsMultipleExpanded
icon={context.icons.chevronRight}
groups={schemas.map(({ name, schema }) => ({
id: name,
label: (
<div className="openapi-response-tab-content" key={`model-${name}`}>
<span className="openapi-response-statuscode">{name}</span>
</div>
),
tabs: [
{
id: 'model',
body: (
<Section className="openapi-section-schemas">
<SectionBody>
<OpenAPIRootSchema
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 { 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.
@@ -14,7 +11,9 @@ export async function resolveOpenAPISchemas(
options: {
schemas: string[];
}
): Promise<OpenAPISchemasData | null> {
): Promise<{
schemas: OpenAPISchema[];
} | null> {
const { schemas: selectedSchemas } = options;
const schema = await dereferenceFilesystem(filesystem);
+34 -29
View File
@@ -1,33 +1,13 @@
import type {
OpenAPICustomOperationProperties,
OpenAPICustomSpecProperties,
OpenAPISchema,
OpenAPIV3,
} 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 {
/**
* Icons used in the block.
*/
icons: {
chevronDown: React.ReactNode;
chevronRight: React.ReactNode;
@@ -39,14 +19,44 @@ export interface OpenAPIClientContext {
* @default false
*/
defaultInteractiveOpened?: boolean;
/**
* The key of the block
*/
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;
}
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 {
path: string;
method: string;
@@ -60,8 +70,3 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
/** Securities that should be used for this operation */
securities: [string, OpenAPIV3.SecuritySchemeObject][];
}
export interface OpenAPISchemasData {
/** Components schemas to be used for schemas */
schemas: OpenAPISchema[];
}