@small-tech/auto-encrypt
Version:
Automatically provisions and renews Let’s Encrypt TLS certificates on Node.js https servers (including Kitten, Polka, Express.js, etc.)
118 lines (97 loc) • 4.19 kB
JavaScript
////////////////////////////////////////////////////////////////////////////////
//
// Throws
//
// Extensible basic error creation and throwing methods that use symbols
// and predefined yet configurable lists of errors to make working with errors
// safer (fewer magic strings) and DRYer (Don’t Repeat Yourself).
//
// Copyright © 2020 Aral Balkan, Small Technology Foundation.
// License: AGPLv3 or later.
//
////////////////////////////////////////////////////////////////////////////////
import fs from 'fs'
class SymbolicError extends Error {
symbol = null
}
// Add the ability to supply a custom hint to global errors. Hints are displayed
// at the end of the error message, in parentheses.
const hinted = (str, hint) => hint ? `${str} (${hint}).` : `${str}.`
export default class Throws {
// Define common global errors. These are mixed into a local ERRORS object
// if it exists on the object the mixin() method is called with a reference to.
static ERRORS = {
[Symbol.for('UndefinedOrNullError')]:
(object, hint) => hinted(`${object} must not be undefined or null`, hint),
[Symbol.for('UndefinedError')]:
(object, hint) => hinted(`${object} must not be undefined`, hint),
[Symbol.for('NullError')]:
(object, hint) => hinted(`${object} must not be null`, hint),
[Symbol.for('ArgumentError')]:
(object, hint) => hinted(`incorrect value for argument ${object}`, hint),
[Symbol.for('MustBeInstantiatedViaAsyncFactoryMethodError')]:
(object, hint) => hinted(`Use the static getInstanceAsync() method to initialise ${object}`),
[Symbol.for('SingletonConstructorIsPrivateError')]:
(object, hint) =>
hinted(`{object} is a singleton; its constructor is private. Do not instantiate using new ${object}()`),
[Symbol.for('StaticClassCannotBeInstantiatedError')]:
(object, hint) => hinted(`${object} is a static class. You cannot instantiate it using new ${object}()`),
[Symbol.for('ReadOnlyAccessorError')]:
(object, hint) => hinted(`{object} is a read-only property`)
}
constructor (customErrors = {}) {
this.errors = Object.assign(customErrors, Throws.ERRORS)
}
messageFor (symbol, ...args) {
return this.errors[symbol](...args)
}
createError (symbol, ...args) {
const errorMessage = this.messageFor(symbol, ...args)
const error = new SymbolicError(errorMessage)
Error.captureStackTrace(error, this.error)
error.name = symbol.description
error.symbol = symbol
return error
}
if (condition, errorSymbol, ...args) {
if (condition) {
this.error(errorSymbol, ...args)
}
}
ifMissing (parameterName) {
// Attempt to automatically get the argument name even if parameterName was not
// manually specified in the code.
//
// Thanks to Christian Bundy (https://social.coop/@christianbundy/) for the
// technique (https://gist.github.com/christianbundy/4c3c29b9f1a52384d7e8a51a956227c2)
// TODO: Generalise and use in the other parameter errors if it holds up to testing.
const copy = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
const stack = new Error().stack;
Error.prepareStackTrace = copy;
const fileName = stack[1].getFileName().replace('file://', '');
const lineNumber = stack[1].getLineNumber() - 1;
const columnNumber = stack[1].getColumnNumber() - 1;
const line = fs.readFileSync(fileName, "utf8").split("\n")[lineNumber];
const partialLine = line.slice(0, columnNumber);
const groups = partialLine.match(/(\)|,)\s*?(([a-z]|[A-Z]|[0-9])+?)\s?=\s?throws\.$/);
let argumentName = null
if (groups !== null) {
argumentName = groups[2]
}
this.error(Symbol.for('UndefinedOrNullError'), parameterName || argumentName || '<unknown>')
}
ifUndefinedOrNull (object, objectName) {
if (object == undefined) {
this.error(Symbol.for('UndefinedOrNullError'), objectName)
}
}
ifUndefined (object, objectName) {
if (object === undefined) {
this.error(Symbol.for('UndefinedError'), objectName)
}
}
error (symbol, ...args) {
throw this.createError(symbol, ...args)
}
}