/**
* @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;