Source: router.js

/**
 * @file express middleware to map source files and methods to an oas specification
 * @author Florian Schaper <f.schaper@reply.de>
 * @copyright Florian Schaper 2018, MIT License
 */

'use strict';

const express = require('express');
const defaultResolver = require('./resolver');

/**
 * @callback Next
 */

/**
 * @callback ExpressHandler
 * @param {Object} req - the http request object
 * @param {Object} res - the http response object
 * @param {Next} next - a continuation function
 */

/**
 * @callback Resolver
 * @param {string} path - api path as defined in the oas specification
 * @param {string} method - method as defined by the oas specification e.g. get, put, post, delete, head, ...
 * @param {Object} options - additional configuration for the method under the given path
 * @returns {ExpressHandler}
 */

/**
 * @callback RouterErrorCallback
 * @param {Error} exception - exception thrown by the resolver
 * @param {string} path - path as defined by the open api specification
 * @param {string} method - a method as defined by the open api specification for that path
 * @param {Object} options - additional configuration options for that path and method from the open api specification
 */

/**
 * @callback RouterOnBeforeRouteAddedCallback
 * @param {string} path - path as defined by the open api specification
 * @param {Object} options - additional configuration options for that path and containing all methods and their configurations
 */

/**
 * @callback RouterOnBeforeMethodAddedCallback
 * @param {ExpressHandler} handler - handler for the specific path and method
 * @param {string} path - path as defined by the open api specification
 * @param {string} method - a method as defined by the open api specification for that path
 * @param {Object} options - additional configuration options for that path and method from the open api specification
 */

/**
 * @typedef {Object} RouterConfiguration
 * @property {Object} specification - the open api specification that will be used for mapping to the controllers
 * @property {Object} [router] - an express router object where the routes from the specification will be attached to. you can use this e.g. to configure a base path to your api. by default a new `express.Router()` object is used.
 * @property {Resolver} [resolver] - function which resolves a uri path and method to an express function handler. See the documentation for the resolver for more detail about the default behavior.
 * @property {RouterErrorCallback} [onError] - callback that will be called for errors that may occur during handler lookup. defaults to a function returning `false` - which will re-throw any exception and abort the router configuration.
 * @property {RouterOnBeforeRouteAddedCallback} [onBeforeRouteAdded] - callback that will be called for every route path that is to be added to the router. defaults to an `noop`.
 * @property {RouterOnBeforeMethodAddedCallback} [onBeforeRouteMethodAdded] - callback that will be called for every route method that is to be added to the current router path. defaults to an `noop`.
 */

/**
 * initializes an express router object with routes as defined by the passed open api 3.0.x specification
 *
 * @param {RouterConfiguration} options - configuration for the middleware. only `specification` is required if the defaults suit your needs
 * @returns {Object} an express router middleware
 * @throws TypeError if an invalid configuration option has been passed to this function
 * @throws forwards additional thrown exceptions to the caller, depending on the resolver used and the configured error handler
 */
function router(options) {
  // ensure that the passed parameter is of object type
  if (typeof options !== 'object') {
    throw new TypeError('the oas router expects to be called with an configuration object');
  }

  // ensure that we were given an specification object
  if (typeof options.specification !== 'object') {
    throw new TypeError('the oas router expects an open api 3.0.x json specification to be passed via the specification option');
  }

  // initialize with default values
  const {
    specification,
    router: expressRouter,
    resolver,
    onError,
    onBeforeRouteAdded,
    onBeforeRouteMethodAdded
  } = Object.assign({
    router: express.Router(),
    resolver: defaultResolver({ basePath: './controller' }),
    onError: () => false,
    onBeforeRouteAdded: () => {},
    onBeforeRouteMethodAdded: () => {}
  }, options);

  // matches oas parameter definitions e.g. `{myPram}`
  const oasParameters = /\{([^}]+)\}/g;
  // walk over all path definitions of the oa spec
  Object.entries(specification.paths || {}).forEach(([path, configuration]) => {
    // execute callback before attaching a new route to the router
    onBeforeRouteAdded({ path, options: configuration });
    // convert any path parameters from open api to express router format
    // e.g. from `/users/{userId}` to `/users/:userId` format create a new route for the specified path
    const route = expressRouter.route(path.replace(oasParameters, ':$1'));
    // iterate over all defined oas methods for this route
    Object.entries(configuration || {}).forEach(([method, methodOptions]) => {
      try {
        // find the method handler code for the specified method
        const handler = resolver({ path, method, options: methodOptions });
        // execute callback before attaching the method to the router
        onBeforeRouteMethodAdded({
          handler,
          path,
          method,
          options: methodOptions
        });
        // attach the method handler to the router
        // eslint-disable-next-line security/detect-object-injection
        route[method](handler);
      } catch (exception) {
        // execute the error callback - if the handler returns true, attaching
        // paths to the router will continue. otherwise the exception will be re-thrown
        const continueProcessing = onError({
          exception,
          path,
          method,
          options: methodOptions
        });
        if (!continueProcessing) {
          throw exception;
        }
      }
    });
  });

  return expressRouter;
}

module.exports = router;