managers/Resolver.js

let Promise
try {
  Promise = require('bluebird')
} catch (err) {
  Promise = global.Promise
}

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

/** Resolver manager for resolving usages */
class Resolver extends Collection {
  /**
   * Creates a new Resolver instance
   * @arg {Client} client Client instance
   */
  constructor (client) {
    super()
    this._client = client
  }

  /**
   * Resolver function
   * @typedef {Object} ResolverObject
   * @prop {String} type Resolver type
   * @prop {Promise(Container)} resolve Promise that takes in {@link Container} as an argument and resolves a result
   */

  /**
   * Loads {@link ResolverObject|ResolverObjects} from a file path
   * @arg {String} path File path to load resolvers from
   * @returns {Promise<ResolverObject>}
   */
  loadResolvers (path) {
    const resolvers = requireRecursive(path)
    for (const name in resolvers) {
      const resolver = resolvers[name]
      if (!resolver.resolve || !resolver.type) continue
      this.set(resolver.type, resolver)
    }
    return resolvers
  }

  /**
   * Loads a command usage locally
   * @arg {CommandUsage} usage CommandUsage object to be loaded
   */
  load (data) {
    this.usage = this.verify(data)
  }

  /**
   * Verifies a {@link CommandUsage}
   * @arg {CommandUsage} usage CommandUsage object to be verified
   * @returns CommandUsage
   */
  verify (usage) {
    /**
     * An object documenting the requirements of a command argument
     * @namespace CommandUsage
     * @type {Object}
     */
    return (Array.isArray(usage) ? usage : [usage]).map(entry => {
      /**
       * The name of the argument
       * @type {String}
       * @memberof CommandUsage
       * @name name
       */
      if (!entry.name) {
        throw new Error('Argument specified in usage has no name')
      }
      /**
       * Single allowed argument type to be resolved
       * @type {String}
       * @memberof CommandUsage
       * @name type
       * @see {@link ResolverObject}
       */

      /**
       * Multiple allowed argument types to be resolved
       * @type {Array}
       * @memberof CommandUsage
       * @name types
       * @see {@link ResolverObject}
       */
      if (!entry.types) entry.types = [ entry.type || 'string' ]

      /**
       * The display name of the argument, to be used when displaying argument info
       * @type {?String}
       * @memberof CommandUsage
       * @name displayName
       */
      if (!entry.displayName) entry.displayName = entry.name
      return entry
    })
  }

  /**
   * Resolves a message
   * @arg {external:"Eris.Message"} message Eris message
   * @arg {String[]} args Array of strings, by default a message split by spaces, without command trigger and prefix
   * @arg {Object} data Additional data
   * @arg {String} data.prefix The client's prefix
   * @arg {String} data.command The command trigger
   * @arg {CommandUsage[]} [usage=this.usage] Array of CommandUsage objects
   * @returns {Promise<Object>}
   */
  resolve (message, rawArgs, data, rawUsage = this.usage) {
    let args = {}

    const usage = this.verify(rawUsage)
    if (!usage.length) return Promise.resolve(args)

    const argsCount = rawArgs.length
    const requiredArgs = usage.filter(arg => !arg.optional).length
    const optionalArgs = argsCount - requiredArgs

    if (argsCount < requiredArgs) {
      return Promise.reject({
        message: 'INSUFFICIENT_ARGS',
        requiredArgs: `**${requiredArgs}**`,
        argsCount: `**${argsCount}**.`
      })
    }

    let idx = 0
    let optArgs = 0
    let resolves = []
    let skip = false
    for (const arg of usage) {
      let rawArg = rawArgs[idx]
      if (arg.last) {
        rawArg = rawArgs.slice(idx).join(' ')
        skip = true
      } else {
        if (arg.optional) {
          if (optionalArgs > optArgs) {
            optArgs++
          } else {
            if (arg.default) args[arg.name] = arg.default
            continue
          }
        }
        if (typeof rawArg !== 'undefined') {
          if (rawArg.startsWith('"')) {
            const endQuote = rawArgs.findIndex((str, i) => str.endsWith('"') && i >= idx)
            if (endQuote > -1) {
              rawArg = rawArgs.slice(idx, endQuote + 1).join(' ').replace(/"/g, '')
              idx = endQuote
            } else {
              return Promise.reject({ message: 'NO_END_QUOTE' })
            }
          }
        }
        idx++
      }
      resolves.push(this._resolveArg(arg, rawArg, message, data).then(res => {
        args[arg.name] = res
        return res
      }))
      if (skip) break
    }
    return Promise.all(resolves).then(() => args)
  }

  _resolveArg (arg, rawArg, message, data) {
    const resolves = arg.types.map(type => {
      const resolver = this.get(type)
      if (!resolver) {
        return Promise.resolve({ err: 'Invalid resolver type' })
      }
      return resolver.resolve(rawArg, arg, message, this._client)
      .catch(err => Object.assign(arg, {
        arg: `**\`${arg.name || 'argument'}\`**`,
        err: err
      }))
    })
    return Promise.all(resolves).then(results => {
      const resolved = results.filter(v => !v.err)
      if (resolved.length) {
        const res = resolved.length === 1 ? resolved[0] : resolved.reduce((arr, c) => arr.concat(c), [])
        return res
      }
      let err = results[0].err
      if (err instanceof Error) {
        return Promise.reject({ message: 'PARSING_ERROR', err: err })
      }
      return Promise.reject({ message: err })
    })
  }

  /**
   * Gets a command usage
   * @arg {CommandUsage[]} [usage=this.usage] Array of CommandUsage objects
   * @arg {Object} data Additional data
   * @arg {String} data.prefix The client's prefix
   * @arg {String} data.command The command trigger
   * @returns {String}
   */
  getUsage (usage = this.usage, { prefix, command } = {}) {
    const argsUsage = usage.map(arg =>
      arg.last ? arg.displayName : arg.optional ? `[${arg.displayName}]` : `<${arg.displayName}>`
    ).join(' ')
    return `${prefix}${command}` + (usage.length ? ' ' + argsUsage : '')
  }
}

module.exports = Resolver