starlight-openapi
Version:
Starlight plugin to generate documentation from OpenAPI/Swagger specifications.
185 lines (147 loc) • 5.41 kB
text/typescript
import type { OpenAPI } from 'openapi-types'
import { type Document, isOpenAPIV2Document } from './document'
import { slug } from './path'
import { isPathItem, type PathItem } from './pathItem'
import type { Schema } from './schema'
const defaultOperationTag = 'Operations'
const operationHttpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const
export function getOperationsByTag({ config, document }: Schema) {
const operationsByTag = new Map<string, { entries: PathItemOperation[]; tag: OperationTag }>()
for (const [pathItemPath, pathItem] of Object.entries(document.paths ?? {})) {
if (!isPathItem(pathItem)) {
continue
}
const allOperationIds = operationHttpMethods.map((method) => {
return isPathItemOperation(pathItem, method) ? (pathItem[method].operationId ?? pathItemPath) : undefined
})
for (const [index, method] of operationHttpMethods.entries()) {
const operationId = allOperationIds[index]
if (!operationId || !isPathItemOperation(pathItem, method)) {
continue
}
const operation = pathItem[method]
const isDuplicateOperationId = allOperationIds.filter((id) => id === operationId).length > 1
const operationIdSlug = slug(operationId)
for (const tag of operation.tags ?? [defaultOperationTag]) {
const operations = operationsByTag.get(tag) ?? { entries: [], tag: { name: tag } }
const title =
operation.summary ?? (isDuplicateOperationId ? `${operationId} (${method.toUpperCase()})` : operationId)
operations.entries.push({
method,
operation,
path: pathItemPath,
pathItem,
sidebar: {
label: config.sidebar.operations.labels === 'summary' && operation.summary ? title : operationId,
},
slug: isDuplicateOperationId
? `operations/${operationIdSlug}/${slug(method)}`
: `operations/${operationIdSlug}`,
title,
})
operationsByTag.set(tag, operations)
}
}
}
if (document.tags) {
const orderedTags = new Map(document.tags.map((tag, index) => [tag.name, { index, tag }]))
const operationsByTagArray = [...operationsByTag.entries()].sort(([tagA], [tagB]) => {
const orderA = orderedTags.get(tagA)?.index ?? Number.POSITIVE_INFINITY
const orderB = orderedTags.get(tagB)?.index ?? Number.POSITIVE_INFINITY
return orderA - orderB
})
operationsByTag.clear()
for (const [tag, operations] of operationsByTagArray) {
operationsByTag.set(tag, { ...operations, tag: orderedTags.get(tag)?.tag ?? operations.tag })
}
}
return operationsByTag
}
export function getWebhooksOperations({ config, document }: Schema): PathItemOperation[] {
if (!('webhooks' in document)) {
return []
}
const operations: PathItemOperation[] = []
for (const [webhookKey, pathItem] of Object.entries(document.webhooks)) {
if (!isPathItem(pathItem)) {
continue
}
for (const method of operationHttpMethods) {
if (!isPathItemOperation(pathItem, method)) {
continue
}
const operation = pathItem[method]
const operationId = operation.operationId ?? webhookKey
const title = operation.summary ?? operationId
operations.push({
method,
operation,
pathItem,
sidebar: {
label: config.sidebar.operations.labels === 'summary' && operation.summary ? title : operationId,
},
slug: `webhooks/${slug(operationId)}`,
title,
})
}
}
return operations
}
export function isPathItemOperation<TMethod extends OperationHttpMethod>(
pathItem: PathItem,
method: TMethod,
): pathItem is Record<TMethod, Operation> {
return method in pathItem
}
export function isMinimalOperationTag(tag: OperationTag): boolean {
return (tag.description === undefined || tag.description.length === 0) && tag.externalDocs === undefined
}
export function getOperationURLs(document: Document, { operation, path, pathItem }: PathItemOperation): OperationURL[] {
const urls: OperationURL[] = []
if (isOpenAPIV2Document(document) && 'host' in document) {
let url = document.host
url += document.basePath ?? ''
url += path ?? ''
if (url.length > 0) {
urls.push(makeOperationURL(url))
}
} else {
const servers =
'servers' in operation
? operation.servers
: 'servers' in pathItem
? pathItem.servers
: 'servers' in document
? document.servers
: []
for (const server of servers) {
let url = server.url
url += path ?? ''
if (url.length > 0) {
urls.push(makeOperationURL(url, server.description))
}
}
}
return urls
}
function makeOperationURL(url: string, description?: string): OperationURL {
return { description, url: url.replace(/^\/\//, '') }
}
export interface PathItemOperation {
method: OperationHttpMethod
operation: Operation
path?: string
pathItem: PathItem
sidebar: {
label: string
}
slug: string
title: string
}
export type Operation = OpenAPI.Operation
export type OperationHttpMethod = (typeof operationHttpMethods)[number]
export type OperationTag = NonNullable<Document['tags']>[number]
interface OperationURL {
description?: string | undefined
url: string
}