UNPKG

fumadocs-openapi

Version:

Generate MDX docs for your OpenAPI spec

213 lines (212 loc) 9.65 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Fragment } from 'react'; import { combineSchema } from '../utils/combine-schema.js'; import { Markdown } from './markdown.js'; import { schemaToString } from '../utils/schema-to-string.js'; import { Tabs, TabsContent, TabsList, TabsTrigger, } from 'fumadocs-ui/components/tabs'; export function Schema({ name, schema, required = false, readOnly = false, writeOnly = false, as = 'property', ctx: renderContext, }) { const { renderer } = renderContext; function propertyBody(schema, renderPrimitive, ctx) { if (Array.isArray(schema.type)) { const items = schema.type.flatMap((type) => { const composed = { ...schema, type, }; if (!isComplexType(composed)) return []; return composed; }); if (items.length === 0) return; if (items.length === 1) return propertyBody(items[0], renderPrimitive, ctx); return (_jsxs(Tabs, { defaultValue: items[0].type, children: [_jsx(TabsList, { children: items.map((item) => (_jsx(TabsTrigger, { value: item.type, children: schemaToString(item, renderContext.schema) }, item.type))) }), items.map((item) => (_jsxs(TabsContent, { value: item.type, children: [item.description && _jsx(Markdown, { text: item.description }), propertyInfo(item), renderPrimitive(item, ctx)] }, item.type)))] })); } if (schema.oneOf) { const oneOf = schema.oneOf.filter((item) => isComplexType(item)); if (oneOf.length === 0) return; if (oneOf.length === 1) { return propertyBody(oneOf[0], renderPrimitive, ctx); } return (_jsxs(Tabs, { defaultValue: "0", children: [_jsx(TabsList, { children: oneOf.map((item, i) => (_jsx(TabsTrigger, { value: i.toString(), children: schemaToString(item, renderContext.schema) }, i))) }), oneOf.map((item, i) => (_jsxs(TabsContent, { value: i.toString(), children: [item.description && _jsx(Markdown, { text: item.description }), propertyInfo(item), propertyBody(item, (child, ctx) => primitiveBody(child, ctx, false, true), ctx)] }, i)))] })); } const of = schema.allOf ?? schema.anyOf; if (of) { const arr = of.filter((item) => !ctx.stack.has(item)); if (arr.length === 0) return; const combined = combineSchema(arr); if (typeof combined === 'boolean') return; return renderPrimitive(combined, ctx); } return renderPrimitive(schema, ctx); } function propertyInfo(schema) { const fields = []; if (schema.default !== undefined) { fields.push({ key: 'Default', value: JSON.stringify(schema.default), }); } if (schema.pattern) { fields.push({ key: 'Match', value: schema.pattern, }); } if (schema.format) { fields.push({ key: 'Format', value: schema.format, }); } if (schema.multipleOf) { fields.push({ key: 'Multiple Of', value: String(schema.multipleOf), }); } let range = getRange('value', schema.minimum, schema.exclusiveMinimum, schema.maximum, schema.exclusiveMaximum); if (range) fields.push({ key: 'Range', value: range, }); range = getRange('length', schema.minLength, undefined, schema.maxLength, undefined); if (range) fields.push({ key: 'Length', value: range, }); range = getRange('properties', schema.minProperties, undefined, schema.maxProperties, undefined); if (range) fields.push({ key: 'Properties', value: range, }); if (schema.enum) { fields.push({ key: 'Value in', value: schema.enum.map((value) => JSON.stringify(value)).join(' | '), }); } if (fields.length === 0) return; return (_jsx("div", { className: "flex flex-wrap gap-2 not-prose", children: fields.map((field) => (_jsxs("div", { className: "bg-fd-secondary border rounded-lg text-xs p-1.5 shadow-md", children: [_jsx("span", { className: "font-medium me-2", children: field.key }), _jsx("code", { className: "text-fd-muted-foreground", children: field.value })] }, field.key))) })); } function primitiveBody(schema, ctx, collapsible, nested) { if (schema.type === 'object') { if (ctx.stack.has(schema)) return _jsx("p", { children: "Recursive" }); const props = Object.entries(schema.properties ?? {}); const patternProps = Object.entries(schema.patternProperties ?? {}); const next = { ...ctx, stack: ctx.stack.next(schema), }; if (props.length === 0 && patternProps.length === 0) return _jsx("p", { children: "Empty Object" }); const children = (_jsxs(_Fragment, { children: [props.map(([key, value]) => (_jsx(Fragment, { children: property(key, value, next, { required: schema.required?.includes(key) ?? false, nested, }) }, key))), patternProps.map(([key, value]) => (_jsx(Fragment, { children: property(key, value, next, { nested }) }, key))), schema.additionalProperties && property('[key: string]', schema.additionalProperties, next, { nested, })] })); if (!collapsible) return children; return (_jsx(renderer.ObjectCollapsible, { name: "Show Attributes", children: children })); } if (schema.type === 'array') { const items = schema.items; if (!items || typeof items === 'boolean' || ctx.stack.has(items)) return; return (_jsxs(renderer.ObjectCollapsible, { name: "Array Item", children: [_jsxs("div", { className: "text-sm border-t p-3 border-x prose-no-margin bg-fd-card last:rounded-b-xl first:rounded-tr-xl last:border-b empty:hidden", children: [_jsx(Markdown, { text: items.description ?? 'No Description' }), propertyInfo(items)] }), propertyBody(items, (child, ctx) => primitiveBody(child, ctx, false, true), { ...ctx, stack: ctx.stack.next(schema), })] })); } } function property(key, schema, ctx, props) { if (schema === true) { return _jsx(renderer.Property, { name: key, type: "any", ...props }); } else if (schema === false) { return _jsx(renderer.Property, { name: key, type: "never", ...props }); } if ((schema.readOnly && !readOnly) || (schema.writeOnly && !writeOnly)) return; return (_jsxs(renderer.Property, { name: key, type: schemaToString(schema, renderContext.schema), deprecated: schema.deprecated, ...props, children: [schema.description && _jsx(Markdown, { text: schema.description }), propertyInfo(schema), propertyBody(schema, (child, ctx) => primitiveBody(child, ctx, true, true), ctx)] })); } const context = { stack: schemaStack(), }; if (typeof schema === 'boolean' || as === 'property' || !isComplexType(schema)) return property(name, schema, context, { required }); return propertyBody(schema, (child, ctx) => primitiveBody(child, ctx, false, false), context); } function schemaStack(parent) { const titles = new Set(); const history = new WeakSet(); return { next(...schemas) { const child = schemaStack(this); for (const item of schemas) { child.add(item); } return child; }, add(schema) { if (typeof schema !== 'object') return; if (schema.title) titles.add(schema.title); history.add(schema); }, has(schema) { if (typeof schema !== 'object') return false; if (parent && parent.has(schema)) return true; if (schema.title && titles.has(schema.title)) return true; return history.has(schema); }, }; } /** * Check if the schema needs another collapsible to explain */ function isComplexType(schema) { if (typeof schema === 'boolean') return false; const arr = schema.anyOf ?? schema.oneOf ?? schema.allOf; if (arr && arr.some(isComplexType)) return true; return (schema.type === 'object' || (schema.type === 'array' && schema.items != null)); } function getRange(value, min, exclusiveMin, max, exclusiveMax) { const out = []; if (min !== undefined) { out.push(`${min} <=`); } else if (exclusiveMin !== undefined) { out.push(`${exclusiveMin} <`); } out.push(value); if (max !== undefined) { out.push(`<= ${max}`); } else if (exclusiveMax !== undefined) { out.push(`< ${exclusiveMax}`); } if (out.length > 1) return out.join(' '); }