callable.js

const camelCase = require('camelcase');

 /**
  * Since {@link Callable} is extended by two classes, and a Callable itself
  * cannot be used, these classes have their own documentation of the callable
  * in which you might be interested:
  * - {@link Handler}'s callable: {@link Handler~callable}
  * - {@link Requirement}'s callable: {@link Requirement~callable}
  *
  * A callable itself looks like this:
  * ```
  * (done, processor) => {
  *   done();
  * }
  * ```
  * You can also overload parameters to the `done` callback. See
  * {@link Handler~done} and {@link Requirement~done} for further information
  *
  * You can also filter specific properties from the {@link Processor} object
  * using the ES6 destruction syntax:
  * ```
  * (done, { data, send }) => {
  *   // send() can not be used as is!
  * }
  * ```
  * Regarding the `send` method, please read {@link Processor#send}!
  *
  * To be able to destruct the processor to use relevant properties and have
  * access to a working send method, you can use the processor's `processor`
  * property:
  * ```
  * (done, { data, send, processor } => {
  *   processor.send('message', data);
  * })
  * ```
  *
  * **Binding the Processor to the function**
  *
  * Using a function, the Processor is bound to the callable. That means,
  * all properties of Processor can also be accessed through `this` inside
  * of the callable function, so the processor parameter is optional.
  *
  * By adding a name to the function, you can leave out the parameter for the
  * Callable name.
  * ```
  * // (namedFunction, requires)
  * new Callable(function testCallable(done, { data }) {
  *   this.data === processor.data;
  *   this.send('message', data)
  * });
  * ```
  *
  * @callback Callable~callable
  *
  * @param {Function}  done      Must be called from the callable when done
  * @param {Processor} processor Processor object containing all relevant
  *                              data
  *
  * @see Processor
  * @see Handler~callable
  * @see Requirement~callable
  */
class Callable {

  /**
   * #### Alternative constructors
   *
   * When registering ({@link Bot#register}) a {@link Handler} or constructing
   * a {@link Requirement}, you can also make use of alternative constructor
   * signatures:
   * ```
   * // (object)
   * new Callable({
   *   name: 'testCallable',
   *   callable: (done, processor) => {},
   *   options: { verbose: true }
   * });
   * ```
   * ```
   * // (name, callable, requires)
   * new Callable('testCallable', (done, processor) => {}, []);
   * ```
   * ```
   * // (namedFunction, requires)
   * new Callable(function testCallable(done, processor) {});
   * ```
   *
   * #### **1**. `(object)` - Object constructor
   * #### **2**. `(callable, [requires])` - Named function constructor
   * #### **3**. `(name, callable, [requires])` - Function constructor
   *
   * @param {object|Function|string} object            Object, Callable
   *                                                   function with name, or
   *                                                   Callable's name
   * @param {string}                 object.name       Callable's name
   * @param {Callable~callable}      object.callable   Callable function
   * @param {Requirement[]}          [object.requires] Requirements
   * @param {Requirement[]|Function} callable          Array of Requirements
   *                                                   or Callable
   * @param {Requirement[]}          [requires]        Array of Requirements
   *
   */
  constructor(object = {}, callable, requires) {
    if (typeof object === 'string' && callable instanceof Function) {
      // new Callable('testCallable', (data) => {})
      object = { name: object, callable, requires };
    } else if (object instanceof Function && object.name) {
      // new Callable(function testCallable, (data) => {})
      object = { name: object.name, callable: object, requires: callable };
    }

    // Check if essential parameters are unusable and throw Error
    this.setName(object.name);
    if (!object.callable || !(object.callable instanceof Function)) {
      throw new Error(this.constructor.name +
        ' needs to have a valid callable!');
    }

    // Use all passed properties, no matter if valid or not
    Object.assign(this, object);

    // Correct wrong values
    this.name = camelCase(object.name);
    this.setCallable(object.callable);

    if (!this.requires) this.requires = [];
    else if (!(this.requires instanceof Array)) this.requires = [this.requires];

    // flattens the requires tree to this.requirements
    this.prepareRequirements();
  }

  /**
   * Flattens the Callback's requires tree to this.requirements
   *
   * @param {Callable} [object] Object that might have `requires` node
   */
  prepareRequirements(object) {
    if (!object) this.requirements = [];
    const requires = object ? object.requires : this.requires;
    if (requires) {
      for (let i = 0; i < requires.length; i++) {
        const requirement = requires[i];
        // if may be required multiple times or
        // has not been added to the requirements until then
        if ((requirement.options && requirement.options.multiple)
          || !this.requirements.includes(requirement)) {
          // recursively get requirements of requirements
          this.prepareRequirements(requirement);
          this.requirements.push(requirement);
        }
      }
    }
  }

  /** @private */
  setCallable(callable) {
    if (callable instanceof Function) {
      this.callable = callable;
    } else {
      throw new TypeError('Callable must be a function!');
    }
  }

  setName(name) {
    if (!name) {
      throw new Error(`${this.constructor.name} needs to have a name!`);
    } else if (typeof name !== 'string') {
      throw new Error(`${this.constructor.name}'s name needs to be a string!`);
    }
    this.name = name;
  }
}

module.exports = Callable;