errors/LocalizableError.js

/**
 * @copyright 2023 Tridium, Inc. All Rights Reserved.
 */

/**
 * API Status: **Developer**
 * @module nmodule/js/rc/errors/LocalizableError
 */

define([
  'baja!',
  'lex!',
  'underscore',
  'Promise' ], function (
  baja,
  lex,
  _,
  Promise) {

  'use strict';

  /**
   * Makes sure the params are an array and also escapes them correctly if they are a string
   * @param {Array.<*>|*} params the parameters that need to be processed
   * @returns {String} the parameters as a String with each entry escaped and separted by a ":"
   */
  function convertParams(params) {
    params = Array.isArray(params) ? params : [ params ];
    return params.map((param) => (_.isString(param) ? baja.SlotPath.escape(param) : param)).join(':');
  }

  /**
   * Provides a simple means of throwing, or rejecting with, an Error that is intended to be
   * displayed to the user.
   *
   * (Previously, in order to both throw an error and display an error dialog to the user, you
   * would have to do it in two steps: display the error dialog with `feDialogs.error` or similar,
   * and then throw the actual error so the Promise itself would reject.)
   *
   * To use `LocalizableError`, simply create and throw an instance of it. Many places in the
   * framework are capable of handling `LocalizableError` and displaying it to the user:
   *
   * - The `log!` plugin
   * - `baja.error()`
   * - `feDialogs.error()`
   * - And transitively, any functionality in the UI that is configured to log/display Errors
   *   through these APIs, including:
   *   - `Command` instances that are invoked through `CommandButtons` (via the HTML5 Hx Profile,
   *    Manager views, etc.)
   *   - Hyperlinking between views
   *
   * It follows that if you display the error yourself, and _also_ throw it:
   *
   * ```
   * this.validators().add((value) => {
   *   if (value > max) {
   *     const err = new LocalizableError('%lexicon(myModule:valueTooBig)%');
   *     return feDialogs.error(err)
   *       .then(() => { throw err; });
   *   }
   * });
   * ```
   *
   * the framework will not know that you already displayed the error, and will display it a second
   * time. `LocalizableError` is intended to simply be thrown by your code, not displayed _and_
   * thrown.
   *
   * @since Niagara 4.14
   * @class
   * @alias module:nmodule/js/rc/errors/LocalizableError
   * @extends Error
   */
  class LocalizableError extends Error {
    /**
     * Creates a LocalizableError
     * @param {String|Object} params either an error message as a string or an object that contains
     * the values for a formatted error
     * @param {String} [params.module] the module name to use for looking up the lexicon entries
     * @param {String} [params.lex] the lexicon key to look up the error message components
     * @param {String} [params.title] the title for the error.  If both lexicon information and title
     * is supplied the title overrides any title found in the lexicon. This can be in the format of
     * a Lexicon format. For instance, `%lexicon(moduleName:keyName)%`.
     * @param {Array<*>} [params.titleArgs] an array of value to be passed to the 'lex.format' command
     * to complete the title.  Only works with module and lex.
     * @param {String} [params.summaryMessage] the summaryMessage for the error that is prepended to
     * the error message.  If both lexicon information and summaryMessage is supplied the summaryMessage
     * overrides any summaryMessage found in the lexicon This can be in the format of * a Lexicon
     * format. For instance, `%lexicon(moduleName:keyName)%`.
     * @param {Array<*>} [params.summaryArgs] an array of value to be passed to the 'lex.format'
     * command to complete the summary message.  Only works with module and lex.
     * @param {String} [params.message] the error message for the error.  If both lexicon
     * information and message is supplied the message overrides any message found in the lexicon
     * This can be in the format of a Lexicon format. For instance, `%lexicon(moduleName:keyName)%`.
     * @param {Array<*>} [params.messageArgs] an array of value to be passed to the 'lex.format' command
     * to complete the message.  Only works with module and lex.
     */
    constructor(params = {}) {
      let { module, lex, title, titleArgs, summaryMessage, summaryArgs, message, messageArgs } = params;

      if (!(params instanceof Object)) {
        message = params;
      }

      if (module && lex) {
        if (!title) {
          title = '%lexicon(' + module + ':' + lex + '.title';
          if (titleArgs) {
            title = title + ':' + convertParams(titleArgs);
          }
          title = title + ')%';
        }

        if (!summaryMessage) {
          summaryMessage = '%lexicon(' + module + ':' + lex + '.summaryMessage';
          if (summaryArgs) {
            summaryMessage = summaryMessage + ':' + convertParams(summaryArgs);
          }
          summaryMessage = summaryMessage + ')%';
        }

        if (!message) {
          message = '%lexicon(' + module + ':' + lex + '.message';
          if (messageArgs) {
            message = message + ':' + convertParams(messageArgs);
          }
          message = message + ')%';
        }
      }

      super(message);

      this.name = "LocalizableError";
      this.$title = title;
      this.$summaryMessage = summaryMessage;
    }

    /**
     * Resolves the title for the error
     * @returns {Promise.<String>}
     */
    toTitle() {
      return this.$doFormat(this.$title);
    }

    /**
     * Resolves the message summary for the error
     * @returns {Promise.<String>}
     */
    toSummaryMessage() {
      return this.$doFormat(this.$summaryMessage);
    }

    /**
     * Resolves the message for the error
     * @returns {Promise.<String>}
     */
    toMessage() {
      return this.$doFormat(this.message);
    }

    /**
     * Resolves to a formatted string that holds the error stack
     * @returns {Promise.<string>}
     */
    toStack() {
      let stack = this.stack;

      if (stack && typeof stack === 'string') {
        return Promise.all([
          this.toSummaryMessage(),
          this.toMessage()
        ])
          .then(([ summary, message ]) => {
            const name = this.name;
            message = name + ": " + ((summary) ? summary + ": " + message : message);

            if (this.message) {
              let stackArray = stack.split('\n');
              if (stackArray[0].indexOf(name + ": " + this.message) !== 0) {
                stackArray.unshift(message);
              } else {
                stackArray[0] = message;
              }

              stack = stackArray.join('\n');
            }

            return stack;
          });
        }

      return Promise.resolve(stack);
    }
    /**
     * Checks to see if the string to be formatted and if undefined returns undefined.
     * @private
     * @param {String} str the string rot formatted
     * @returns {Promise<String|undefined>}
     */
    $doFormat(str) {
      if (str === undefined) {
        return Promise.resolve(undefined);
      }

      return lex.format(...arguments);
    }
  }

  return LocalizableError;
});