sirap-group/connect-sequence

View on GitHub
lib/ConnectSequence.js

Summary

Maintainability
B
4 hrs
Test Coverage
'use strict'

var MissingArgumentError = require('./errors/MissingArgumentError')

module.exports = ConnectSequence

/**
 * @class
 * @param {object} req The request object
 * @param {object} res The response object
 * @param {function} next The next middleware
 * @returns {undefined}
 */
function ConnectSequence (req, res, next) {
  var errorMsg
  if (arguments.length < 3) {
    errorMsg = 'ConnectSequence#run() takes 3 arguments. '
    errorMsg += arguments.length + ' given.'
    throw new MissingArgumentError(errorMsg)
  }
  if (typeof req !== 'object') {
    errorMsg = 'The first argument must be a request object. '
    errorMsg += typeof req + ' given.'
    throw new TypeError(errorMsg)
  }
  if (typeof res !== 'object') {
    errorMsg = 'The second argument must be a request object. '
    errorMsg += typeof req + ' given.'
    throw new TypeError(errorMsg)
  }
  if (typeof next !== 'function') {
    errorMsg = 'The third argument must be a middleware function. '
    errorMsg += typeof req + ' given.'
    throw new TypeError(errorMsg)
  }

  this.req = req
  this.res = res
  this.next = next
  this.middlewares = []
}

ConnectSequence.prototype = {
  append: append,
  appendList: appendList,
  appendIf: appendIf,
  appendListIf: appendListIf,
  run: run
}

/**
 * Run sequencially each appended middleware, using the next argument as the final callback.
 * @method
 * @memberof ConnectSequence.prototype
 * @returns {undefined}
 */
function run () {
  var midSequence = this.middlewares.reverse()
  var initialNext = this.next.bind()
  var req = this.req
  var res = this.res
  var nestedCallSequence

  // create the call sequence
  nestedCallSequence = midSequence.reduce(middlewareReducer, initialNext)
  // call it
  nestedCallSequence.call()

  /**
   * Reduce the middleware sequence to a nested middleware handler sequence
   * @function
   * @inner
   * @private
   * @param {Function} callSequence intermediate resulting call sequence
   * @param {Function} middleware the current middleware
   * @returns {Function} the new intermediate resulting call sequence
   */
  function middlewareReducer (callSequence, middleware) {
    return function nextHandler (err) {
      // if the previous middleware passed an error argument
      if (err !== undefined) {
        if (isErrorHandler(middleware)) {
          // call the current middleware if it is an error handler middleware
          middleware(err, req, res, callSequence)
        } else {
          // else skip the current middleware and call the intermediate sequence
          callSequence(err)
        }
      } else {
        // if no error argument is passed
        if (isErrorHandler(middleware)) {
          // skip the current middleware if it is an errorHandler
          callSequence()
        } else {
          // else call it
          middleware(req, res, callSequence)
        }
      }
    }
  }
}

/**
 * Append an arbitrary number of middlewares as an argument list
 * @method
 * @memberof ConnectSequence.prototype
 * @param {...Function} middlewares A list of middleware functions (or errorHandler middlewares)
 * @returns {ConnectSequence} a reference to the instance to be chainable
 * @throws TypeError if one of the given middlewares is not a function. All the given middlewares would be rejected.
 */
function append (/* mid_0, ..., mid_n */) {
  var i
  for (i = 0; i < arguments.length; i++) {
    if (typeof arguments[i] !== 'function') {
      var type = typeof arguments[i]
      var errMsg = 'Given middlewares must be functions. "' + type + '" given.'
      throw new TypeError(errMsg)
    }
  }
  for (i = 0; i < arguments.length; i++) {
    this.middlewares.push(arguments[i])
  }
  return this
}

/**
 * Append an arbitrary number of middlewares as an array
 * @method
 * @memberof ConnectSequence.prototype
 * @param {Array<Function>} middlewares An array of middleware functions (or errorHandler middlewares)
 * @returns {ConnectSequence} a reference to the instance to be chainable
 */
