mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +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,
|
"prettier.enable": false,
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.codeActionsOnSave": {
|
"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 { Icon } from '@gitbook/icons';
|
||||||
import { OpenAPIOperation } from '@gitbook/react-openapi';
|
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 { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import type { BlockProps } from '../Block';
|
import type { BlockProps } from '../Block';
|
||||||
@@ -12,11 +12,12 @@ import { Heading } from '../Heading';
|
|||||||
|
|
||||||
import './scalar.css';
|
import './scalar.css';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
import type { AnyOpenAPIBlock } from '@/lib/openapi/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render an openapi block or an openapi-operation block.
|
* 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;
|
const { style } = props;
|
||||||
return (
|
return (
|
||||||
<div className={tcls('flex w-full', style, 'max-w-full')}>
|
<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;
|
const { block, context } = props;
|
||||||
|
|
||||||
if (!context.contentContext) {
|
if (!context.contentContext) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, specUrl, error } = await resolveOpenAPIBlock({
|
const { data, specUrl, error } = await resolveOpenAPIOperationBlock({
|
||||||
block,
|
block,
|
||||||
context: context.contentContext,
|
context: context.contentContext,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
@apply flex-1 flex flex-col gap-4 mb-14;
|
@apply flex-1 flex flex-col gap-4 mb-14;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-models {
|
||||||
|
@apply flex flex-col mb-14 flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
.openapi-columns {
|
.openapi-columns {
|
||||||
@apply grid grid-cols-1 lg:grid-cols-2 gap-6 print-mode:grid-cols-1 justify-stretch;
|
@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 {
|
.openapi-section-body.openapi-schema.openapi-schema-root {
|
||||||
@apply space-y-2.5;
|
@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 type { GitBookAnyContext } from '@v2/lib/context';
|
||||||
|
|
||||||
import { getNodeText } from './document';
|
import { getNodeText } from './document';
|
||||||
import { resolveOpenAPIBlock } from './openapi/fetch';
|
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
|
||||||
|
|
||||||
export interface DocumentSection {
|
export interface DocumentSection {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -38,7 +38,7 @@ export async function getDocumentSections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
|
if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
|
||||||
const { data: operation } = await resolveOpenAPIBlock({
|
const { data: operation } = await resolveOpenAPIOperationBlock({
|
||||||
block,
|
block,
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,82 +1,40 @@
|
|||||||
import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation } from '@gitbook/api';
|
import { parseOpenAPI } from '@gitbook/openapi-parser';
|
||||||
import { OpenAPIParseError, parseOpenAPI } from '@gitbook/openapi-parser';
|
|
||||||
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
|
|
||||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
|
||||||
|
|
||||||
import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache';
|
import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache';
|
||||||
|
import type { ResolveOpenAPIBlockArgs } from '@/lib/openapi/types';
|
||||||
import { assert } from 'ts-essentials';
|
import { assert } from 'ts-essentials';
|
||||||
import { resolveContentRef } from '../references';
|
import { resolveContentRef } from '../references';
|
||||||
import { isV2 } from '../v2';
|
import { isV2 } from '../v2';
|
||||||
import { enrichFilesystem } from './enrich';
|
import { enrichFilesystem } from './enrich';
|
||||||
|
import type { FetchOpenAPIFilesystemResult } from './types';
|
||||||
export type AnyOpenAPIOperationBlock = DocumentBlockOpenAPI | DocumentBlockOpenAPIOperation;
|
|
||||||
|
|
||||||
const weakmap = new WeakMap<AnyOpenAPIOperationBlock, Promise<ResolveOpenAPIBlockResult>>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache the result of resolving an OpenAPI block.
|
* Fetch OpenAPI block.
|
||||||
* It is important because the resolve is called in sections and in the block itself.
|
|
||||||
*/
|
*/
|
||||||
export function resolveOpenAPIBlock(
|
export async function fetchOpenAPIFilesystem(
|
||||||
args: ResolveOpenAPIBlockArgs
|
args: ResolveOpenAPIBlockArgs
|
||||||
): Promise<ResolveOpenAPIBlockResult> {
|
): Promise<FetchOpenAPIFilesystemResult> {
|
||||||
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> {
|
|
||||||
const { context, block } = args;
|
const { context, block } = args;
|
||||||
if (!block.data.path || !block.data.method) {
|
|
||||||
return { data: null, specUrl: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const ref = block.data.ref;
|
const ref = block.data.ref;
|
||||||
const resolved = ref ? await resolveContentRef(ref, context) : null;
|
const resolved = ref ? await resolveContentRef(ref, context) : null;
|
||||||
|
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
return { data: null, specUrl: null };
|
return { filesystem: null, specUrl: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const filesystem = await (() => {
|
||||||
const filesystem = await (() => {
|
if (ref.kind === 'openapi') {
|
||||||
if (ref.kind === 'openapi') {
|
assert(resolved.openAPIFilesystem);
|
||||||
assert(resolved.openAPIFilesystem);
|
return 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 };
|
|
||||||
}
|
}
|
||||||
|
return fetchFilesystem(resolved.href);
|
||||||
|
})();
|
||||||
|
|
||||||
throw error;
|
return {
|
||||||
}
|
filesystem,
|
||||||
|
specUrl: resolved.href,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchFilesystem(url: string) {
|
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);
|
const keys = Object.keys(specification);
|
||||||
await Promise.all(
|
const results = await Promise.all(
|
||||||
keys.map(async (key) => {
|
keys.map(async (key, index) => {
|
||||||
const value = specification[key];
|
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>;
|
return transform(result, path) as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type TDisclosureGroup = {
|
|||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
tabs?: {
|
tabs?: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string | React.ReactNode;
|
label?: string | React.ReactNode;
|
||||||
body?: React.ReactNode;
|
body?: React.ReactNode;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
@@ -121,7 +121,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
) : group.tabs[0] ? (
|
) : group.tabs[0]?.label ? (
|
||||||
<span>{group.tabs[0].label}</span>
|
<span>{group.tabs[0].label}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import clsx from 'clsx';
|
|||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
||||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
|
||||||
import type { OpenAPIClientContext } from './types';
|
import type { OpenAPIClientContext } from './types';
|
||||||
import { checkIsReference, resolveDescription } from './utils';
|
import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
|
||||||
|
|
||||||
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
|
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.
|
* Render the enum value for a schema.
|
||||||
*/
|
*/
|
||||||
export function OpenAPISchemaEnum(props: { enumValues: any[] }) {
|
function OpenAPISchemaEnum(props: { enumValues: any[] }) {
|
||||||
const { enumValues } = props;
|
const { enumValues } = props;
|
||||||
|
|
||||||
return (
|
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 {
|
const {
|
||||||
property: { schema, propertyName, required },
|
property: { schema, propertyName, required },
|
||||||
} = props;
|
} = 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 description = resolveDescription(schema);
|
||||||
|
const example = resolveFirstExample(schema);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openapi-schema-presentation">
|
<div className="openapi-schema-presentation">
|
||||||
@@ -237,9 +228,9 @@ export function OpenAPISchemaPresentation(props: { property: OpenAPISchemaProper
|
|||||||
{description ? (
|
{description ? (
|
||||||
<Markdown source={description} className="openapi-schema-description" />
|
<Markdown source={description} className="openapi-schema-description" />
|
||||||
) : null}
|
) : null}
|
||||||
{shouldDisplayExample(schema) ? (
|
{example ? (
|
||||||
<div className="openapi-schema-example">
|
<div className="openapi-schema-example">
|
||||||
Example: <code>{formatExample(schema.example)}</code>
|
Example: <code>{example}</code>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{schema.pattern ? (
|
{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
|
// Otherwise try to infer a nice title
|
||||||
let type = 'any';
|
let type = 'any';
|
||||||
|
|
||||||
@@ -418,16 +409,3 @@ function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string {
|
|||||||
|
|
||||||
return schema.title || 'child attributes';
|
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 './OpenAPIOperation';
|
||||||
export * from './OpenAPIOperationContext';
|
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 { fromJSON, toJSON } from 'flatted';
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
type Filesystem,
|
Filesystem,
|
||||||
type OpenAPIV3,
|
OpenAPIV3,
|
||||||
type OpenAPIV3_1,
|
OpenAPIV3_1,
|
||||||
type OpenAPIV3xDocument,
|
OpenAPIV3xDocument,
|
||||||
dereference,
|
|
||||||
} from '@gitbook/openapi-parser';
|
} from '@gitbook/openapi-parser';
|
||||||
|
import { dereferenceFilesystem } from './dereference';
|
||||||
import type { OpenAPIOperationData } from './types';
|
import type { OpenAPIOperationData } from './types';
|
||||||
import { checkIsReference } from './utils';
|
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.
|
* 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> {
|
): Promise<OpenAPIOperationData | null> {
|
||||||
const { path, method } = operationDescriptor;
|
const { path, method } = operationDescriptor;
|
||||||
const schema = await memoDereferenceFilesystem(filesystem);
|
const schema = await dereferenceFilesystem(filesystem);
|
||||||
let operation = getOperationByPathAndMethod(schema, path, method);
|
let operation = getOperationByPathAndMethod(schema, path, method);
|
||||||
|
|
||||||
if (!operation) {
|
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.
|
* 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 that should be used for this operation */
|
||||||
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
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 type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||||
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||||
|
|
||||||
export function checkIsReference(
|
export function checkIsReference(
|
||||||
input: unknown
|
input: unknown
|
||||||
@@ -10,11 +11,19 @@ export function createStateKey(key: string, scope?: string) {
|
|||||||
return scope ? `${scope}_${key}` : key;
|
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.
|
* Resolve the description of an object.
|
||||||
*/
|
*/
|
||||||
export function resolveDescription(object: OpenAPIV3.SchemaObject | AnyObject) {
|
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);
|
return resolveDescription(object.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +59,17 @@ export function resolveFirstExample(object: AnyObject) {
|
|||||||
return object.examples[firstKey];
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,3 +115,34 @@ export function parameterToProperty(
|
|||||||
required: parameter.required,
|
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