core/Router.js

const path = require('path')
const fs = require('fs')

const { requireRecursive, isDir, Collection } = require('../util')

/**
 * Router class for event routing
 * @prop {Object} events Event map
 */
class Router extends Collection {
  /**
   * Creates a new Router instance
   * @arg {Client} client Client instance
   */
  constructor (client) {
    super()
    this._client = client
    this._cached = []
    this.events = {}
  }

  /**
   * Class, object or function that can be utilised as a module
   * @typedef {(Object|function)} AbstractModule
   * @prop {String} name Module name
   * @prop {Object} events Object mapping Eris event name to a function name
   * @example
   * // in constructor
   * events: {
   *   messageCreate: 'onMessage'
   * }
   *
   * // in class or object
   * onMessage (msg) {
   *   // handle message
   * }
   */

   /**
    * Registers modules
    * @arg {String|Object|Array} modules An object, array or relative path to a folder or file to load modules from
    * @returns {Client}
    */
  register (modules) {
    switch (typeof modules) {
      case 'string': {
        const filepath = path.join(process.cwd(), modules)
        if (!fs.existsSync(filepath)) {
          throw new Error(`Folder path ${filepath} does not exist`)
        }
        const mods = isDir(filepath) ? requireRecursive(filepath) : require(filepath)
        this._cached.push(filepath)
        return this.register(mods)
      }
      case 'object': {
        if (Array.isArray(modules)) {
          for (const module of modules) {
            this.attach(module)
          }
          return this
        }
        for (const key in modules) {
          this.attach(modules[key])
        }
        return this
      }
      default: {
        throw new Error('Path supplied is not an object or string')
      }
    }
  }

  /**
   * Attaches a module
   * @arg {AbstractModule} Module Module class, object or function
   * @returns {Router}
   */
  attach (Module) {
    const module = typeof Module === 'function' ? new Module(this._client) : Module
    this.set(module.name, module)
    for (const event in module.events) {
      if (typeof this.events[event] === 'undefined') {
        this.record(event)
      }

      const listener = module.events[event]
      if (typeof module[listener] !== 'function') {
        this._client.throwOrEmit('router:error', new TypeError(`${listener} in ${module.name} is not a function`))
        return this
      }

      this.events[event] = Object.assign(this.events[event] || {}, { [module.name]: listener })
    }

    /**
     * Fires when a module is registered
     *
     * @event Client#router:registered
     * @type {Object}
     * @prop {String} name Module name
     * @prop {Number} events Number of events in the module
     * @prop {Number} count Number of loaded modules
     */
    this._client.emit('router:registered', {
      name: module.name,
      events: Object.keys(module.events),
      count: this.size
    })
    return this
  }

  /**
   * Registers an event
   * @arg {String} event Event name
   * @returns {Router}
   */
  record (event) {
    this._client.on(event, (...args) => {
      const events = this.events[event] || {}
      for (const name in events) {
        const module = this.get(name)
        if (!module) continue
        try {
          module[events[name]](...args)
        } catch (err) {
          this._client.throwOrEmit('router:runError', err)
        }
      }
    })
    return this
  }

  /**
   * Initialises all modules
   * @returns {Router}
   */
  initAll () {
    this.forEach(module => {
      if (typeof module.init === 'function') {
        module.init()
      }
    })
    return this
  }

  /**
   * Unregisters all modules
   * @returns {Router}
   */
  unregister () {
    return this.destroy()
  }

  /**
   * Destroys all modules and unloads them
   * @returns {Router}
   */
  destroy () {
    for (const event in this.events) {
      this.events[event] = {}
    }
    this.forEach(module => {
      if (typeof module.unload === 'function') {
        module.unload()
      }
    })
    this.clear()
    return this
  }

  /**
   * Reloads module files (only those that have been added from by file path)
   * @returns {Client}
   */
  reload () {
    for (const filepath of this._cached) {
      this._client.unload(filepath)
      this._cached.shift()
      this.register(filepath)
    }
    return this
  }

  /**
   * Fires when an error occurs in Router
   *
   * @event Client#router:error
   * @type {Error}
   */

  /**
   * Fires when an error occurs in Router's event handling
   *
   * @event Client#router:runError
   * @type {Error}
   */
}

module.exports = Router