vitepress-openapi
Version:
Generate VitePress API Documentation from OpenAPI Specification.
447 lines (384 loc) • 13 kB
text/typescript
import type { OpenAPIV3 } from '@scalar/openapi-types'
import type { PlaygroundSecurityScheme } from '../../types'
import { unref } from 'vue'
import { DEFAULT_BASE_URL } from '../../composables/useTheme'
import { getPropertyExample } from '../examples/getPropertyExample'
import { resolveBaseUrl } from '../utils/resolveBaseUrl'
import { OARequest } from './request'
type ParameterValue =
| string
| number
| boolean
| Record<string, unknown>
| Array<string | number | boolean | Record<string, unknown>>
function processParameters(
variables: Record<string, ParameterValue>,
parameters: OpenAPIV3.ParameterObject[],
callback: (key: string, value: string) => void,
) {
const parameterNames = new Set(parameters.map(parameter => parameter.name))
for (const [key, rawValue] of Object.entries(variables)) {
if (!parameterNames.has(key)) {
continue
}
if (rawValue === undefined || rawValue === '') {
continue
}
callback(key, String(rawValue))
}
}
function getPath(variables: Record<string, ParameterValue>, pathParameters: OpenAPIV3.ParameterObject[], path: string = '') {
let resolvedPath = path
processParameters(variables, pathParameters, (key, value) => {
resolvedPath = resolvedPath.replace(`{${key}}`, value)
})
return resolvedPath
}
function getHeaders(
headers: Record<string, string> | Headers | undefined,
variables: Record<string, ParameterValue>,
headerParameters: OpenAPIV3.ParameterObject[],
authorizations: PlaygroundSecurityScheme | PlaygroundSecurityScheme[],
): Record<string, string> {
const resolvedHeaders = new Headers()
if (headers) {
if (headers instanceof Headers) {
for (const [key, value] of headers.entries()) {
resolvedHeaders.set(key.toLowerCase(), value)
}
} else {
for (const [key, value] of Object.entries(headers)) {
resolvedHeaders.set(key.toLowerCase(), value)
}
}
}
processParameters(variables, headerParameters, (key: string, value: string) => {
resolvedHeaders.set(key.toLowerCase(), value)
})
getAuthorizationsHeaders(authorizations).forEach((value: string, key: string) => {
resolvedHeaders.set(key.toLowerCase(), value)
})
return Object.fromEntries(resolvedHeaders)
}
export function getAuthorizationsHeaders(authorizations: PlaygroundSecurityScheme | PlaygroundSecurityScheme[]) {
const headers = new Headers()
if (!authorizations) {
return headers
}
const authArray = Array.isArray(authorizations) ? authorizations : [authorizations]
if (authArray.length === 0) {
return headers
}
for (const authorization of authArray) {
if (!authorization?.type) {
continue
}
const value = unref(authorization.value ?? authorization.name ?? '')
if (!value) {
console.warn('Empty value for authorization scheme:', authorization.type)
continue
}
switch (authorization.type) {
case 'http':
headers.set('Authorization', value)
break
case 'apiKey':
if (!authorization.in || authorization.in === 'header') {
headers.set(authorization.name ?? '', value)
}
break
case 'openIdConnect':
case 'oauth2':
headers.set('Authorization', `Bearer ${value}`)
break
default:
console.warn('Unknown authorization type:', authorization.type)
}
}
return headers
}
function serializeDeepObject(key: string, value: Record<string, unknown>): Record<string, string> {
const result: Record<string, string> = {}
Object.entries(value).forEach(([k, v]) => {
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
const nested = serializeDeepObject(`${key}[${k}]`, v as Record<string, unknown>)
Object.assign(result, nested)
} else if (Array.isArray(v)) {
// Check if array contains only primitives or has objects/arrays
const hasComplexElements = v.some(el => el !== null && typeof el === 'object')
if (hasComplexElements) {
// JSON stringify arrays with objects or nested arrays
result[`${key}[${k}]`] = JSON.stringify(v)
} else {
// Use comma-separated for arrays of primitives
result[`${key}[${k}]`] = v.join(',')
}
} else if (v !== null && typeof v === 'object') {
// JSON stringify non-plain objects
result[`${key}[${k}]`] = JSON.stringify(v)
} else {
result[`${key}[${k}]`] = String(v)
}
})
return result
}
function serializeParameter(
key: string,
value: ParameterValue,
style: OpenAPIV3.ParameterObject['style'] = 'form',
explode = true,
): Record<string, string | string[]> {
if (value === undefined || value === null || value === '') {
return {}
}
if (Array.isArray(value)) {
// Helper to serialize array elements
const serializeElement = (element: unknown): string => {
if (Array.isArray(element) || (element !== null && typeof element === 'object')) {
return JSON.stringify(element)
}
return String(element)
}
const serializedValues = value.map(serializeElement)
if (style === 'spaceDelimited') {
if (!explode) {
return { [key]: serializedValues.join(' ') }
}
// Return as array for repeated parameters
return { [key]: serializedValues }
} else if (style === 'pipeDelimited') {
if (!explode) {
return { [key]: serializedValues.join('|') }
}
// Return as array for repeated parameters
return { [key]: serializedValues }
} else if (style === 'form') {
if (explode) {
// Return as array for repeated parameters
return { [key]: serializedValues }
}
// Return comma-separated
return { [key]: serializedValues.join(',') }
}
// Default: explode behavior
return { [key]: explode ? serializedValues : serializedValues.join(',') }
}
if (typeof value === 'object') {
if (style === 'deepObject') {
if (explode) {
return serializeDeepObject(key, value as Record<string, unknown>)
} else {
const serialized = Object.entries(value).map(([k, v]) => `${k},${v}`).join(',')
return { [key]: serialized }
}
}
if (style === 'form') {
if (explode) {
return Object.entries(value).reduce((acc, [k, v]) => {
if (Array.isArray(v)) {
const hasComplexElements = v.some(el => el !== null && typeof el === 'object')
acc[k] = hasComplexElements ? JSON.stringify(v) : v.join(',')
} else if (v !== null && typeof v === 'object') {
acc[k] = JSON.stringify(v)
} else {
acc[k] = String(v)
}
return acc
}, {} as Record<string, string>)
} else {
const serialized = Object.entries(value).map(([k, v]) => {
let serializedValue: string
if (Array.isArray(v)) {
const hasComplexElements = v.some(el => el !== null && typeof el === 'object')
serializedValue = hasComplexElements ? JSON.stringify(v) : v.join(',')
} else if (v !== null && typeof v === 'object') {
serializedValue = JSON.stringify(v)
} else {
serializedValue = String(v)
}
return `${k},${serializedValue}`
}).join(',')
return { [key]: serialized }
}
}
return { [key]: JSON.stringify(value) }
}
return { [key]: String(value) }
}
function getQuery(
variables: Record<string, ParameterValue>,
queryParameters: OpenAPIV3.ParameterObject[],
): Record<string, string | string[]> {
let query: Record<string, string | string[]> = {}
queryParameters.forEach((parameter) => {
if (!parameter.name) {
return
}
const value = variables[parameter.name]
if (value === undefined || value === '') {
return
}
// Default style for query is form, explode is true
const style = parameter.style || 'form'
const explode = parameter.explode ?? true
const serialized = serializeParameter(parameter.name, value, style, explode)
query = { ...query, ...serialized }
})
return query
}
function getCookies(
variables: Record<string, ParameterValue>,
cookieParameters: OpenAPIV3.ParameterObject[],
) {
const cookies: Record<string, string> = {}
processParameters(variables, cookieParameters, (key: string, value: string) => {
cookies[key] = value
})
return cookies
}
export function getAuthorizationsQuery(authorizations: PlaygroundSecurityScheme | PlaygroundSecurityScheme[]) {
const params: Record<string, string> = {}
if (!authorizations) {
return params
}
const authArray = Array.isArray(authorizations) ? authorizations : [authorizations]
for (const authorization of authArray) {
if (!authorization?.type) {
continue
}
if (authorization.type === 'apiKey' && authorization.in === 'query') {
const value = unref(authorization.value ?? authorization.name ?? '')
if (!value) {
continue
}
const name = authorization.name ?? ''
if (!name) {
continue
}
params[name] = value
}
}
return params
}
export function getAuthorizationsCookies(authorizations: PlaygroundSecurityScheme | PlaygroundSecurityScheme[]) {
const cookies: Record<string, string> = {}
if (!authorizations) {
return cookies
}
const authArray = Array.isArray(authorizations) ? authorizations : [authorizations]
for (const authorization of authArray) {
if (!authorization?.type) {
continue
}
if (authorization.type === 'apiKey' && authorization.in === 'cookie') {
const value = unref(authorization.value ?? authorization.name ?? '')
if (!value) {
continue
}
const name = authorization.name ?? ''
if (!name) {
continue
}
cookies[name] = value
}
}
return cookies
}
function setExamplesAsVariables(parameters: OpenAPIV3.ParameterObject[], variables: Record<string, string>): Record<string, string> {
parameters.forEach((parameter) => {
if (!parameter.name) {
return
}
if (variables[parameter.name] !== undefined) {
return
}
const example = getPropertyExample(parameter)
if (example != null) {
if (typeof example === 'object' && example !== null) {
variables[parameter.name] = JSON.stringify(example)
} else {
variables[parameter.name] = String(example)
}
}
})
return variables
}
export function buildRequest({
url = undefined,
path,
method = 'GET' as OpenAPIV3.HttpMethods,
baseUrl,
parameters = [],
authorizations = [],
body = undefined,
headers = undefined,
variables = {},
cookies = {},
contentType = undefined,
}: Partial<OARequest>): OARequest {
const resolvedVariables = setExamplesAsVariables(parameters, variables)
const pathParameters = parameters.filter(parameter => parameter.in === 'path')
const queryParameters = parameters.filter(parameter => parameter.in === 'query')
const headerParameters = parameters.filter(parameter => parameter.in === 'header')
const cookieParameters = parameters.filter(parameter => parameter.in === 'cookie')
if (import.meta.env.VITE_DEBUG) {
console.warn('Building request with parameters:', {
path,
method,
baseUrl,
pathParameters,
queryParameters,
headerParameters,
cookieParameters,
authorizations,
body,
resolvedVariables,
})
}
const resolvedPath = getPath(resolvedVariables, pathParameters, path)
const resolveMethod = (method?.toUpperCase() || 'GET') as OpenAPIV3.HttpMethods
const resolvedQuery = {
...getQuery(resolvedVariables, queryParameters),
...getAuthorizationsQuery(authorizations),
}
const resolvedHeaders = getHeaders(
headers,
resolvedVariables,
headerParameters,
authorizations,
)
const resolvedCookies = {
...(cookies || {}),
...getCookies(resolvedVariables, cookieParameters),
...getAuthorizationsCookies(authorizations),
}
baseUrl = baseUrl ? resolveBaseUrl(baseUrl) : DEFAULT_BASE_URL
const urlInstance = url ? new URL(url) : (baseUrl ? new URL(`${baseUrl}${resolvedPath}`) : new URL(resolvedPath, 'http://localhost'))
if (contentType && !(body instanceof FormData)) {
resolvedHeaders['content-type'] = contentType
} else if (body && !resolvedHeaders['content-type'] && !(body instanceof FormData)) {
resolvedHeaders['content-type'] = 'application/json'
}
return new OARequest({
path: resolvedPath,
url: urlInstance,
method: resolveMethod,
parameters,
authorizations,
body: [
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS',
'TRACE',
].includes(resolveMethod) && body
? body
: undefined,
variables: resolvedVariables,
headers: resolvedHeaders,
query: resolvedQuery,
cookies: resolvedCookies,
contentType: resolvedHeaders['content-type'],
})
}