'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 'classnames'; 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, getSchemaTitle, resolveDescription, resolveFirstExample } from './utils'; type CircularRefsIds = Map; export interface OpenAPISchemaPropertyEntry { propertyName?: string; required?: boolean | null; isDiscriminatorProperty?: boolean; 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 (alternatives?.schemas) { const { schemas, discriminator } = alternatives; return (
{schemas.map((alternativeSchema, index) => (
{index < schemas.length - 1 ? ( ) : null}
))}
); } if (properties?.length) { return ( ); } 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; discriminator: OpenAPIV3.DiscriminatorObject | undefined; circularRefs: CircularRefsIds; context: OpenAPIClientContext; }) { const { schema, discriminator, circularRefs, context } = props; const properties = getSchemaProperties(schema, discriminator); 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, isDiscriminatorProperty }, 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, discriminator?: OpenAPIV3.DiscriminatorObject | undefined ): 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.map((prop) => ({ ...prop, isDiscriminatorProperty: discriminator?.propertyName === prop.propertyName, })); } // 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, isDiscriminatorProperty: discriminator?.propertyName === propertyName, 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'; type SchemaAlternatives = { type: AlternativeType; schemas: OpenAPIV3.SchemaObject[]; discriminator?: OpenAPIV3.DiscriminatorObject; } | null; /** * Get the alternatives to display for a schema. */ export function getSchemaAlternatives( schema: OpenAPIV3.SchemaObject, ancestors: Set = new Set() ): SchemaAlternatives { // Check for nested alternatives in `items` 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)[], OpenAPIV3.DiscriminatorObject?, ] | null = (() => { if (schema.anyOf) { return ['anyOf', schema.anyOf, schema.discriminator]; } if (schema.oneOf) { return ['oneOf', schema.oneOf, schema.discriminator]; } if (schema.allOf) { return ['allOf', schema.allOf, schema.discriminator]; } return null; })(); if (!alternatives) { return null; } const [type, schemas, discriminator] = alternatives; return { type, schemas: mergeAlternatives( type, flattenAlternatives(type, schemas, new Set(ancestors).add(schema)) ) ?? [], discriminator, }; } // These extensions are safe to merge const safeExtensions = [ 'description', 'title', 'example', 'examples', 'default', 'readOnly', 'writeOnly', 'deprecated', ]; /** * Determine if a schema is safe to merge based on its properties */ function isSafeToMerge(schema: OpenAPIV3.SchemaObject): boolean { const keys = Object.keys(schema); const coreProperties = ['type', 'properties', 'required', 'nullable']; const coreKeys = keys.filter((key) => coreProperties.includes(key)); const unknownKeys = keys.filter( (key) => !coreProperties.includes(key) && !safeExtensions.includes(key) && !key.startsWith('x-') ); return coreKeys.length > 0 && unknownKeys.length === 0; } /** * Merge alternatives of the same type into a single schema. * - Merge string enums * - Safely merge object schemas with compatible properties */ 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 (isSafeToMerge(schemaOrRef)) { const safeKeys = keys.filter((key) => safeExtensions.includes(key)); const vendorKeys = keys.filter((key) => key.startsWith('x-')); latest.properties = { ...(latest.properties || {}), ...(schemaOrRef.properties || {}), }; latest.required = Array.from( new Set([ ...(latest.required && Array.isArray(latest.required) ? latest.required : []), ...(schemaOrRef.required && Array.isArray(schemaOrRef.required) ? schemaOrRef.required : []), ]) ); latest.nullable = latest.nullable || schemaOrRef.nullable; // Preserve safe extensions and vendor extensions // Always overwrite (last schema has priority) [...vendorKeys, ...safeKeys].forEach((key) => { if ( typeof latest[key] === 'object' && typeof schemaOrRef[key] === 'object' ) { latest[key] = { ...latest[key], ...schemaOrRef[key] }; } else { latest[key] = schemaOrRef[key]; } }); 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 alternatives = getSchemaAlternatives(schemaOrRef, ancestors); if (alternatives?.schemas) { acc.push( ...alternatives.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 || [])]) ); }