Command/Command.js

'use strict';

const assert = require('assert');

/**
 * @class Command
 * @constructor
 *
 * @description
 * Undoable command
 *
 * @example
 * const a = { sum: 0 };
 *
 * const command = new Command(
 *   (subject, inc) => subject.sum += inc,
 *   (subject, inc) => subject.sum -= inc,
 *   [a, 1]
 * );
 *
 * command.execute();
 * // a.sum = 1
 *
 * command.undo();
 * // a.sum = 0
 *
 * @tutorial command-creating-command-object
 */
class Command {
  /**
   * @param {Object} context - context of the command
   * @param {Function} execute - function for action
   * @param {Object} executeArguments - context of the command
   * @param {Function} undo - function for undo
   * @param {Object} undoArguments - context of the command
   */
  constructor(context, execute, executeArguments = [], undo = null, undoArguments = []) {
    this._context = context;

    assert(
      typeof context[execute] === 'function' ||
      typeof context.constructor[execute] === 'function',
      'Wrong input parameter: execute must be a valid function in context');
    assert(executeArguments instanceof Array,
      'Wrong input parameter: executeArguments must be an array');

    // Command execution standard workflow:
    this._execute = execute;
    this._executeArguments = executeArguments;

    if (undo !== null) {
      assert(typeof context[undo] === 'function',
        'Wrong input parameter: undo must be a valid function in context');
      assert(undoArguments instanceof Array,
        'Wrong input parameter: undoArguments must be an array');

      // Command undo workflow:
      this._undo = undo;
      this._undoArguments = undoArguments;
      this._undoable = true;
    } else {
      this._undoable = false;
    }
  }

  /**
   * Is the command undoable.
   *
   * @returns {Boolean} True if undoable, false otherwise
   */
  undoable() {
    return this._undoable;
  }

  /**
   * Apply the given function with arguments in the command context.
   *
   * @private
   * @param {Function} method Function to call
   * @param {Array} [args=[]] Arguments to apply on
   * @returns {mixed}
   */
  _apply(method, args = []) {
    if (typeof this._context.emit === 'function') {
      this._context.emit('execute', method, args);
    }
    const fn = this._context[method] || this._context.constructor[method];
    return fn.apply(this._context, args);
  }

  /**
   * Execute the command function with the given arguments
   *
   * @returns {mixed}
   */
  execute() {
    return this._apply(this._execute, this._executeArguments);
  }

  /**
   * Undo the command if possible.
   *
   * @see #undoable
   * @exception {Error} Raises an exception if the command is not _undoable_
   * @returns {mixed}
   */
  undo() {
    if (!this.undoable()) {
      throw new Error('Command is not undoable');
    }
    return this._apply(this._undo, this._undoArguments);
  }
}

module.exports = Command;