betterthrow
Version:
Define the errors your program can generate, create beautiful unions.
497 lines (496 loc) • 14.2 kB
JavaScript
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
};
import { ResultLikeSymbol } from "retuple-symbols";
export function betterthrow(kind) {
return new ClassBuilderPrefix({
kind: kind || null,
prefix: "",
data: undefined,
errors: [],
messages: undefined,
json: true,
props: undefined,
});
}
export function group(prefix) {
return new GroupBuilderContext({
prefix: prefix || "",
errors: [],
data: {},
});
}
export function error(code, message) {
return new ErrorBuilderContext({
code,
message: typeof message === "string" && message !== "" ? message : undefined,
chain: undefined,
});
}
export const RESERVED_WORDS = Object.freeze([
"__proto__",
"prototype",
"constructor",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
"name",
"message",
"cause",
"stack",
"toJSON",
"toPlainObject",
"kind",
"code",
]);
const INTERNAL = Symbol("betterthrow/internal");
const INFER = Symbol("betterthrow/infer");
const DEFAULT_MESSAGE = "No error message";
class BetterThrowError extends Error {
constructor(message, cause) {
super(message, cause === undefined ? undefined : { cause });
}
[ResultLikeSymbol]() {
return { ok: false, value: this };
}
toPlainObject() {
const { name, message, cause, stack, kind, code, ...ctx } = this;
return { kind, code, message, ...ctx };
}
}
BetterThrowError.prototype.toJSON = function toJSON() {
return {};
};
/**
* @TODO
*/
class ClassBuilderBuild {
constructor(settings) {
this.settings = settings;
}
/**
* ## Build
*
* @TODO
*/
build({ classNames = true } = {}) {
var _a, _b;
const { kind, prefix, errors, data, props, json, messages } = this.settings;
const baseClassName = kind
? getClassName(toPascalCase(kind))
: "BetterThrowCustomError";
const BetterThrowCustomError = (_a = class extends BetterThrowError {
constructor(input, context) {
if (input !== INTERNAL) {
const TargetClass = classes[input.code];
if (!TargetClass) {
throw new Error("BetterThrow: Invalid error construction, no error with code " +
`${String(input.code)} exists on class '${kind}'`);
}
return new TargetClass(input);
}
const { message, cause, ...ctx } = context;
super(context.message, cause);
Object.assign(this, ctx);
}
},
__setFunctionName(_a, "BetterThrowCustomError"),
_a.kind = kind,
(() => {
if (classNames) {
Object.defineProperty(_a, "name", {
value: baseClassName,
});
}
})(),
_a);
if (json === true) {
BetterThrowCustomError.prototype.toJSON = toJSON;
}
else if (typeof json === "function") {
BetterThrowCustomError.prototype.toJSON = function toJSON() {
return json(this);
};
}
const codes = new Set();
const classes = {};
const baseClassKey = baseClassName.toLowerCase().endsWith("error")
? baseClassName.slice(0, -5)
: baseClassName;
const filter = createContextFilter(props);
for (const error of errors.flatMap(getErrorSettings)) {
const code = prefix ? `${prefix}_${error.code}` : error.code;
if (codes.has(code)) {
throw new Error(`BetterThrow: Duplicate error code '${code}' on class ` +
`'${kind}', error codes must be unique`);
}
codes.add(code);
const subclassKey = toPascalCase(error.code);
if (BetterThrowCustomError[subclassKey]) {
throw new Error(`BetterThrow: Duplicate class name '${subclassKey}' on class ` +
`${kind}, class names must be unique`);
}
const resolve = createContextResolver(kind, code, data, messages, filter, error.chain, error.message);
const BetterThrowCustomCodedError = (_b = class extends BetterThrowCustomError {
constructor(ctx) {
super(INTERNAL, resolve(ctx));
}
},
__setFunctionName(_b, "BetterThrowCustomCodedError"),
(() => {
if (classNames) {
Object.defineProperty(_b, "name", {
value: getClassName(`${baseClassKey}${subclassKey}`),
});
}
})(),
_b);
classes[code] = BetterThrowCustomCodedError;
BetterThrowCustomError[subclassKey] =
BetterThrowCustomCodedError;
}
BetterThrowCustomError.codes = Object.freeze([...codes]);
return BetterThrowCustomError;
}
}
/**
* @TODO
*/
class ClassBuilderTypes extends ClassBuilderBuild {
types() {
return new ClassBuilderBuild(this.settings);
}
}
class ClassBuilderFilter extends ClassBuilderTypes {
/**
* ## Filter
*
* @TODO
*/
filter(filter) {
return new ClassBuilderTypes({
...this.settings,
props: Object.keys(filter).filter(notReservedWord),
});
}
}
/**
* @TODO
*/
class ClassBuilderJson extends ClassBuilderFilter {
json(json) {
return new ClassBuilderFilter({ ...this.settings, json });
}
}
/**
* @TODO
*/
class ClassBuilderMessages extends ClassBuilderJson {
messages(messages) {
return new ClassBuilderJson({
...this.settings,
messages: this.getMessagesSetting(messages),
});
}
getMessagesSetting(setting) {
switch (typeof setting) {
case "function":
return { type: "function", message: setting };
case "string":
return { type: "string", message: setting };
case "object":
if (!setting || Array.isArray(setting)) {
return;
}
const message = Object.fromEntries(Object.entries(setting).filter(([, option]) => typeof option === "function" || typeof option === "string"));
return { message, type: "object" };
}
}
}
/**
* @TODO
*/
class ClassBuilderErrors {
constructor(settings) {
this.settings = settings;
}
/**
* ## Errors
*
* @TODO
*/
errors(...errors) {
return new ClassBuilderMessages({ ...this.settings, errors });
}
}
/**
* @TODO
*/
class ClassBuilderData extends ClassBuilderErrors {
/**
* ## Data
*
* @TODO
*/
data(data) {
return new ClassBuilderErrors({ ...this.settings, data });
}
}
/**
* @TODO
*/
class ClassBuilderContext extends ClassBuilderData {
/**
* ## Context
*
* @TODO
*/
context() {
return new ClassBuilderData(this.settings);
}
}
/**
* @TODO
*/
class ClassBuilderPrefix extends ClassBuilderContext {
/**
* ## Prefix
*
* @TODO
*/
prefix(prefix) {
return new ClassBuilderContext({ ...this.settings, prefix });
}
}
/**
* ## Group Definition
*
* @TODO
*/
class GroupDefinition {
constructor(settings) {
this.settings = settings;
}
[INTERNAL]() {
const { prefix, errors, data } = this.settings;
return mergeErrorSettings(prefix, data, errors.flatMap(getErrorSettings));
}
}
/**
* @TODO
*/
class GroupBuilderErrors {
constructor(settings) {
this.settings = settings;
}
/**
* ## Errors
*
* @TODO
*/
errors(...errors) {
return new GroupDefinition({ ...this.settings, errors });
}
}
/**
* @TODO
*/
class GroupBuilderData extends GroupBuilderErrors {
/**
* ## Data
*
* @TODO
*/
data(data) {
return new GroupBuilderErrors({ ...this.settings, data });
}
}
class GroupBuilderContext extends GroupBuilderData {
/**
* ## Context
*
* @TODO
*/
context() {
return new GroupBuilderData(this.settings);
}
}
/**
* ## Error Definition
*
* @TODO
*/
class ErrorDefinition {
constructor(settings) {
this.settings = settings;
}
[INTERNAL]() {
return [this.settings];
}
}
class ErrorBuilderData extends ErrorDefinition {
/**
* ## Data
*
* @TODO
*/
data(data) {
return new ErrorDefinition({ ...this.settings, chain: [data] });
}
}
class ErrorBuilderContext extends ErrorBuilderData {
/**
* ## Context
*
* @TODO
*/
context() {
return new ErrorBuilderData(this.settings);
}
}
function createContextResolver(kind, code, data, messages, filter, chain, message) {
const assignData = createDataAssigner(mergeDataChain(data, chain));
const getMessage = createMessageResolver(code, message, messages);
return (input) => {
let ctx;
let message;
let cause;
if (input) {
ctx = filter(input);
cause = input.cause;
message = input.message;
}
ctx = assignData(ctx);
ctx.kind = kind;
ctx.code = code;
ctx.message = getMessage(ctx, message);
ctx.cause = cause;
return ctx;
};
}
function createContextFilter(props) {
if (!props) {
return omitReservedProps;
}
return (ctx) => {
const output = {};
for (const key of props) {
if (ctx[key] !== undefined) {
output[key] = ctx[key];
}
}
return output;
};
}
function createDataAssigner(chain) {
if (!chain) {
return (ctx) => ctx || {};
}
if (chain.some((part) => typeof part === "function")) {
return (ctx) => ctx === undefined
? resolveDataChain(chain)
: assignDataToContext(ctx, resolveDataChain(chain));
}
const data = resolveDataChain(chain);
const keys = Object.keys(data);
return (ctx) => (ctx ? assignDataToContext(ctx, data, keys) : { ...data });
}
function createMessageResolver(code, errorMessage, setting) {
if (isNonEmptyString(errorMessage)) {
return (_, input) => (isNonEmptyString(input) ? input : errorMessage);
}
if (!setting) {
return (_, input) => (isNonEmptyString(input) ? input : DEFAULT_MESSAGE);
}
const { type, message } = setting;
switch (type) {
case "string":
const resolved = message || DEFAULT_MESSAGE;
return (_, input) => (isNonEmptyString(input) ? input : resolved);
case "function":
return (ctx, input) => {
if (isNonEmptyString(input)) {
return input;
}
const resolved = message(ctx);
if (isNonEmptyString(resolved)) {
return resolved;
}
return DEFAULT_MESSAGE;
};
case "object":
const option = message[code];
switch (typeof option) {
case "string":
return createMessageResolver(code, option, undefined);
case "function":
return createMessageResolver(code, undefined, {
type: "function",
message: option,
});
default:
return createMessageResolver(code, undefined, undefined);
}
}
}
function assignDataToContext(ctx, data, keys = Object.keys(data)) {
for (const key of keys) {
if (ctx[key] === undefined) {
ctx[key] = data[key];
}
}
return ctx;
}
function resolveDataChain(chain) {
return omitReservedProps(Object.assign({}, ...chain.map(resolveDataOption)));
}
function resolveDataOption(data) {
return typeof data === "function" ? data() : data;
}
function mergeErrorSettings(prefix, data, settings) {
if (!prefix && !data) {
return settings;
}
return settings.map(({ code, message, chain }) => ({
message,
code: prefix ? `${prefix}_${code}` : code,
chain: mergeDataChain(data, chain),
}));
}
function mergeDataChain(data, chain) {
if (data && chain) {
if (typeof data === "object" && typeof chain[0] === "object") {
return [{ ...data, ...chain[0] }, ...chain.slice(1)];
}
return [data, ...chain];
}
return data ? [data] : chain;
}
function isNonEmptyString(value) {
return typeof value === "string" && value !== "";
}
function omitReservedProps({ __proto__, prototype, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toLocaleString, toString, valueOf, name, message, cause, stack, toJSON, toPlainObject, code, ...context }) {
return context;
}
function getClassName(name) {
return name.toLowerCase().endsWith("error") ? name : `${name}Error`;
}
function getErrorSettings(errors) {
return errors[INTERNAL]();
}
function notReservedWord(value) {
return !RESERVED_WORDS.includes(value);
}
function toJSON() {
return this.toPlainObject();
}
function toPascalCase(code) {
return code.toLowerCase().split("_").map(toCapitalized).join("");
}
function toCapitalized(word) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}