vitepress-openapi
Version:
Generate VitePress API Documentation from OpenAPI Specification.
282 lines (241 loc) • 8.11 kB
text/typescript
import type { OpenAPI } from '@scalar/openapi-types'
import { literalTypes } from '../../index'
import { getPropertyExamples } from '../examples/getPropertyExamples'
import { getConstraints, hasConstraints } from './constraintsParser'
import { resolveCircularRef } from './resolveCircularRef'
type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null'
interface Metadata {
isCircularReference?: boolean
isAdditionalProperties?: boolean
isOneOf?: boolean
isOneOfItem?: boolean
isConstant?: 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[]
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 createOneOfProperty(oneOfProperties: Partial<OpenAPI.SchemaObject>[], name: string = ''): OAProperty {
return {
name,
types: ['object'],
required: false,
properties: oneOfProperties.map((prop) => {
const property = UiPropertyFactory.schemaToUiProperty('', prop)
property.meta = { ...(property.meta || {}), isOneOfItem: true }
return property
}),
meta: { isOneOf: true },
}
}
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)
}
if (schema.const !== undefined) {
const example = getPropertyExamples(schema) || schema.const
return {
name,
types: [schema.type as JSONSchemaType || 'string'],
required: false,
examples: [example],
meta: { isConstant: true },
}
}
if (literalTypes.includes(String(schema.type)) && schema.enum) {
return {
name,
types: [schema.type as JSONSchemaType],
required: false,
enum: schema.enum,
description: schema.description,
}
}
const property = UiPropertyFactory.createBaseProperty(name, schema, required)
switch (schema.type) {
case '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) {
property.meta = { ...(property.meta || {}), isOneOf: true }
property.properties = schema.items.oneOf.map((prop: any) => {
const propSchema = { ...prop, type: schema.items.type }
return {
...UiPropertyFactory.schemaToUiProperty('', propSchema),
meta: { ...(prop.meta || {}), isOneOfItem: true },
}
})
}
}
break
case 'object':
property.properties = UiPropertyFactory.extractProperties(
schema.properties,
schema.required || [],
schema.additionalProperties,
)
break
case undefined:
if (schema.properties || schema.additionalProperties) {
property.types = ['object']
property.properties = UiPropertyFactory.extractProperties(
schema.properties,
schema.required || [],
schema.additionalProperties,
)
}
break
}
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 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
}