@beignet/core
Version:
Core framework primitives for Beignet
116 lines (107 loc) • 3.53 kB
text/typescript
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.`,
);
}
}