vitepress-openapi
Version:
Generate VitePress API Documentation from OpenAPI Specification.
446 lines (377 loc) • 13.4 kB
text/typescript
import type { OpenAPI } from '@scalar/openapi-types'
import { getPropertyExamples } from '../examples/getPropertyExamples'
import { getConstraints, hasConstraints } from './constraintsParser'
import { resolveCircularRef } from './resolveCircularRef'
export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null'
interface Metadata {
isCircularReference?: boolean
isAdditionalProperties?: boolean
isOneOf?: boolean
isOneOfItem?: boolean
isAnyOf?: boolean
isAnyOfItem?: boolean
isConstant?: boolean
isPrefixItem?: boolean
prefixItemIndex?: number
hasPrefixItems?: boolean
isAdditionalItems?: boolean
extra?: Record<string, unknown>
}
interface DocumentationReference {
url?: string
description?: string
}
export interface OAProperty {
name: string
types: JSONSchemaType[]
required: boolean
examples?: unknown[]
title?: string
description?: string
defaultValue?: unknown
docs?: DocumentationReference
constraints?: Record<string, unknown>
properties?: OAProperty[]
items?: OAProperty
enum?: unknown[]
subtype?: JSONSchemaType
subexamples?: unknown[]
nullable?: boolean
meta?: Metadata
}
class UiPropertyFactory {
static createBaseProperty(
name: string,
property: Partial<OpenAPI.SchemaObject> = {},
required = false,
): OAProperty {
const nodeTypes = Array.isArray(property.type)
? property.type
: [property.type || 'string'] as JSONSchemaType[]
const examples = getPropertyExamples(property)
const baseProperty: OAProperty = {
name,
types: nodeTypes,
required,
...(property.title && { title: property.title }),
...(property.description && { description: property.description }),
...(property.default !== undefined && { defaultValue: property.default }),
...(property.externalDocs && { docs: property.externalDocs }),
...(examples && { examples }),
...(property.nullable && { nullable: property.nullable }),
}
if (property.const !== undefined) {
baseProperty.meta = { ...(baseProperty.meta || {}), isConstant: true }
}
if (hasConstraints(property)) {
baseProperty.constraints = getConstraints(property)
}
Object.keys(property).forEach((key) => {
if (key.startsWith('x-')) {
baseProperty.meta = baseProperty.meta || {}
baseProperty.meta.extra = baseProperty.meta.extra || {}
baseProperty.meta.extra[key] = property[key]
}
})
return baseProperty
}
static createCircularReferenceProperty(name: string, circularRef: string): OAProperty {
return {
name,
types: ['object'],
required: false,
description: `Circular reference to **${circularRef}**`,
meta: { isCircularReference: true },
}
}
static createUnionProperty(
unionProperties: Partial<OpenAPI.SchemaObject>[],
unionType: 'oneOf' | 'anyOf',
name: string = '',
baseSchema: Partial<OpenAPI.SchemaObject> = {},
required = false,
): OAProperty {
const baseProperty = UiPropertyFactory.createBaseProperty(
name,
baseSchema,
required,
)
const unionTypes = Array.from(
new Set(
unionProperties
.map(prop => determineSchemaType(prop as OpenAPI.SchemaObject))
.filter(Boolean),
),
) as JSONSchemaType[]
if (unionTypes.length > 0) {
baseProperty.types = unionTypes
}
const isOneOf = unionType === 'oneOf'
const metaItemKey = isOneOf ? 'isOneOfItem' : 'isAnyOfItem'
const metaKey = isOneOf ? 'isOneOf' : 'isAnyOf'
baseProperty.properties = unionProperties.map((prop) => {
const property = UiPropertyFactory.schemaToUiProperty('', prop)
property.meta = { ...(property.meta || {}), [metaItemKey]: true }
return property
})
baseProperty.meta = { ...(baseProperty.meta || {}), [metaKey]: true }
return baseProperty
}
static createOneOfProperty(
oneOfProperties: Partial<OpenAPI.SchemaObject>[],
name: string = '',
baseSchema: Partial<OpenAPI.SchemaObject> = {},
required = false,
): OAProperty {
return UiPropertyFactory.createUnionProperty(oneOfProperties, 'oneOf', name, baseSchema, required)
}
static createAnyOfProperty(
anyOfProperties: Partial<OpenAPI.SchemaObject>[],
name: string = '',
baseSchema: Partial<OpenAPI.SchemaObject> = {},
required = false,
): OAProperty {
return UiPropertyFactory.createUnionProperty(anyOfProperties, 'anyOf', name, baseSchema, required)
}
static schemaToUiProperty(
name: string,
schema: Partial<OpenAPI.SchemaObject>,
required = false,
): OAProperty {
if (!schema || Object.keys(schema).length === 0) {
return {
name,
types: [],
required,
}
}
if (schema.circularReference) {
return UiPropertyFactory.createCircularReferenceProperty(name, schema.circularReference)
}
if (schema.oneOf) {
return UiPropertyFactory.createOneOfProperty(schema.oneOf, name, schema, required)
}
if (schema.anyOf) {
return UiPropertyFactory.createAnyOfProperty(schema.anyOf, name, schema, required)
}
if (schema.const !== undefined) {
// Preserve annotations (title, description, docs, etc.) for annotated enums.
const baseProperty = UiPropertyFactory.createBaseProperty(name, schema, required)
// Ensure type is properly inferred from `const` when not explicitly set.
const inferredType = determineSchemaType(schema as OpenAPI.SchemaObject)
if (inferredType) {
baseProperty.types = [inferredType]
}
// Ensure we have an example showing the const value.
const example = getPropertyExamples(schema) || schema.const
if (example !== undefined) {
baseProperty.examples = Array.isArray(example) ? example : [example]
}
// Mark as constant explicitly (createBaseProperty already sets it, but be explicit).
baseProperty.meta = { ...(baseProperty.meta || {}), isConstant: true }
return baseProperty
}
if (schema.enum) {
let types: JSONSchemaType[] = []
if (schema.type) {
types = Array.isArray(schema.type)
? schema.type as JSONSchemaType[]
: [schema.type as JSONSchemaType]
} else {
types = inferTypesFromEnum(schema.enum)
}
if (types.length === 0) {
types = ['string']
}
const property = UiPropertyFactory.createBaseProperty(name, schema, required)
property.enum = schema.enum
property.types = types
return property
}
const property = UiPropertyFactory.createBaseProperty(name, schema, required)
if (Array.isArray(schema.type) ? schema.type.includes('array') : schema.type === 'array') {
if (schema.items) {
const schemaType = determineSchemaType(schema.items)
property.properties = schemaType === 'object'
? UiPropertyFactory.extractProperties(
schema.items.properties,
schema.items.required || [],
schema.items.additionalProperties,
)
: undefined
if (schemaType !== undefined) {
property.subtype = schemaType as JSONSchemaType
}
const itemsExamples = getPropertyExamples(schema.items)
if (itemsExamples) {
property.subexamples = itemsExamples
}
if (schema.items.const !== undefined) {
property.meta = { ...(property.meta || {}), isConstant: true }
}
if (schema.items.oneOf || schema.items.anyOf) {
const isOneOf = !!schema.items.oneOf
const unionProperties = isOneOf ? schema.items.oneOf : schema.items.anyOf
const metaKey = isOneOf ? 'isOneOf' : 'isAnyOf'
const metaItemKey = isOneOf ? 'isOneOfItem' : 'isAnyOfItem'
property.meta = { ...(property.meta || {}), [metaKey]: true }
property.properties = unionProperties.map((prop: any) => {
const propSchema = { ...prop, type: schema.items.type }
return {
...UiPropertyFactory.schemaToUiProperty('', propSchema),
meta: { ...(prop.meta || {}), [metaItemKey]: true },
}
})
}
// store primitive item details
if (
schemaType !== 'object'
&& schemaType !== 'array'
&& !schema.items.oneOf
&& !schema.items.anyOf
) {
const itemProperty = UiPropertyFactory.schemaToUiProperty('[item]', schema.items)
if (
itemProperty.description
|| itemProperty.enum
|| itemProperty.constraints
|| itemProperty.docs
) {
property.items = itemProperty
}
}
}
if (schema.prefixItems && Array.isArray(schema.prefixItems)) {
property.properties = schema.prefixItems.map((prefixItem, index) => {
const prefixItemProperty = UiPropertyFactory.schemaToUiProperty(
`[${index}]`,
prefixItem,
false,
)
prefixItemProperty.meta = {
...(prefixItemProperty.meta || {}),
isPrefixItem: true,
prefixItemIndex: index,
}
return prefixItemProperty
})
property.meta = {
...(property.meta || {}),
hasPrefixItems: true,
}
}
// Handle case when both prefixItems and items are present.
if (schema.prefixItems && Array.isArray(schema.prefixItems) && schema.items) {
const additionalItemsProperty = UiPropertyFactory.schemaToUiProperty(
'[n+]', // Name indicating "additional items".
schema.items,
false,
)
additionalItemsProperty.meta = {
...(additionalItemsProperty.meta || {}),
isAdditionalItems: true,
}
property.properties = [
...(property.properties || []),
additionalItemsProperty,
]
// Don't set subtype when we have prefixItems.
property.subtype = undefined
}
} else if (Array.isArray(schema.type) ? schema.type.includes('object') : schema.type === 'object') {
property.properties = UiPropertyFactory.extractProperties(
schema.properties,
schema.required || [],
schema.additionalProperties,
)
} else if (schema.type === undefined) {
if (schema.properties || schema.additionalProperties) {
property.types = ['object']
property.properties = UiPropertyFactory.extractProperties(
schema.properties,
schema.required || [],
schema.additionalProperties,
)
}
}
return property
}
static extractProperties(
propertiesNode?: Record<string, OpenAPI.SchemaObject>,
requiredProperties: string[] = [],
additionalPropertiesNode?: OpenAPI.SchemaObject | boolean,
): OAProperty[] {
const properties: OAProperty[] = []
if (propertiesNode) {
Object.entries(propertiesNode).forEach(([key, value]) => {
const isRequired = requiredProperties.includes(key)
properties.push(UiPropertyFactory.schemaToUiProperty(key, value, isRequired))
})
}
if (additionalPropertiesNode) {
const additionalProps = typeof additionalPropertiesNode === 'object'
? additionalPropertiesNode
: { type: 'string' }
properties.push({
name: 'additionalProperties',
types: [additionalProps.type as JSONSchemaType],
required: false,
meta: { isAdditionalProperties: true },
})
}
return properties
}
}
export function getSchemaUi(jsonSchema: OpenAPI.SchemaObject): OAProperty | OAProperty[] {
if (!jsonSchema || Object.keys(jsonSchema).length === 0) {
return []
}
const resolvedSchema = resolveCircularRef(jsonSchema)
return UiPropertyFactory.schemaToUiProperty('', resolvedSchema)
}
function inferTypesFromEnum(values: unknown[]): JSONSchemaType[] {
const types = new Set<JSONSchemaType>()
values.forEach((value) => {
if (value === null) {
types.add('null')
} else if (Array.isArray(value)) {
types.add('array')
} else if (typeof value === 'object') {
types.add('object')
} else if (typeof value === 'string') {
types.add('string')
} else if (typeof value === 'boolean') {
types.add('boolean')
} else if (typeof value === 'number') {
types.add(Number.isInteger(value) ? 'integer' : 'number')
}
})
if (types.has('number')) {
types.delete('integer')
}
return Array.from(types)
}
function determineSchemaType(schema: OpenAPI.SchemaObject): JSONSchemaType {
if (!schema.type && schema.properties) {
return 'object'
}
if (!schema.type && schema.items) {
return 'array'
}
if (!schema.type && schema.const !== undefined) {
if (Array.isArray(schema.const)) {
return 'array'
} else if (typeof schema.const === 'object' && schema.const !== null) {
return 'object'
} else if (typeof schema.const === 'string') {
return 'string'
} else if (typeof schema.const === 'number') {
return 'number'
} else if (typeof schema.const === 'boolean') {
return 'boolean'
} else {
return 'null'
}
}
return schema.type as JSONSchemaType
}