Implement OpenAPI models blocks (#2908)

This commit is contained in:
Nolann B.
2025-03-07 09:44:14 +01:00
committed by GitHub
parent 8ee9757132
commit bb3ca9c165
20 changed files with 457 additions and 144 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@gitbook/openapi-parser': minor
'@gitbook/react-openapi': minor
'gitbook': minor
---
Implement OpenAPI models blocks
+2 -1
View File
@@ -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,
});
+17 -59
View File
@@ -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;
}
}
+50
View File
@@ -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;
};
+11 -3
View File
@@ -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>
+10 -32
View File
@@ -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);
}
+29
View File
@@ -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;
}
+3 -2
View File
@@ -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.
*/
+10
View File
@@ -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[];
}
+51 -3
View File
@@ -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)
);
}