rjrodger/eraro

View on GitHub
eraro.js

Summary

Maintainability
A
0 mins
Test Coverage
/* Copyright (c) 2014-2023 Richard Rodger, MIT License */
/* jshint node:true, asi:true, eqnull:true */

// Create JavaScript Error objects with code strings, context details,
// and templated messages.
'use strict'

// #### System modules
const Util = require('util')

// #### Exports
module.exports = eraro

// #### Create an _eraro_ function
// Parameters:
//
//   * _options_ : (optional) Object; properties:
//      * _package_ : (optional) String; package name to mark Error objects
//      * _prefix_  : (optional) Boolean/String; If false, then no prefix is used; If not defined, the package name is used
//      * _module_  : (optional) Object; _module_ object to use as starting point for _require_ calls
//      * _msgmap_  : (optional) Object; map codes to message templates
//      * _inspect_ : (optional) Boolean; If true, _Util.inspect_ is called on values; default: true.
//
// Returns: Function
//
// The created function has parameters:
//
//   * _exception_ : (optional) Error; the original exception to be wrapped
//   * _code_ : (optional) String; code value
//   * _message_ : (optional) String; error message, will be processed as a template
//   * _details_ : (optional) Object; contextual details of the error, used to insert details into message
//
// and returns an Error object (to be thrown or used in a callback, as needed).
// The returned Error object has the following additional properties:
//
//   * _code_: String; the code string
//   * _package_: String; the package name
//   * _**package-name**_: Boolean (true); a convenience marker for the package
//   * _msg_: String; the generated message, may differ from original exception message (if any)
//   * _details_: Object; contextual details of error
//   * _callpoint_: String; first line of stacktrace that is external to eraro and calling module
function eraro(options) {
  options = options || {}

  const msgprefix =
    false === options.prefix
      ? ''
      : 'string' === typeof options.prefix
      ? options.prefix
      : 'string' === typeof options.package
      ? options.package + ': '
      : ''

  const pkg = options.package || 'unknown'
  const callmodule = options.module || module
  const msgmap = options.msgmap || {}
  const inspect = null == options.inspect ? true : !!options.inspect

  const markers = [module.filename]

  const filename = callmodule.filename
  if (filename) {
    markers.push(filename)
  }

  const errormaker = function (ex, code, msg, details) {
    if (Util.isError(ex)) {
      if (ex.eraro && !options.override) {
        return ex
      }
    } else {
      ex = null
      code = arguments[0]
      msg = arguments[1]
      details = arguments[2]
    }

    code =
      'string' === typeof code
        ? code
        : ex
        ? ex.code
          ? ex.code
          : ex.message
          ? ex.message
          : 'unknown'
        : 'unknown'

    details =
      'object' === typeof details ? details : 'object' === typeof msg ? msg : {}

    if (ex) {
      details.errmsg = ex.message
      details.errline = callpoint(ex, markers)
    }

    msg = 'string' === typeof msg ? msg : null
    msg = buildmessage(
      options,
      msg,
      msgmap,
      msgprefix,
      inspect,
      code,
      details,
      ex,
    )

    const err = new Error(msg)

    if (ex) {
      details.orig$ = null == details.orig$ ? ex : details.orig$
      details.message$ =
        null == details.message$ ? ex.message : details.message$

      // drag along properties from original exception
      for (const p in ex) {
        err[p] = ex[p]
      }
    }

    err.eraro = true

    err.orig = ex // orig
    err.code = code
    err[pkg] = true
    err.package = pkg
    err.msg = msg
    err.details = details

    err.stack = ex ? ex.stack : err.stack
    err.callpoint = details.errline || callpoint(err, markers)

    return err
  }

  errormaker.callpoint = callpoint

  errormaker.has = function (code) {
    return !!msgmap[code]
  }

  return errormaker
}

// #### Find the first external stack trace line.
// Parameters:
//
//   * _error_ : (optional) Error; provides the stack
//   * _markers_ : (optional) Array[String]; ignore lines containing these strings
//
// Returns: String; stack trace line, with indent removed
function callpoint(error, markers) {
  markers = Array.isArray(markers) ? markers : []

  const stack = error ? error.stack : null
  let out = ''

  if (stack) {
    const lines = stack.split('\n')
    let i = 0

    line_loop: for (i = 1; i < lines.length; i++) {
      const line = lines[i]

      let found = false
      for (let j = 0; j < markers.length; j++) {
        if ('string' === typeof markers[j]) {
          found = -1 != line.indexOf(markers[j])
          if (found) break
        }
      }

      if (!found) break line_loop
    }

    out = 'string' === typeof lines[i] ? lines[i].substring(4) : out
  }

  return out
}

// #### Build the message string from a template by inserting details
// Uses the underscore template function with default settings.
// The original message (_msg_) has priority over messages from the _msgmap_.
// If no message can be found, the _code_ is used as a message.
// If an insert property is not defined, it is replaced with _[name?]_ in the message.
// As a convenience, _util_ and ___ are made available in the templates.
//
// Parameters:
//
//   * _msg_ : (required) String; message template
//   * _msgmap_ : (required) Object; map codes to message templates
//   * _msgprefix_: (required) String; prefix for all messages, useful as indentification of error origin
//   * _code_: (required) String; error code
//   * _details_: (required) Object; error details providing context
//
// Returns: String; human readable error message
function buildmessage(
  options,
  msg,
  msgmap,
  msgprefix,
  _inspect,
  code,
  details,
  ex,
) {
  let message =
    msgprefix +
    ('string' === typeof msg
      ? msg
      : 'string' === typeof msgmap[code]
      ? msgmap[code]
      : ex
      ? originalmsg(options.override, ex)
      : code)

  // These are the inserts.
  let valmap = Object.assign({}, details, { code: code })

  message = message.replace(/<%=\s*(.*?)\s*%>/g, (m, p1) => {
    let val = valmap[p1]
    val = 'object' === typeof val && null != val ? stringify(val) : val

    return val
  })

  return message
}

function stringify(val) {
  try {
    return JSON.stringify(val)
      .substring(0, 111)
      .replace(/([^\\])"/g, '$1')
  } catch (e) {
    return '' + val
  }
}

function originalmsg(override, ex) {
  if (!ex) return

  if (override && ex.eraro && ex.orig) return ex.orig.message

  return ex.message
}