/** * @since 1.0.0 */ import * as Effect from 'effect/Effect' import type * as OpenApiParser from './OpenApiParser.js' import type * as SecurityParser from './SecurityParser.js' /** * @since 1.0.0 * @category Models */ export interface ParsedOperation { readonly operationId: string readonly method: 'get' | 'post' | 'put' | 'patch' | 'delete' readonly path: string readonly summary?: string | undefined readonly description?: string | undefined readonly deprecated?: boolean readonly tags: ReadonlyArray readonly pathParameters: ReadonlyArray readonly queryParameters: ReadonlyArray readonly headerParameters: ReadonlyArray readonly cookieParameters: ReadonlyArray readonly requestBody?: | { readonly schema: OpenApiParser.SchemaObject readonly required: boolean } | undefined readonly responses: ReadonlyArray<{ readonly statusCode: string readonly schema: OpenApiParser.SchemaObject }> readonly security?: ReadonlyArray } /** * Check if a parameter is a $ref */ const isParameterRef = ( param: OpenApiParser.ParameterObject | { readonly $ref: string } ): param is { readonly $ref: string } => '$ref' in param && typeof param.$ref === 'string' /** * Resolve a parameter $ref to its actual ParameterObject */ const resolveParameterRef = ( ref: string, components: OpenApiParser.ComponentsObject | undefined ): Effect.Effect => Effect.gen(function* () { // Parse the $ref - expected format: #/components/parameters/ParamName const match = ref.match(/^#\/components\/parameters\/(.+)$/) if (!match) { yield* Effect.logWarning(`Invalid parameter $ref format: ${ref}`) return undefined } const paramName = match[1] const resolved = components?.parameters?.[paramName] if (!resolved) { yield* Effect.logWarning(`Unable to resolve parameter $ref: ${ref}`) return undefined } return resolved }) /** * Resolve all parameters, handling both inline and $ref parameters */ const resolveParameters = ( rawParameters: ReadonlyArray, components: OpenApiParser.ComponentsObject | undefined ): Effect.Effect> => Effect.gen(function* () { const resolved: Array = [] for (const param of rawParameters) { if (isParameterRef(param)) { const resolvedParam = yield* resolveParameterRef(param.$ref, components) if (resolvedParam) { resolved.push(resolvedParam) } } else { resolved.push(param) } } return resolved }) /** * Extract all operations from an OpenAPI specification * * @since 1.0.0 * @category Parsing */ export const extractOperations = (spec: OpenApiParser.OpenApiSpec): Effect.Effect> => Effect.gen(function* () { const operations: Array = [] for (const [path, pathItem] of Object.entries(spec.paths)) { const methods: Array<'get' | 'post' | 'put' | 'patch' | 'delete'> = ['get', 'post', 'put', 'patch', 'delete'] for (const method of methods) { const operation = pathItem[method] if (!operation) continue const rawParameters = operation.parameters || [] // Resolve all parameters (both inline and $ref) const parameters = yield* resolveParameters(rawParameters, spec.components) // Separate parameters by type const pathParameters = parameters.filter((p) => p.in === 'path') const queryParameters = parameters.filter((p) => p.in === 'query') const headerParameters = parameters.filter((p) => p.in === 'header') const cookieParameters = parameters.filter((p) => p.in === 'cookie') // Extract request body let requestBody: ParsedOperation['requestBody'] | undefined if (operation.requestBody?.content) { const contentTypes = Object.keys(operation.requestBody.content) const nonJsonTypes = contentTypes.filter((ct) => ct !== 'application/json') if (nonJsonTypes.length > 0) { yield* Effect.logWarning( `Operation '${operation.operationId}' (${method.toUpperCase()} ${path}) has non-JSON content types that will be ignored: ${nonJsonTypes.join(', ')}` ) } const content = operation.requestBody.content['application/json'] if (content?.schema) { requestBody = { schema: content.schema, required: operation.requestBody.required ?? false, } } } // Extract all responses with schemas const responses: Array<{ statusCode: string; schema: OpenApiParser.SchemaObject }> = [] for (const [statusCode, response] of Object.entries(operation.responses)) { if (response.content) { const contentTypes = Object.keys(response.content) const nonJsonTypes = contentTypes.filter((ct) => ct !== 'application/json') if (nonJsonTypes.length > 0) { yield* Effect.logWarning( `Operation '${operation.operationId}' (${method.toUpperCase()} ${path}) response ${statusCode} has non-JSON content types that will be ignored: ${nonJsonTypes.join(', ')}` ) } } const content = response.content?.['application/json'] if (content?.schema) { responses.push({ statusCode, schema: content.schema, }) } } // Extract security requirements (if specified at operation level) const security = operation.security operations.push({ operationId: operation.operationId, method, path, ...(operation.summary ? { summary: operation.summary } : {}), ...(operation.description ? { description: operation.description } : {}), ...(operation.deprecated ? { deprecated: operation.deprecated } : {}), tags: operation.tags || [], pathParameters, queryParameters, headerParameters, cookieParameters, requestBody, responses, ...(security ? { security } : {}), }) } } return operations })