UNPKG

@furystack/rest

Version:
164 lines (145 loc) 5.75 kB
import type { ApiDocumentMetadata, ApiEndpointSchema } from './api-endpoint-schema.js' import type { Method } from './methods.js' import type { OpenApiDocument, Operation, ReferenceObject, SecuritySchemeObject } from './openapi-document.js' const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace'] as const const isReferenceObject = (obj: unknown): obj is ReferenceObject => typeof obj === 'object' && obj !== null && '$ref' in obj /** * Converts an OpenAPI `{param}` path format to FuryStack `:param` format. * * @param path - The OpenAPI path (e.g. `/users/{id}`) * @returns The FuryStack path (e.g. `/users/:id`) */ export const convertOpenApiPathToFuryStack = (path: string): string => path.replace(/\{([^{}]+)\}/g, ':$1') const extractResponseSchema = (operation: Operation): unknown => { for (const statusCode of ['200', '201', '2XX', 'default']) { const response = operation.responses?.[statusCode] if (!response || isReferenceObject(response)) continue const jsonContent = response.content?.['application/json'] if (jsonContent?.schema) return jsonContent.schema } return undefined } const extractSchemaName = (operation: Operation, method: string, path: string): string => { if (operation.operationId) return operation.operationId const cleanPath = path .replace(/\{([^{}]+)\}/g, '$1') .replace(/\//g, '_') .replace(/^_/, '') return `${method}_${cleanPath}` } const isOperationAuthenticated = (operation: Operation, docSecurity?: OpenApiDocument['security']): boolean => { if (operation.security !== undefined) { return operation.security.length > 0 } if (docSecurity !== undefined) { return docSecurity.length > 0 } return false } const extractSecuritySchemeNames = ( operation: Operation, docSecurity?: OpenApiDocument['security'], ): string[] | undefined => { const security = operation.security !== undefined ? operation.security : docSecurity if (!security || security.length === 0) return undefined const names = security.flatMap((req) => Object.keys(req)) return names.length > 0 ? names : undefined } const extractDocumentMetadata = (doc: OpenApiDocument): ApiDocumentMetadata | undefined => { const metadata: ApiDocumentMetadata = {} let hasMetadata = false if (doc.info.summary) { metadata.summary = doc.info.summary hasMetadata = true } if (doc.info.termsOfService) { metadata.termsOfService = doc.info.termsOfService hasMetadata = true } if (doc.info.contact) { metadata.contact = doc.info.contact hasMetadata = true } if (doc.info.license) { metadata.license = doc.info.license hasMetadata = true } if (doc.servers?.length) { metadata.servers = doc.servers hasMetadata = true } if (doc.tags?.length) { metadata.tags = doc.tags hasMetadata = true } if (doc.externalDocs) { metadata.externalDocs = doc.externalDocs hasMetadata = true } const schemes = doc.components?.securitySchemes if (schemes) { const resolved: Record<string, SecuritySchemeObject> = {} for (const [name, scheme] of Object.entries(schemes)) { if (!isReferenceObject(scheme)) { resolved[name] = scheme } } if (Object.keys(resolved).length > 0) { metadata.securitySchemes = resolved hasMetadata = true } } return hasMetadata ? metadata : undefined } /** * Converts an OpenAPI 3.x document to a FuryStack `ApiEndpointSchema`. * * This enables consuming external OpenAPI documents with FuryStack's runtime pipeline. * Preserves operation-level metadata (tags, deprecated, summary, description) and * document-level metadata (servers, tags, contact, license, securitySchemes). * * **Important:** If the document contains `$ref` pointers, call `resolveOpenApiRefs(doc)` * first to inline them. This function does not resolve `$ref` on its own. * * @param doc - The OpenAPI document to convert * @returns An ApiEndpointSchema that can be used with FuryStack's API tools */ export const openApiToSchema = (doc: OpenApiDocument): ApiEndpointSchema => { const endpoints: ApiEndpointSchema['endpoints'] = {} if (doc.paths) { for (const [openApiPath, pathItemOrRef] of Object.entries(doc.paths)) { if (isReferenceObject(pathItemOrRef)) continue const pathItem = pathItemOrRef const furyStackPath = convertOpenApiPathToFuryStack(openApiPath) for (const method of HTTP_METHODS) { const operation = pathItem[method] if (!operation) continue const upperMethod = method.toUpperCase() as Method const methodEndpoints = endpoints[upperMethod] ?? {} endpoints[upperMethod] = methodEndpoints const responseSchema = extractResponseSchema(operation) const schemaName = extractSchemaName(operation, method, openApiPath) const securitySchemeNames = extractSecuritySchemeNames(operation, doc.security) methodEndpoints[furyStackPath] = { path: furyStackPath, schema: responseSchema ?? {}, schemaName, isAuthenticated: isOperationAuthenticated(operation, doc.security), ...(securitySchemeNames ? { securitySchemes: securitySchemeNames } : {}), ...(operation.tags?.length ? { tags: operation.tags } : {}), ...(operation.deprecated ? { deprecated: true } : {}), ...(operation.summary ? { summary: operation.summary } : {}), ...(operation.description ? { description: operation.description } : {}), } } } } return { name: doc.info.title, description: doc.info.description ?? '', version: doc.info.version, metadata: extractDocumentMetadata(doc), endpoints, } }