@lokalise/api-contracts
Version:
136 lines • 6.03 kB
JavaScript
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