'use client'; // This component does not use any client feature but we don't want to // render it server-side because it has recursion. import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser'; import { useId } from 'react'; import type { ComponentPropsWithoutRef } from 'react'; import clsx from 'clsx'; import { Markdown } from './Markdown'; import { OpenAPICopyButton } from './OpenAPICopyButton'; import { OpenAPIDisclosure } from './OpenAPIDisclosure'; import { OpenAPISchemaName } from './OpenAPISchemaName'; import type { OpenAPIClientContext } from './context'; import { retrocycle } from './decycle'; import { getDisclosureLabel } from './getDisclosureLabel'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import { tString } from './translate'; import { checkIsReference, resolveDescription, resolveFirstExample } from './utils'; type CircularRefsIds = Map; export interface OpenAPISchemaPropertyEntry { propertyName?: string; required?: boolean | null; schema: OpenAPIV3.SchemaObject; } /** * Render a property of an OpenAPI schema. */ function OpenAPISchemaProperty( props: { property: OpenAPISchemaPropertyEntry; context: OpenAPIClientContext; circularRefs: CircularRefsIds; className?: string; } & Omit, 'property' | 'context' | 'circularRefs' | 'className'> ) { const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props; const { schema } = property; const id = useId(); const circularRefId = parentCircularRefs.get(schema); // Avoid recursing infinitely, and instead render a link to the parent schema if (circularRefId) { return ; } const circularRefs = new Map(parentCircularRefs); circularRefs.set(schema, id); const properties = getSchemaProperties(schema); const ancestors = new Set(circularRefs.keys()); const alternatives = getSchemaAlternatives(schema, ancestors); const header = ; const content = (() => { if (properties?.length) { return ( ); } if (alternatives) { return (
{alternatives.map((alternativeSchema, index) => (
{index < alternatives.length - 1 ? ( ) : null}
))}
); } return null; })(); if (properties?.length) { return ( getDisclosureLabel({ schema, isExpanded, context })} {...rest} > {content} ); } return (
{header} {content}
); } /** * Render a set of properties of an OpenAPI schema. */ function OpenAPISchemaProperties(props: { id?: string; properties: OpenAPISchemaPropertyEntry[]; circularRefs?: CircularRefsIds; context: OpenAPIClientContext; }) { const { id, properties, circularRefs = new Map(), context, } = props; return (
{properties.map((property, index) => { return ( ); })}
); } export function OpenAPISchemaPropertiesFromServer(props: { id?: string; properties: string; context: OpenAPIClientContext; }) { return ( ); } /** * Render a root schema (such as the request body or response body). */ function OpenAPIRootSchema(props: { schema: OpenAPIV3.SchemaObject; context: OpenAPIClientContext; circularRefs?: CircularRefsIds; }) { const { schema, context, circularRefs: parentCircularRefs = new Map(), } = props; const id = useId(); const properties = getSchemaProperties(schema); const description = resolveDescription(schema); if (properties?.length) { const circularRefs = new Map(parentCircularRefs); circularRefs.set(schema, id); return ( <> {description ? ( ) : null} ); } return ( ); } export function OpenAPIRootSchemaFromServer(props: { schema: string; context: OpenAPIClientContext; }) { return ( ); } /** * Render a tab for an alternative schema. * It renders directly the properties if relevant; * for primitives, it renders the schema itself. */ function OpenAPISchemaAlternative(props: { schema: OpenAPIV3.SchemaObject; circularRefs: CircularRefsIds; context: OpenAPIClientContext; }) { const { schema, circularRefs, context } = props; const properties = getSchemaProperties(schema); return properties?.length ? ( } label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} > ) : ( ); } function OpenAPISchemaAlternativeSeparator(props: { schema: OpenAPIV3.SchemaObject; context: OpenAPIClientContext; }) { const { schema, context } = props; const anyOf = schema.anyOf || schema.items?.anyOf; const oneOf = schema.oneOf || schema.items?.oneOf; const allOf = schema.allOf || schema.items?.allOf; if (!anyOf && !oneOf && !allOf) { return null; } return ( {(anyOf || oneOf) && tString(context.translation, 'or')} {allOf && tString(context.translation, 'and')} ); } /** * Render a circular reference to a schema. */ function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaObject }) { const { id, schema } = props; return (
Circular reference to {getSchemaTitle(schema)}{' '}
); } /** * Render the enum value for a schema. */ function OpenAPISchemaEnum(props: { schema: OpenAPIV3.SchemaObject & OpenAPICustomOperationProperties; context: OpenAPIClientContext; }) { const { schema, context } = props; const enumValues = (() => { // Render x-gitbook-enum first, as it has a different format if (schema['x-gitbook-enum']) { return Object.entries(schema['x-gitbook-enum']).map(([name, { description }]) => { return { value: name, description, }; }); } if (schema['x-enumDescriptions']) { return Object.entries(schema['x-enumDescriptions']).map(([value, description]) => { return { value, description, }; }); } return schema.enum?.map((value) => { return { value, description: undefined, }; }); })(); if (!enumValues?.length) { return null; } return ( {tString(context.translation, 'possible_values')}:{' '} {enumValues.map((item, index) => ( {`${item.value}`} ))} ); } /** * Render the top row of a schema. e.g: name, type, and required status. */ export function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry; context: OpenAPIClientContext; }) { const { property: { schema, propertyName, required }, context, } = props; const description = resolveDescription(schema); const example = resolveFirstExample(schema); return (
{typeof schema['x-deprecated-sunset'] === 'string' ? (
Sunset date:{' '} {schema['x-deprecated-sunset']}
) : null} {description ? ( ) : null} {schema.default !== undefined ? ( Default:{' '} {typeof schema.default === 'string' && schema.default ? schema.default : stringifyOpenAPI(schema.default)} ) : null} {typeof example === 'string' ? ( Example: {example} ) : null} {schema.pattern ? ( Pattern: {schema.pattern} ) : null}
); } /** * Get the sub-properties of a schema. */ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] { // check array AND schema.items as this is sometimes null despite what the type indicates if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) { const items = schema.items; const itemProperties = getSchemaProperties(items); if (itemProperties) { return itemProperties; } // If the items are a primitive type, we don't need to display them if ( (items.type === 'string' || items.type === 'number' || items.type === 'boolean' || items.type === 'integer') && !items.enum ) { return null; } return [{ propertyName: 'items', schema: items }]; } if (schema.type === 'object' || schema.properties) { const result: OpenAPISchemaPropertyEntry[] = []; if (schema.properties) { Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => { if (checkIsReference(propertySchema)) { return; } result.push({ propertyName, required: Array.isArray(schema.required) ? schema.required.includes(propertyName) : undefined, schema: propertySchema, }); }); } if (schema.additionalProperties && !checkIsReference(schema.additionalProperties)) { result.push({ propertyName: 'Other properties', schema: schema.additionalProperties === true ? {} : schema.additionalProperties, }); } return result; } return null; } type AlternativeType = 'oneOf' | 'allOf' | 'anyOf'; /** * Get the alternatives to display for a schema. */ export function getSchemaAlternatives( schema: OpenAPIV3.SchemaObject, ancestors: Set = new Set() ): OpenAPIV3.SchemaObject[] | null { // Search for alternatives in the items property if it exists if ( schema.items && ('oneOf' in schema.items || 'allOf' in schema.items || 'anyOf' in schema.items) ) { return getSchemaAlternatives(schema.items, ancestors); } const alternatives: | [AlternativeType, (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]] | null = (() => { if (schema.anyOf) { return ['anyOf', schema.anyOf]; } if (schema.oneOf) { return ['oneOf', schema.oneOf]; } if (schema.allOf) { return ['allOf', schema.allOf]; } return null; })(); if (!alternatives) { return null; } const [type, schemas] = alternatives; return mergeAlternatives( type, flattenAlternatives(type, schemas, new Set(ancestors).add(schema)) ); } /** * Merge alternatives of the same type into a single schema. * - Merge string enums */ function mergeAlternatives( alternativeType: AlternativeType, schemasOrRefs: OpenAPIV3.SchemaObject[] ): OpenAPIV3.SchemaObject[] | null { switch (alternativeType) { case 'oneOf': { return schemasOrRefs.reduce((acc, schemaOrRef) => { const latest = acc.at(-1); if ( latest && latest.type === 'string' && latest.enum && schemaOrRef.type === 'string' && schemaOrRef.enum ) { latest.enum = Array.from(new Set([...latest.enum, ...schemaOrRef.enum])); latest.nullable = latest.nullable || schemaOrRef.nullable; return acc; } acc.push(schemaOrRef); return acc; }, []); } case 'allOf': { return schemasOrRefs.reduce((acc, schemaOrRef) => { const latest = acc.at(-1); if ( latest && latest.type === 'string' && latest.enum && schemaOrRef.type === 'string' && schemaOrRef.enum ) { const keys = Object.keys(schemaOrRef); if (keys.every((key) => ['type', 'enum', 'nullable'].includes(key))) { latest.enum = Array.from(new Set([...latest.enum, ...schemaOrRef.enum])); latest.nullable = latest.nullable || schemaOrRef.nullable; return acc; } } if (latest && latest.type === 'object' && schemaOrRef.type === 'object') { const keys = Object.keys(schemaOrRef); if ( keys.every((key) => ['type', 'properties', 'required', 'nullable'].includes(key) ) ) { latest.properties = { ...latest.properties, ...schemaOrRef.properties, }; latest.required = Array.from( new Set([ ...(Array.isArray(latest.required) ? latest.required : []), ...(Array.isArray(schemaOrRef.required) ? schemaOrRef.required : []), ]) ); latest.nullable = latest.nullable || schemaOrRef.nullable; return acc; } } acc.push(schemaOrRef); return acc; }, []); } default: return schemasOrRefs; } } function flattenAlternatives( alternativeType: AlternativeType, schemasOrRefs: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[], ancestors: Set ): OpenAPIV3.SchemaObject[] { // Get the parent schema's required fields from the most recent ancestor const latestAncestor = Array.from(ancestors).pop(); return schemasOrRefs.reduce((acc, schemaOrRef) => { if (checkIsReference(schemaOrRef)) { return acc; } if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) { const schemas = getSchemaAlternatives(schemaOrRef, ancestors); if (schemas) { acc.push( ...schemas.map((schema) => ({ ...schema, required: mergeRequiredFields(schema, latestAncestor), })) ); } return acc; } // For direct schemas, handle required fields const schema = { ...schemaOrRef, required: mergeRequiredFields(schemaOrRef, latestAncestor), }; acc.push(schema); return acc; }, []); } /** * Merge the required fields of a schema with the required fields of its latest ancestor. */ function mergeRequiredFields( schemaOrRef: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, latestAncestor: OpenAPIV3.SchemaObject | undefined ) { if (!schemaOrRef.required && !latestAncestor?.required) { return undefined; } if (checkIsReference(schemaOrRef)) { return latestAncestor?.required; } return Array.from( new Set([...(latestAncestor?.required || []), ...(schemaOrRef.required || [])]) ); } function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { // Otherwise try to infer a nice title let type = 'any'; if (schema.enum || schema['x-enumDescriptions'] || schema['x-gitbook-enum']) { type = `${schema.type} · enum`; // check array AND schema.items as this is sometimes null despite what the type indicates } else if (schema.type === 'array' && !!schema.items) { type = `${getSchemaTitle(schema.items)}[]`; } else if (Array.isArray(schema.type)) { type = schema.type.join(' | '); } else if (schema.type || schema.properties) { type = schema.type ?? 'object'; if (schema.format) { type += ` · ${schema.format}`; } // Only add the title if it's an object (no need for the title of a string, number, etc.) if (type === 'object' && schema.title) { type += ` · ${schema.title.replaceAll(' ', '')}`; } } if ('anyOf' in schema) { type = 'any of'; } else if ('oneOf' in schema) { type = 'one of'; } else if ('allOf' in schema) { type = 'all of'; } else if ('not' in schema) { type = 'not'; } return type; }