router.js

const { compose, lazy, passThru } = require('./composer')

/**
 * {@link Router} is used to direct the flow of update. It accepts as arguments a routing function and, optionally,
 * a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
 * with predefined routes and handlers. Routing function accepts {@link OpengramContext context}
 * and return object with route property set to String value. Handler for a specific route is just another middleware.
 *
 * As `Router` object is itself a middleware, routers can be nested, e.g., `router1.on('yo', router2)`.
 * Thus, they allow for very deep, well-structured and flexible logic of updates processing.
 * Possible use-cases include multilevel menus, setting different levels of access for bot users and much, much more
 *
 * ```js
 * const { Router } = require('opengram')
 *
 * // Can be any function that returns { route: String }
 * function routeFn(ctx) {
 *   return { route: ctx.updateType }
 * }
 *
 * const router = new Router(routeFn)
 *
 * // Registering 'callback_query' route
 * const middlewareCb = function (ctx, next) { ... }
 * router.on('callback_query', middlewareCb)
 *
 * // Registering 'message' route
 * const middlewareMessage = new Composer(...)
 * router.on('message', middlewareMessage)
 *
 * // Setting handler for routes that are not registered
 * const middlewareDefault = someOtherRouter
 * router.otherwise(middlewareDefault).
 * ```
 */
class Router {
  /**
   * Constructs a router with a routing function and optionally some
   * preinstalled middlewares.
   *
   * Note that you can always install more middlewares on the router by calling {@link Router#on}.
   *
   * @param {Function} routeFn A routing function that decides which middleware to run
   * @param {Map<Middleware>} [routeHandlers] Map object with middlewares
   */
  constructor (routeFn, routeHandlers = new Map()) {
    if (typeof routeFn !== 'function') {
      throw new Error('Missing routing function')
    }
    this.routeFn = routeFn
    this.handlers = routeHandlers
    this.otherwiseHandler = passThru()
  }

  /**
   * Registers new middleware for a given route. The initially supplied routing
   * function may return this route as a string to select the respective
   * middleware for execution for an incoming update.
   *
   * @param {string} route The route for which to register the middleware
   * @param {Middleware} fns Middleware(s) to register
   * @throws {TypeError}
   * @return {Router}
   */
  on (route, ...fns) {
    if (fns.length === 0) {
      throw new TypeError('At least one handler must be provided')
    }
    this.handlers.set(route, compose(fns))
    return this
  }

  /**
   * Allows to register middleware that is executed when no route matches, or
   * when the routing function returns `undefined`. If this method is not
   * called, then the router will simply pass through all requests to the
   * downstream middleware.
   *
   * @param {Middleware} fns Middleware(s) to run if no route matches
   * @throws {TypeError}
   */
  otherwise (...fns) {
    if (fns.length === 0) {
      throw new TypeError('At least one otherwise handler must be provided')
    }
    this.otherwiseHandler = compose(fns)
    return this
  }

  middleware () {
    return lazy((ctx) => {
      const result = this.routeFn(ctx)
      if (result == null) {
        return this.otherwiseHandler
      }
      Object.assign(ctx, result.context)
      Object.assign(ctx.state, result.state)
      return this.handlers.get(result.route) || this.otherwiseHandler
    })
  }
}

module.exports = Router