mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Implement OpenAPI models blocks (#2908)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@gitbook/openapi-parser': minor
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Implement OpenAPI models blocks
|
||||
Vendored
+2
-1
@@ -11,6 +11,7 @@
|
||||
"prettier.enable": false,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit"
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { JSONDocument } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { OpenAPIOperation } from '@gitbook/react-openapi';
|
||||
|
||||
import { type AnyOpenAPIOperationBlock, resolveOpenAPIBlock } from '@/lib/openapi/fetch';
|
||||
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
@@ -12,11 +12,12 @@ import { Heading } from '../Heading';
|
||||
|
||||
import './scalar.css';
|
||||
import './style.css';
|
||||
import type { AnyOpenAPIBlock } from '@/lib/openapi/types';
|
||||
|
||||
/**
|
||||
* Render an openapi block or an openapi-operation block.
|
||||
*/
|
||||
export async function OpenAPI(props: BlockProps<AnyOpenAPIOperationBlock>) {
|
||||
export async function OpenAPI(props: BlockProps<AnyOpenAPIBlock>) {
|
||||
const { style } = props;
|
||||
return (
|
||||
<div className={tcls('flex w-full', style, 'max-w-full')}>
|
||||
@@ -25,14 +26,14 @@ export async function OpenAPI(props: BlockProps<AnyOpenAPIOperationBlock>) {
|
||||
);
|
||||
}
|
||||
|
||||
async function OpenAPIBody(props: BlockProps<AnyOpenAPIOperationBlock>) {
|
||||
async function OpenAPIBody(props: BlockProps<AnyOpenAPIBlock>) {
|
||||
const { block, context } = props;
|
||||
|
||||
if (!context.contentContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data, specUrl, error } = await resolveOpenAPIBlock({
|
||||
const { data, specUrl, error } = await resolveOpenAPIOperationBlock({
|
||||
block,
|
||||
context: context.contentContext,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
@apply flex-1 flex flex-col gap-4 mb-14;
|
||||
}
|
||||
|
||||
.openapi-models {
|
||||
@apply flex flex-col mb-14 flex-1;
|
||||
}
|
||||
|
||||
.openapi-columns {
|
||||
@apply grid grid-cols-1 lg:grid-cols-2 gap-6 print-mode:grid-cols-1 justify-stretch;
|
||||
}
|
||||
@@ -615,3 +619,11 @@
|
||||
.openapi-section-body.openapi-schema.openapi-schema-root {
|
||||
@apply space-y-2.5;
|
||||
}
|
||||
|
||||
.openapi-section-models {
|
||||
@apply border border-tint-subtle rounded-lg;
|
||||
}
|
||||
|
||||
.openapi-section-models > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
|
||||
@apply p-2.5;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { JSONDocument } from '@gitbook/api';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
|
||||
import { getNodeText } from './document';
|
||||
import { resolveOpenAPIBlock } from './openapi/fetch';
|
||||
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
|
||||
|
||||
export interface DocumentSection {
|
||||
id: string;
|
||||
@@ -38,7 +38,7 @@ export async function getDocumentSections(
|
||||
}
|
||||
|
||||
if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
|
||||
const { data: operation } = await resolveOpenAPIBlock({
|
||||
const { data: operation } = await resolveOpenAPIOperationBlock({
|
||||
block,
|
||||
context,
|
||||
});
|
||||
|
||||
@@ -1,82 +1,40 @@
|
||||
import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation } from '@gitbook/api';
|
||||
import { OpenAPIParseError, parseOpenAPI } from '@gitbook/openapi-parser';
|
||||
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
import { parseOpenAPI } from '@gitbook/openapi-parser';
|
||||
|
||||
import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache';
|
||||
|
||||
import type { ResolveOpenAPIBlockArgs } from '@/lib/openapi/types';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { resolveContentRef } from '../references';
|
||||
import { isV2 } from '../v2';
|
||||
import { enrichFilesystem } from './enrich';
|
||||
|
||||
export type AnyOpenAPIOperationBlock = DocumentBlockOpenAPI | DocumentBlockOpenAPIOperation;
|
||||
|
||||
const weakmap = new WeakMap<AnyOpenAPIOperationBlock, Promise<ResolveOpenAPIBlockResult>>();
|
||||
import type { FetchOpenAPIFilesystemResult } from './types';
|
||||
|
||||
/**
|
||||
* Cache the result of resolving an OpenAPI block.
|
||||
* It is important because the resolve is called in sections and in the block itself.
|
||||
* Fetch OpenAPI block.
|
||||
*/
|
||||
export function resolveOpenAPIBlock(
|
||||
export async function fetchOpenAPIFilesystem(
|
||||
args: ResolveOpenAPIBlockArgs
|
||||
): Promise<ResolveOpenAPIBlockResult> {
|
||||
if (weakmap.has(args.block)) {
|
||||
return weakmap.get(args.block)!;
|
||||
}
|
||||
|
||||
const result = baseResolveOpenAPIBlock(args);
|
||||
weakmap.set(args.block, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
type ResolveOpenAPIBlockArgs = {
|
||||
block: AnyOpenAPIOperationBlock;
|
||||
context: GitBookAnyContext;
|
||||
};
|
||||
export type ResolveOpenAPIBlockResult =
|
||||
| { error?: undefined; data: OpenAPIOperationData | null; specUrl: string | null }
|
||||
| { error: OpenAPIParseError; data?: undefined; specUrl?: undefined };
|
||||
/**
|
||||
* Resolve OpenAPI block.
|
||||
*/
|
||||
async function baseResolveOpenAPIBlock(
|
||||
args: ResolveOpenAPIBlockArgs
|
||||
): Promise<ResolveOpenAPIBlockResult> {
|
||||
): Promise<FetchOpenAPIFilesystemResult> {
|
||||
const { context, block } = args;
|
||||
if (!block.data.path || !block.data.method) {
|
||||
return { data: null, specUrl: null };
|
||||
}
|
||||
|
||||
const ref = block.data.ref;
|
||||
const resolved = ref ? await resolveContentRef(ref, context) : null;
|
||||
|
||||
if (!resolved) {
|
||||
return { data: null, specUrl: null };
|
||||
return { filesystem: null, specUrl: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const filesystem = await (() => {
|
||||
if (ref.kind === 'openapi') {
|
||||
assert(resolved.openAPIFilesystem);
|
||||
return resolved.openAPIFilesystem;
|
||||
}
|
||||
return fetchFilesystem(resolved.href);
|
||||
})();
|
||||
|
||||
const data = await resolveOpenAPIOperation(filesystem, {
|
||||
path: block.data.path,
|
||||
method: block.data.method,
|
||||
});
|
||||
|
||||
return { data, specUrl: resolved.href };
|
||||
} catch (error) {
|
||||
if (error instanceof OpenAPIParseError) {
|
||||
return { error };
|
||||
const filesystem = await (() => {
|
||||
if (ref.kind === 'openapi') {
|
||||
assert(resolved.openAPIFilesystem);
|
||||
return resolved.openAPIFilesystem;
|
||||
}
|
||||
return fetchFilesystem(resolved.href);
|
||||
})();
|
||||
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
filesystem,
|
||||
specUrl: resolved.href,
|
||||
};
|
||||
}
|
||||
|
||||
function fetchFilesystem(url: string) {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { fetchOpenAPIFilesystem } from '@/lib/openapi/fetch';
|
||||
import type { ResolveOpenAPIBlockResult } from '@/lib/openapi/types';
|
||||
import { OpenAPIParseError } from '@gitbook/openapi-parser';
|
||||
import { type OpenAPIModelsData, resolveOpenAPIModels } from '@gitbook/react-openapi';
|
||||
import type { AnyOpenAPIBlock, ResolveOpenAPIBlockArgs } from './types';
|
||||
|
||||
type ResolveOpenAPIModelsBlockResult = ResolveOpenAPIBlockResult<OpenAPIModelsData>;
|
||||
|
||||
const weakmap = new WeakMap<AnyOpenAPIBlock, Promise<ResolveOpenAPIModelsBlockResult>>();
|
||||
|
||||
/**
|
||||
* Cache the result of resolving an OpenAPI block.
|
||||
* It is important because the resolve is called in sections and in the block itself.
|
||||
*/
|
||||
export function resolveOpenAPIModelsBlock(
|
||||
args: ResolveOpenAPIBlockArgs
|
||||
): Promise<ResolveOpenAPIModelsBlockResult> {
|
||||
if (weakmap.has(args.block)) {
|
||||
return weakmap.get(args.block)!;
|
||||
}
|
||||
|
||||
const result = baseResolveOpenAPIModelsBlock(args);
|
||||
weakmap.set(args.block, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve OpenAPI models block.
|
||||
*/
|
||||
async function baseResolveOpenAPIModelsBlock(
|
||||
args: ResolveOpenAPIBlockArgs
|
||||
): Promise<ResolveOpenAPIModelsBlockResult> {
|
||||
const { context, block } = args;
|
||||
if (!block.data.path || !block.data.method) {
|
||||
return { data: null, specUrl: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
|
||||
|
||||
if (!filesystem || !specUrl) {
|
||||
return { data: null, specUrl: null };
|
||||
}
|
||||
|
||||
const data = await resolveOpenAPIModels(filesystem);
|
||||
|
||||
return { data, specUrl };
|
||||
} catch (error) {
|
||||
if (error instanceof OpenAPIParseError) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { fetchOpenAPIFilesystem } from '@/lib/openapi/fetch';
|
||||
import { OpenAPIParseError } from '@gitbook/openapi-parser';
|
||||
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
|
||||
import type { AnyOpenAPIBlock, ResolveOpenAPIBlockArgs, ResolveOpenAPIBlockResult } from './types';
|
||||
|
||||
type ResolveOpenAPIOperationBlockResult = ResolveOpenAPIBlockResult<OpenAPIOperationData>;
|
||||
|
||||
const weakmap = new WeakMap<AnyOpenAPIBlock, Promise<ResolveOpenAPIOperationBlockResult>>();
|
||||
|
||||
/**
|
||||
* Cache the result of resolving an OpenAPI block.
|
||||
* It is important because the resolve is called in sections and in the block itself.
|
||||
*/
|
||||
export function resolveOpenAPIOperationBlock(
|
||||
args: ResolveOpenAPIBlockArgs
|
||||
): Promise<ResolveOpenAPIOperationBlockResult> {
|
||||
if (weakmap.has(args.block)) {
|
||||
return weakmap.get(args.block)!;
|
||||
}
|
||||
|
||||
const result = baseResolveOpenAPIOperationBlock(args);
|
||||
weakmap.set(args.block, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve OpenAPI operation block.
|
||||
*/
|
||||
async function baseResolveOpenAPIOperationBlock(
|
||||
args: ResolveOpenAPIBlockArgs
|
||||
): Promise<ResolveOpenAPIOperationBlockResult> {
|
||||
const { context, block } = args;
|
||||
if (!block.data.path || !block.data.method) {
|
||||
return { data: null, specUrl: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
|
||||
|
||||
if (!filesystem) {
|
||||
return { data: null, specUrl: null };
|
||||
}
|
||||
|
||||
const data = await resolveOpenAPIOperation(filesystem, {
|
||||
path: block.data.path,
|
||||
method: block.data.method,
|
||||
});
|
||||
|
||||
return { data, specUrl };
|
||||
} catch (error) {
|
||||
if (error instanceof OpenAPIParseError) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation } from '@gitbook/api';
|
||||
import type { Filesystem, OpenAPIParseError, OpenAPIV3xDocument } from '@gitbook/openapi-parser';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
|
||||
//!!TODO: Add DocumentBlockOpenAPIModels when available in @gitbook/api
|
||||
export type AnyOpenAPIBlock = DocumentBlockOpenAPI | DocumentBlockOpenAPIOperation;
|
||||
|
||||
/**
|
||||
* Arguments for resolving OpenAPI block.
|
||||
*/
|
||||
export type ResolveOpenAPIBlockArgs = {
|
||||
block: AnyOpenAPIBlock;
|
||||
context: GitBookAnyContext;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch OpenAPI filesystem result.
|
||||
*/
|
||||
export type FetchOpenAPIFilesystemResult =
|
||||
| {
|
||||
error?: undefined;
|
||||
filesystem: Filesystem<OpenAPIV3xDocument> | null;
|
||||
specUrl: string | null;
|
||||
}
|
||||
| FetchOpenAPIFilesystemError;
|
||||
|
||||
/**
|
||||
* Fetch OpenAPI filesystem error.
|
||||
*/
|
||||
type FetchOpenAPIFilesystemError = {
|
||||
error: OpenAPIParseError;
|
||||
filesystem?: undefined;
|
||||
specUrl?: undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolved OpenAPI block result.
|
||||
*/
|
||||
export type ResolveOpenAPIBlockResult<T> =
|
||||
| { error?: undefined; data: T | null; specUrl: string | null }
|
||||
| ResolveOpenAPIBlockError;
|
||||
|
||||
/**
|
||||
* Resolved OpenAPI block error.
|
||||
*/
|
||||
type ResolveOpenAPIBlockError = {
|
||||
error: OpenAPIParseError;
|
||||
data?: undefined;
|
||||
specUrl?: undefined;
|
||||
};
|
||||
@@ -29,12 +29,20 @@ export async function traverse<T extends AnyObject | AnyObject[]>(
|
||||
}
|
||||
|
||||
const keys = Object.keys(specification);
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
const results = await Promise.all(
|
||||
keys.map(async (key, index) => {
|
||||
const value = specification[key];
|
||||
result[key] = await traverse(value, transform, [...path, key], seen);
|
||||
const processed = await traverse(value, transform, [...path, key], seen);
|
||||
return { key, value: processed, index };
|
||||
})
|
||||
);
|
||||
|
||||
// Promise.all does not guarantee the order of the results
|
||||
// So we need to sort them to preserve the original order
|
||||
results.sort((a, b) => a.index - b.index);
|
||||
for (const { key, value } of results) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return transform(result, path) as Promise<T>;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type TDisclosureGroup = {
|
||||
label: string | React.ReactNode;
|
||||
tabs?: {
|
||||
id: string;
|
||||
label: string | React.ReactNode;
|
||||
label?: string | React.ReactNode;
|
||||
body?: React.ReactNode;
|
||||
}[];
|
||||
};
|
||||
@@ -121,7 +121,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : group.tabs[0] ? (
|
||||
) : group.tabs[0]?.label ? (
|
||||
<span>{group.tabs[0].label}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,8 @@ import clsx from 'clsx';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { checkIsReference, resolveDescription } from './utils';
|
||||
import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
|
||||
|
||||
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
|
||||
|
||||
@@ -181,7 +180,7 @@ function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaO
|
||||
/**
|
||||
* Render the enum value for a schema.
|
||||
*/
|
||||
export function OpenAPISchemaEnum(props: { enumValues: any[] }) {
|
||||
function OpenAPISchemaEnum(props: { enumValues: any[] }) {
|
||||
const { enumValues } = props;
|
||||
|
||||
return (
|
||||
@@ -199,24 +198,16 @@ export function OpenAPISchemaEnum(props: { enumValues: any[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry }) {
|
||||
/**
|
||||
* Render the top row of a schema. e.g: name, type, and required status.
|
||||
*/
|
||||
function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry }) {
|
||||
const {
|
||||
property: { schema, propertyName, required },
|
||||
} = props;
|
||||
|
||||
const shouldDisplayExample = (schema: OpenAPIV3.SchemaObject): boolean => {
|
||||
return (
|
||||
(typeof schema.example === 'string' && !!schema.example) ||
|
||||
typeof schema.example === 'number' ||
|
||||
typeof schema.example === 'boolean' ||
|
||||
(Array.isArray(schema.example) && schema.example.length > 0) ||
|
||||
(typeof schema.example === 'object' &&
|
||||
schema.example !== null &&
|
||||
Object.keys(schema.example).length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
const description = resolveDescription(schema);
|
||||
const example = resolveFirstExample(schema);
|
||||
|
||||
return (
|
||||
<div className="openapi-schema-presentation">
|
||||
@@ -237,9 +228,9 @@ export function OpenAPISchemaPresentation(props: { property: OpenAPISchemaProper
|
||||
{description ? (
|
||||
<Markdown source={description} className="openapi-schema-description" />
|
||||
) : null}
|
||||
{shouldDisplayExample(schema) ? (
|
||||
{example ? (
|
||||
<div className="openapi-schema-example">
|
||||
Example: <code>{formatExample(schema.example)}</code>
|
||||
Example: <code>{example}</code>
|
||||
</div>
|
||||
) : null}
|
||||
{schema.pattern ? (
|
||||
@@ -370,7 +361,7 @@ function flattenAlternatives(
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
|
||||
function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
|
||||
// Otherwise try to infer a nice title
|
||||
let type = 'any';
|
||||
|
||||
@@ -418,16 +409,3 @@ function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string {
|
||||
|
||||
return schema.title || 'child attributes';
|
||||
}
|
||||
|
||||
function formatExample(example: any): string {
|
||||
if (typeof example === 'string') {
|
||||
return example
|
||||
.replace(/\n/g, ' ') // Replace newlines with spaces
|
||||
.replace(/\s+/g, ' ') // Collapse multiple spaces/newlines into a single space
|
||||
.replace(/([\{\}:,])\s+/g, '$1 ') // Ensure a space after {, }, :, and ,
|
||||
.replace(/\s+([\{\}:,])/g, ' $1') // Ensure a space before {, }, :, and ,
|
||||
.trim();
|
||||
}
|
||||
|
||||
return stringifyOpenAPI(example);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { type Filesystem, type OpenAPIV3xDocument, dereference } from '@gitbook/openapi-parser';
|
||||
|
||||
const dereferenceCache = new WeakMap<Filesystem, Promise<OpenAPIV3xDocument>>();
|
||||
|
||||
/**
|
||||
* Memoized version of `dereferenceSchema`.
|
||||
*/
|
||||
export function dereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
||||
if (dereferenceCache.has(filesystem)) {
|
||||
return dereferenceCache.get(filesystem) as Promise<OpenAPIV3xDocument>;
|
||||
}
|
||||
|
||||
const promise = baseDereferenceFilesystem(filesystem);
|
||||
dereferenceCache.set(filesystem, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dereference an OpenAPI schema.
|
||||
*/
|
||||
async function baseDereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
||||
const result = await dereference(filesystem);
|
||||
|
||||
if (!result.schema) {
|
||||
throw new Error('Failed to dereference OpenAPI document');
|
||||
}
|
||||
|
||||
return result.schema as OpenAPIV3xDocument;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './resolveOpenAPIOperation';
|
||||
export * from './models';
|
||||
export * from './OpenAPIOperation';
|
||||
export * from './OpenAPIOperationContext';
|
||||
export type { OpenAPIOperationData } from './types';
|
||||
export * from './resolveOpenAPIOperation';
|
||||
export type { OpenAPIModelsData, OpenAPIOperationData } from './types';
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import clsx from 'clsx';
|
||||
import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
|
||||
import { OpenAPIRootSchema } from '../OpenAPISchema';
|
||||
import { Section, SectionBody } from '../StaticSection';
|
||||
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIModelsData } from '../types';
|
||||
|
||||
/**
|
||||
* Display OpenAPI Models.
|
||||
*/
|
||||
export function OpenAPIModels(props: {
|
||||
className?: string;
|
||||
data: OpenAPIModelsData;
|
||||
context: OpenAPIContextProps;
|
||||
}) {
|
||||
const { className, data, context } = props;
|
||||
const { models } = data;
|
||||
|
||||
const clientContext: OpenAPIClientContext = {
|
||||
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
||||
icons: context.icons,
|
||||
blockKey: context.blockKey,
|
||||
};
|
||||
|
||||
if (!models.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('openapi-models', className)}>
|
||||
<OpenAPIRootModelsSchema models={models} context={clientContext} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root schema for OpenAPI models.
|
||||
* It displays a single model or a disclosure group for multiple models.
|
||||
*/
|
||||
function OpenAPIRootModelsSchema(props: {
|
||||
models: OpenAPIModelsData['models'];
|
||||
context: OpenAPIClientContext;
|
||||
}) {
|
||||
const { models, context } = props;
|
||||
|
||||
// If there is only one model, we show it directly.
|
||||
if (models.length === 1) {
|
||||
const schema = models?.[0]?.schema;
|
||||
|
||||
if (!schema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema schema={schema} context={context} />
|
||||
</SectionBody>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
// If there are multiple models, we use a disclosure group to show them all.
|
||||
return (
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={models.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-models">
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema schema={schema} context={context} />
|
||||
</SectionBody>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
],
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './OpenAPIModels';
|
||||
export * from './resolveOpenAPIModels';
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
type Filesystem,
|
||||
type OpenAPIV3,
|
||||
type OpenAPIV3_1,
|
||||
type OpenAPIV3xDocument,
|
||||
shouldIgnoreEntity,
|
||||
} from '@gitbook/openapi-parser';
|
||||
import { dereferenceFilesystem } from '../dereference';
|
||||
import type { OpenAPIModel, OpenAPIModelsData } from '../types';
|
||||
|
||||
//!!TODO: We should return only the models that are used in the block. Still a WIP awaiting future work.
|
||||
|
||||
/**
|
||||
* Resolve an OpenAPI models from a file and compile it to a more usable format.
|
||||
* Models are extracted from the OpenAPI components.schemas
|
||||
*/
|
||||
export async function resolveOpenAPIModels(
|
||||
filesystem: Filesystem<OpenAPIV3xDocument>
|
||||
): Promise<OpenAPIModelsData | null> {
|
||||
const schema = await dereferenceFilesystem(filesystem);
|
||||
|
||||
const models = getOpenAPIComponents(schema);
|
||||
|
||||
return { models };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenAPI components.schemas that are not ignored.
|
||||
*/
|
||||
function getOpenAPIComponents(schema: OpenAPIV3.Document | OpenAPIV3_1.Document): OpenAPIModel[] {
|
||||
const schemas = schema.components?.schemas ?? {};
|
||||
return Object.entries(schemas)
|
||||
.filter(([, schema]) => !shouldIgnoreEntity(schema))
|
||||
.map(([key, schema]) => ({ name: key, schema }));
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { fromJSON, toJSON } from 'flatted';
|
||||
|
||||
import {
|
||||
type Filesystem,
|
||||
type OpenAPIV3,
|
||||
type OpenAPIV3_1,
|
||||
type OpenAPIV3xDocument,
|
||||
dereference,
|
||||
import type {
|
||||
Filesystem,
|
||||
OpenAPIV3,
|
||||
OpenAPIV3_1,
|
||||
OpenAPIV3xDocument,
|
||||
} from '@gitbook/openapi-parser';
|
||||
import { dereferenceFilesystem } from './dereference';
|
||||
import type { OpenAPIOperationData } from './types';
|
||||
import { checkIsReference } from './utils';
|
||||
|
||||
export { toJSON, fromJSON };
|
||||
export { fromJSON, toJSON };
|
||||
|
||||
/**
|
||||
* Resolve an OpenAPI operation in a file and compile it to a more usable format.
|
||||
@@ -23,7 +23,7 @@ export async function resolveOpenAPIOperation(
|
||||
}
|
||||
): Promise<OpenAPIOperationData | null> {
|
||||
const { path, method } = operationDescriptor;
|
||||
const schema = await memoDereferenceFilesystem(filesystem);
|
||||
const schema = await dereferenceFilesystem(filesystem);
|
||||
let operation = getOperationByPathAndMethod(schema, path, method);
|
||||
|
||||
if (!operation) {
|
||||
@@ -69,34 +69,6 @@ export async function resolveOpenAPIOperation(
|
||||
};
|
||||
}
|
||||
|
||||
const dereferenceCache = new WeakMap<Filesystem, Promise<OpenAPIV3xDocument>>();
|
||||
|
||||
/**
|
||||
* Memoized version of `dereferenceSchema`.
|
||||
*/
|
||||
function memoDereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
||||
if (dereferenceCache.has(filesystem)) {
|
||||
return dereferenceCache.get(filesystem) as Promise<OpenAPIV3xDocument>;
|
||||
}
|
||||
|
||||
const promise = dereferenceFilesystem(filesystem);
|
||||
dereferenceCache.set(filesystem, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dereference an OpenAPI schema.
|
||||
*/
|
||||
async function dereferenceFilesystem(filesystem: Filesystem): Promise<OpenAPIV3xDocument> {
|
||||
const result = await dereference(filesystem);
|
||||
|
||||
if (!result.schema) {
|
||||
throw new Error('Failed to dereference OpenAPI document');
|
||||
}
|
||||
|
||||
return result.schema as OpenAPIV3xDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a path object from its path.
|
||||
*/
|
||||
|
||||
@@ -55,3 +55,13 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
/** Securities that should be used for this operation */
|
||||
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
||||
}
|
||||
|
||||
export type OpenAPIModel = {
|
||||
name: string;
|
||||
schema: OpenAPIV3.SchemaObject;
|
||||
};
|
||||
|
||||
export interface OpenAPIModelsData {
|
||||
/** Components schemas to be used for models */
|
||||
models: OpenAPIModel[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
|
||||
export function checkIsReference(
|
||||
input: unknown
|
||||
@@ -10,11 +11,19 @@ export function createStateKey(key: string, scope?: string) {
|
||||
return scope ? `${scope}_${key}` : key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an object has a description. Either at the root level or in items.
|
||||
*/
|
||||
function hasDescription(object: AnyObject) {
|
||||
return 'description' in object || 'x-gitbook-description-html' in object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the description of an object.
|
||||
*/
|
||||
export function resolveDescription(object: OpenAPIV3.SchemaObject | AnyObject) {
|
||||
if ('items' in object && object.items) {
|
||||
// If the object has items and has a description, we resolve the description from items
|
||||
if ('items' in object && typeof object.items === 'object' && hasDescription(object.items)) {
|
||||
return resolveDescription(object.items);
|
||||
}
|
||||
|
||||
@@ -50,9 +59,17 @@ export function resolveFirstExample(object: AnyObject) {
|
||||
return object.examples[firstKey];
|
||||
}
|
||||
}
|
||||
if ('example' in object && object.example !== undefined) {
|
||||
return object.example;
|
||||
|
||||
// Resolve top level example first
|
||||
if (shouldDisplayExample(object)) {
|
||||
return formatExample(object.example);
|
||||
}
|
||||
|
||||
// Resolve example from items if it exists
|
||||
if (object.items && typeof object.items === 'object') {
|
||||
return formatExample(object.items.example);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -98,3 +115,34 @@ export function parameterToProperty(
|
||||
required: parameter.required,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the example of a schema.
|
||||
*/
|
||||
function formatExample(example: unknown): string {
|
||||
if (typeof example === 'string') {
|
||||
return example
|
||||
.replace(/\n/g, ' ') // Replace newlines with spaces
|
||||
.replace(/\s+/g, ' ') // Collapse multiple spaces/newlines into a single space
|
||||
.replace(/([\{\}:,])\s+/g, '$1 ') // Ensure a space after {, }, :, and ,
|
||||
.replace(/\s+([\{\}:,])/g, ' $1') // Ensure a space before {, }, :, and ,
|
||||
.trim();
|
||||
}
|
||||
|
||||
return stringifyOpenAPI(example);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an example should be displayed.
|
||||
*/
|
||||
function shouldDisplayExample(schema: OpenAPIV3.SchemaObject): boolean {
|
||||
return (
|
||||
(typeof schema.example === 'string' && !!schema.example) ||
|
||||
typeof schema.example === 'number' ||
|
||||
typeof schema.example === 'boolean' ||
|
||||
(Array.isArray(schema.example) && schema.example.length > 0) ||
|
||||
(typeof schema.example === 'object' &&
|
||||
schema.example !== null &&
|
||||
Object.keys(schema.example).length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user