UNPKG

@lokalise/api-contracts

Version:
136 lines 6.03 kB
import { ContractNoBody } from "./constants.js"; export const textResponse = (contentType, options) => ({ _tag: 'TextResponse', contentType, ...(options?.description !== undefined && { description: options.description }), }); export const isTextResponse = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'TextResponse'; export const blobResponse = (contentType, options) => ({ _tag: 'BlobResponse', contentType, ...(options?.description !== undefined && { description: options.description }), }); export const isBlobResponse = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'BlobResponse'; export const sseResponse = (schemaByEventName, options) => ({ _tag: 'SseResponse', schemaByEventName, ...(options?.description !== undefined && { description: options.description }), }); export const isSseResponse = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'SseResponse'; export const isJsonResponse = (value) => typeof value === 'object' && value !== null && !('_tag' in value); export const anyOfResponses = (responses, options) => ({ _tag: 'AnyOfResponses', responses, ...(options?.description !== undefined && { description: options.description }), }); export const isAnyOfResponses = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'AnyOfResponses'; export const noBodyResponse = (options) => ({ _tag: 'NoBodyResponse', ...(options?.description !== undefined && { description: options.description }), }); export const isNoBodyResponse = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'NoBodyResponse'; const matchTypedResponse = (entry, contentType) => { if (isTextResponse(entry)) { return contentType.includes(entry.contentType) ? { kind: 'text' } : null; } if (isBlobResponse(entry)) { return contentType.includes(entry.contentType) ? { kind: 'blob' } : null; } if (isSseResponse(entry)) { return contentType.includes('text/event-stream') ? { kind: 'sse', schemaByEventName: entry.schemaByEventName } : null; } if (contentType.includes('application/json')) { return { kind: 'json', schema: entry }; } return null; }; const resolveByKind = (entry) => { if (isTextResponse(entry)) { return { kind: 'text' }; } if (isBlobResponse(entry)) { return { kind: 'blob' }; } if (isSseResponse(entry)) { return { kind: 'sse', schemaByEventName: entry.schemaByEventName }; } return { kind: 'json', schema: entry }; }; /** * Resolves a contract's response entry for a given status code into a concrete `ResponseKind`, * taking the response `content-type` into account. * * Returns `null` when the content-type cannot be matched to any entry in the contract, * indicating the response is unexpected and should be treated as an error by the caller. * * @param schemaEntry - The contract entry for the matched status code (`ContractNoBody`, * a Zod schema, `textResponse`, `blobResponse`, `sseResponse`, or `anyOfResponses`). * @param contentType - The `content-type` header value from the actual HTTP response, * or `undefined` when the header is absent. * @param strict - When `true` (default), returns `null` if the `content-type` is absent or does * not match the contract entry. When `false`, falls back to the entry's declared kind instead of * returning `null` — only applies to single-entry responses; `anyOfResponses` always requires a * content-type to disambiguate regardless of this flag. */ export const resolveContractResponse = (schemaEntry, contentType, strict = true) => { if (schemaEntry === ContractNoBody || isNoBodyResponse(schemaEntry)) { return { kind: 'noContent' }; } if (isAnyOfResponses(schemaEntry)) { // AnyOfResponses always requires content-type to disambiguate — strict mode has no effect here if (!contentType) { return null; } for (const item of schemaEntry.responses) { const resolved = matchTypedResponse(item, contentType); if (resolved) { return resolved; } } return null; } if (!contentType) { return strict ? null : resolveByKind(schemaEntry); } const matched = matchTypedResponse(schemaEntry, contentType); return matched ?? (strict ? null : resolveByKind(schemaEntry)); }; function getRangeKey(statusCode) { if (statusCode >= 100 && statusCode < 200) return '1xx'; if (statusCode >= 200 && statusCode < 300) return '2xx'; if (statusCode >= 300 && statusCode < 400) return '3xx'; if (statusCode >= 400 && statusCode < 500) return '4xx'; if (statusCode >= 500 && statusCode < 600) return '5xx'; return null; } /** * Combines status-code lookup and content-type resolution into a single call. * Lookup precedence: exact code → range key (e.g. `'4xx'`) → `'default'`. * Returns `null` when no entry matches or the content-type cannot be matched. */ export function resolveResponseEntry(responsesByStatusCode, statusCode, contentType, strictContentType) { const exactEntry = responsesByStatusCode[statusCode]; if (exactEntry) { return resolveContractResponse(exactEntry, contentType, strictContentType); } const rangeKey = getRangeKey(statusCode); if (rangeKey) { const rangeEntry = responsesByStatusCode[rangeKey]; if (rangeEntry) { return resolveContractResponse(rangeEntry, contentType, strictContentType); } } const defaultEntry = responsesByStatusCode['default']; if (defaultEntry) { return resolveContractResponse(defaultEntry, contentType, strictContentType); } return null; } //# sourceMappingURL=contractResponse.js.map