function appendList (middlewares) {
  if (!Array.isArray(middlewares)) {
    var errorMsg = 'First argument must be an array of middlewares. '
    errorMsg += typeof middlewares + ' given.'
    throw new TypeError(errorMsg)
  }
  return this.append.apply(this, middlewares)
}

/**
 * Append an arbitrary number of middlewares as an argument list if the filter pass at runtime of the first middleware in the given list
 * @method
 * @memberof ConnectSequence.prototype
 * @param {Function} filter A filter function (returning a Boolean)
 * @param {...Function} middlewares A list of middleware functions (or errorHandler middlewares)
 * @returns {ConnectSequence} a reference to the instance to be chainable
 */
function appendIf (filter /*, middlewares */) {
  var errorMsg
  var middlewares = []

  if (arguments.length < 2) {
    errorMsg = 'ConnectSequence#appendIf() takes 2 arguments. '
    errorMsg += arguments.length + ' given.'
    throw new MissingArgumentError(errorMsg)
  }
  if (typeof filter !== 'function') {
    errorMsg = 'The first argument must be a filter function. '
    errorMsg += typeof filter + ' given.'
    throw new TypeError(errorMsg)
  }

  var middleware, i
  for (i = 1; i < arguments.length; i++) {
    middleware = arguments[i]
    if (typeof middleware !== 'function') {
      errorMsg = 'The second argument must be a middleware function. '
      errorMsg += typeof middleware + ' given.'
      throw new TypeError(errorMsg)
    }
    middlewares.push(middleware)
  }

  var firstMiddleware = middlewares[0]
  if (isErrorHandler(firstMiddleware)) {
    this.append(function (err, req, res, next) {
      req.__connectSequenceFilterValue = filter(req)
      if (req.__connectSequenceFilterValue) {
        firstMiddleware(err, req, res, next)
      } else {
        next()
      }
    })
  } else {
    this.append(function (req, res, next) {
      req.__connectSequenceFilterValue = filter(req)
      if (req.__connectSequenceFilterValue) {
        firstMiddleware(req, res, next)
      } else {
        next()
      }
    })
  }

  for (i = 1; i < middlewares.length; i++) {
    middleware = middlewares[i]
    appendOnFilterValue.call(this, middleware)
  }

  return this
}

/**
 * A middleware filter function run at the middleware runtime, causing the middleware or middleware list should be ran or skiped
 * @callback middlewareFilter
 * @param {IncomingMessage} req The express request object
 * @returns {Boolean}
 */

/**
 * Append an arbitrary number of middlewares as an array if the filter pass at runtime of the first middleware in the given list
 * @method
 * @memberof ConnectSequence.prototype
 * @param {middlewareFilter} filter A filter function on the req object
 * @param {Array<Function>} middlewares An array of middleware functions (or errorHandler middlewares)
 * @returns {ConnectSequence} a reference to the instance to be chainable
 */
function appendListIf (filter, middlewares) {
  var args = [filter]
  for (var i = 0; i < middlewares.length; i++) {
    args.push(middlewares[i])
  }
  return this.appendIf.apply(this, args)
}

/**
 * Tells if a given middleware is a regular middleware or an error handler
 * @function
 * @inner
 * @private
 * @param {Function} middleware
 * @returns {Boolean}
 */
function isErrorHandler (cb) {
  return cb.length === 4
}

/**
 * Append a middleware in function of its nature and the filter value stored in req.__connectSequenceFilterValue
 * @function
 * @inner
 * @private
 * @param {Function} middleware
 * @returns {undefined}
 */
function appendOnFilterValue (middleware) {
  if (isErrorHandler(middleware)) {
    this.append(function (err, req, res, next) {
      if (req.__connectSequenceFilterValue) {
        middleware(err, req, res, next)
      } else {
        next()
      }
    })
  } else {
    this.append(function (req, res, next) {
      if (req.__connectSequenceFilterValue) {
        middleware(req, res, next)
      } else {
        next()
      }
    })
  }
}