Skip to content

Commit

Permalink
Moved version verifier in own file & added param parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
mallocator committed Oct 16, 2016
1 parent d0b5e06 commit 037b048
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 125 deletions.
169 changes: 57 additions & 112 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var express = require('express');
var semver = require('semver');
var versionHandler = require('./lib/version');


/**
Expand All @@ -26,19 +27,15 @@ var semver = require('semver');
*/

/**
* @callback versionCb
* @param {string} incomingVersion The version that has been parsed off of the incoming request
* @param {string[]|number[]|RegExp[]} acceptVersion The version that this endpoint is accepting
* @param {versionResponseCb} cb A callback that lets the router know whether the version should be
* handled by this handler or not.
* @property {ClientRequest} req The http request object
* @property {ServerResponse} res The http response object
* @typedef {object} EndpointConfig
* @private
* @property {function} original The original router function to be called
* @property {string} method The name of the original function
* @property {string|RegExp} path The path configuration for the router
* @property {Array.<string|number|RegExp>} version An array with allowed versions
* @property {Array.<function>} handlers A list of request handlers to be called by the router
*/

/**
* @callback versionResponseCb
* @param {boolean} match Signal whether the version is a match or not.
*/

/**
* All supported methods by the express router that need to be proxied.
Expand Down Expand Up @@ -71,36 +68,69 @@ function Router(configuration = {}) {
let getRouter = generateRouter.bind({routers: [], configuration});
for (let method of methods) {
let original = router[method];
router[method] = (path, version, ...handlers) => {
if (path.toString().startsWith('/v:'+ configuration.param)) {
throw new Error('Versioned paths will be generated automatically, please avoid prefixing paths');
router[method] = (path, ...args) => {
if (typeof path != 'string' && !(path instanceof RegExp)) {
throw new Error('First parameter needs to be a path (string or RegExp)')
}
let methodRouter = getRouter(path, method);
if (typeof version == 'function') {
handlers.unshift(version);
version = [];
} else {
version = Array.isArray(version) ? version : [version];
let epc = parseParams(original, method, path, args);
if (epc.path.toString().startsWith('/v:'+ configuration.param)) {
throw new Error('Versioned paths will be generated automatically, please avoid prefixing paths');
}
if (!(path instanceof RegExp)) {
methodRouter[method]('/v:' + configuration.param + path, ...handlers);
original.call(router, '/v:' + configuration.param + path, parseVersion.bind({
let methodRouter = getRouter(epc.path, epc.method);
if (!(epc.path instanceof RegExp)) {
methodRouter[epc.method]('/v:' + configuration.param + epc.path, ...epc.handlers);
epc.original.call(router, '/v:' + configuration.param + epc.path, versionHandler.parseVersion.bind({
configuration,
acceptVersion: version,
acceptVersion: epc.version,
router: methodRouter
}));
}
methodRouter[method](path, ...handlers);
original.call(router, path, parseVersion.bind({
methodRouter[method](path, ...epc.handlers);
original.call(router, epc.path, versionHandler.parseVersion.bind({
configuration,
acceptVersion: version,
acceptVersion: epc.version,
router: methodRouter
}));
}
}
return router;
}

/**
* Parses incoming parameters into an object that is easy to pass around.
* @param {function} original
* @param {string} method
* @param {string|RegExp} path
* @param {Array} args
* @param {EndpointConfig} [config]
* @returns {EndpointConfig}
*/
function parseParams(original, method, path, args, config = {original, method, path, version: [], handlers: []}) {
for (let arg of args) {
if (arg instanceof Array) {
parseParams(original, method, path, arg, config);
continue;
}
switch (typeof arg) {
case 'object':
if (arg instanceof RegExp) {
config.version.push(arg);
}
break;
case 'number':
case 'string':
config.version.push(arg);
break;
case 'function':
config.handlers.push(arg);
break;
default:
throw new Error('Unsupported router parameter: ' + arg);
}
}
return config;
}

/**
* Returns a router based on the endpoint given. The function will try to minimize the number of routers required to
* support versions. It does that by looking in an array of routers whether there is one that doesn't have the given
Expand Down Expand Up @@ -130,89 +160,4 @@ function generateRouter(endpoint, method) {
return router;
}

/**
*
* @param {ClientRequest} req
* @param {ServerResponse} res
* @param {function} next
* @property {Object} router
* @property {string|number|RegExp} acceptVersion
*/
function parseVersion(req, res, next) {
let version = null;
for (let params of this.configuration.paramOrder) {
if (params == 'header') {
version = version || req.get(this.configuration.header);;
} else {
version = version || req[params] && req[params][this.configuration.param];
}
}
let validator = (this.configuration.validate || validateVersion).bind({ req, res });
validator(version, this.acceptVersion, (err, matches) => {
if (err) {
return next(err);
}
if (matches){
if (this.configuration.passVersion) {
req.incomingVersion = version;
req.acceptedVersion = this.acceptedVersion;
}
if (this.acceptVersion && !res.headersSent && this.configuration.responseHeader) {
res.set(this.configuration.responseHeader, this.acceptVersion.toString());
}
this.router.handle(req, res, next);
}
next()
});
}

