UNPKG

betterthrow

Version:

Define the errors your program can generate, create beautiful unions.

503 lines (502 loc) 14.3 kB
"use strict"; 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 }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RESERVED_WORDS = void 0; exports.betterthrow = betterthrow; exports.group = group; exports.error = error; const retuple_symbols_1 = require("retuple-symbols"); function betterthrow(kind) { return new ClassBuilderPrefix({ kind: kind || null, prefix: "", data: undefined, errors: [], messages: undefined, json: true, props: undefined, }); } function group(prefix) { return new GroupBuilderContext({ prefix: prefix || "", errors: [], data: {}, }); } function error(code, message) { return new ErrorBuilderContext({ code, message: typeof message === "string" && message !== "" ? message : undefined, chain: undefined, }); } exports.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 }); } [retuple_symbols_1.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 !exports.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(); }