UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

116 lines (107 loc) 3.53 kB
import type { ContractErrorDefinition, ContractErrorResponses, ContractMeta, ContractResponses, } from "./types.js"; import { STANDARD_ERROR_RESPONSE_SCHEMA } from "./types.js"; function isContractErrorDefinition( value: unknown, ): value is ContractErrorDefinition { return ( typeof value === "object" && value !== null && typeof (value as { code?: unknown }).code === "string" && typeof (value as { status?: unknown }).status === "number" && typeof (value as { message?: unknown }).message === "string" ); } /** * Extract catalog error definitions declared in contract metadata. */ export function catalogErrorsFromMeta( metadata: ContractMeta, ): ContractErrorResponses { const errors = metadata.errors; if (typeof errors !== "object" || errors === null) return {}; const catalog: ContractErrorResponses = {}; for (const [key, definition] of Object.entries(errors)) { if (isContractErrorDefinition(definition)) { catalog[key] = definition; } } return catalog; } /** * Convert catalog error definitions into standard envelope response schemas. */ export function responsesFromErrors( errors: ContractErrorResponses, ): ContractResponses { const responses: ContractResponses = {}; for (const error of Object.values(errors)) { responses[error.status] = STANDARD_ERROR_RESPONSE_SCHEMA; } return responses; } /** * Merge catalog errors already declared in metadata with new declarations. * * Later declarations win when the same catalog key is declared twice. */ export function mergeCatalogErrors( metadata: ContractMeta, errorDefs: ContractErrorResponses, ): ContractErrorResponses { return { ...catalogErrorsFromMeta(metadata), ...errorDefs }; } function catalogErrorStatuses(metadata: ContractMeta): Set<string> { return new Set( Object.values(catalogErrorsFromMeta(metadata)).map((error) => String(error.status), ), ); } /** * Reject `.errors()` statuses that collide with custom response schemas. * * Statuses declared by earlier `.errors()` calls are allowed: catalog errors * share the standard error envelope, so multiple codes can map to one status. */ export function assertErrorsAvoidCustomResponseStatuses( metadata: ContractMeta, responses: ContractResponses, nextErrorResponses: ContractResponses, ): void { const existingErrorStatuses = catalogErrorStatuses(metadata); const customStatuses = new Set( Object.keys(responses).filter( (status) => !existingErrorStatuses.has(status), ), ); const conflicts = Object.keys(nextErrorResponses).filter((status) => customStatuses.has(status), ); if (conflicts.length) { throw new Error( `.errors() cannot declare status ${conflicts.join(", ")} because a response schema for that status already exists. Use .responses() without .errors() when you need a custom error response body.`, ); } } /** * Reject `.responses()` statuses already declared by catalog errors. */ export function assertResponsesAvoidCatalogErrorStatuses( metadata: ContractMeta, next: ContractResponses, ): void { const errorStatuses = catalogErrorStatuses(metadata); const conflicts = Object.keys(next).filter((status) => errorStatuses.has(status), ); if (conflicts.length) { throw new Error( `.responses() cannot declare status ${conflicts.join(", ")} because that status is already declared by .errors(). Use .responses() without .errors() when you need a custom error response body.`, ); } }