/**
* The default version validator that will match the incoming version against the acceptable version for various types.
* @type versionCb
*/
function validateVersion(incomingVersion, acceptVersions, cb) {
let acceptRequest = true;
for (let acceptVersion of acceptVersions) {
acceptRequest = false;
switch (typeof acceptVersion) {
case 'string':
incomingVersion = semverizeVersion(incomingVersion);
acceptRequest = semver.satisfies(incomingVersion, acceptVersion);
break;
case 'number':
acceptRequest = acceptVersion == parseInt(incomingVersion);
break;
case 'object':
if (acceptVersion instanceof RegExp) {
acceptRequest = acceptVersion.test(incomingVersion);
break;
} else {
cb('Unable to understand object as version in router config. Use string, integer or RegExp instead.');
}
}
if (acceptRequest) {
return cb(null, true);
}
}
cb(null, acceptRequest);
}

/**
* This function tries to convert an incoming version into something that semver might understand.
* @param {string} version
* @returns {string}
*/
function semverizeVersion(version) {
version = '' + version;
let splitVersion = version.split('.');
switch(splitVersion.length) {
case 1:
return version + '.0.0';
case 2:
return splitVersion.join('.') + '.0';
default:
return version;
}
}

module.exports = Router;
96 changes: 96 additions & 0 deletions lib/version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
var semver = require('semver');

/**
* @callback versionCb
* @param {string} incomingVersion The version that has been parsed off of the incoming request
* @param {string[]|number[]|RegExp[]} acceptVersion The version that this endpoint is accepting
* @param {versionResponseCb} cb A callback that lets the router know whether the version should be
* handled by this handler or not.
* @property {ClientRequest} req The http request object
* @property {ServerResponse} res The http response object
*/

/**
* @callback versionResponseCb
* @param {boolean} match Signal whether the version is a match or not.
*/

/**
*
* @param {ClientRequest} req
* @param {ServerResponse} res
* @param {function} next
* @property {Object} router
* @property {string|number|RegExp} acceptVersion
*/
exports.parseVersion = function (req, res, next) {
let version = null;
for (let params of this.configuration.paramOrder) {
if (params == 'header') {
version = version || req.get(this.configuration.header);
} else {
version = version || req[params] && req[params][this.configuration.param];
}
}
let validator = (this.configuration.validate || exports.validateVersion).bind({req, res});
validator(version, this.acceptVersion, matches => {
if (matches) {
if (this.configuration.passVersion) {
req.incomingVersion = version;
req.acceptedVersion = this.acceptedVersion;
}
if (this.acceptVersion && !res.headersSent && this.configuration.responseHeader) {
res.set(this.configuration.responseHeader, this.acceptVersion.toString());
}
this.router.handle(req, res, next);
}
next()
});
};

/**
* The default version validator that will match the incoming version against the acceptable version for various types.
* @type versionCb
*/
exports.validateVersion = function(incomingVersion, acceptVersions, cb) {
let acceptRequest = true;
for (let acceptVersion of acceptVersions) {
acceptRequest = false;
switch (typeof acceptVersion) {
case 'string':
incomingVersion = exports.semverizeVersion(incomingVersion);
acceptRequest = semver.satisfies(incomingVersion, acceptVersion);
break;
case 'number':
acceptRequest = acceptVersion == parseInt(incomingVersion);
break;
case 'object':
if (acceptVersion instanceof RegExp) {
acceptRequest = acceptVersion.test(incomingVersion);
break;
}
}
if (acceptRequest) {
return cb(true);
}
}
cb(acceptRequest);
};

/**
* This function tries to convert an incoming version into something that semver might understand.
* @param {string} version
* @returns {string}
*/
exports.semverizeVersion = function(version) {
version = '' + version;
let splitVersion = version.split('.');
switch (splitVersion.length) {
case 1:
return version + '.0.0';
case 2:
return splitVersion.join('.') + '.0';
default:
return version;
}
};
24 changes: 11 additions & 13 deletions test/router.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* global describe, it, beforeEach, afterEach */
'use strict';

var async = require('async');
var expect = require('chai').expect;
Expand Down Expand Up @@ -60,6 +59,17 @@ describe('Router', () => {
expect(router.get.bind(null, '/v:v/test')).to.throw(Error);
});

it('should prevent me from not passing in a path', () => {
var router = Router();
expect(router.get.bind({})).to.throw(Error);
expect(router.get.bind(() => {})).to.throw(Error);
});

it('should prevent me from passing in unsupported parameters', () => {
var router = Router();
expect(router.get.bind(null, '/test', false)).to.throw(Error);
});

it('should be able to support multiple versions for the same endpoint', done => {
var router = Router();
router.get('/test', 1, (req, res) => res.end('success 1'));
Expand Down Expand Up @@ -147,18 +157,6 @@ describe('Router', () => {
request(app).get('/test?v=1').expect(200, 'success').end(done);
});

it('should ignore object configurations', done => {
var router = Router();
router.get('/test', {}, (req, res) => res.end('success'));

var app = express();
app.use(router);

async.series([
cb => request(app).get('/v1/test').expect(500).end(cb),
], done);
});

it('should be able to process multiple handlers', done => {
var router = Router();
router.get(/\/test/, 1, (req, res, next) => next(), (req, res) => res.end('success'));
Expand Down

0 comments on commit 037b048

Please sign in to comment.