Skip to content

Commit

Permalink
First impl that makes version and api work together
Browse files Browse the repository at this point in the history
  • Loading branch information
mallocator committed Oct 17, 2016
1 parent 827f0ab commit 4a3ebe0
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 77 deletions.
44 changes: 24 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var apiVerifier = require('./lib/apiVerifier');
var express = require('express');
var Endpoints = require('./lib/endpoints');
var path = require('path');
var responder = require('./lib/responder');
var versionVerifier = require('./lib/versionVerifier');
Expand Down Expand Up @@ -87,7 +88,6 @@ const defaultConfig = {
};

// TODO support use, param and route?
// TODO Store API documentation nested under version
// TODO return api for specific version with parameter given
// TODO write tests for mix between version and api configs

Expand All @@ -101,11 +101,7 @@ function Router(configuration = {}) {
configuration.prefix = normalizePrefix(configuration.prefix);
let router = configuration.routerFunction(configuration);
let getRouter = generateRouter.bind({routers: [], configuration});
let context = {
endpoints: {},
router: null,
configuration
};
let endpoints = new Endpoints(configuration);
for (let method of methods) {
let original = router[method];
router[method] = (path, ...args) => {
Expand All @@ -115,25 +111,28 @@ function Router(configuration = {}) {
if (path.toString().startsWith('/v:'+ configuration.param)) {
throw new Error('Versioned paths will be generated automatically, please avoid prefixing paths');
}
let epc = parseParams(original, method, path, args);
let epc = parseParams.call(configuration, original, method, path, args);
let methodRouter = getRouter(epc.path, epc.method);
context.router = methodRouter;
let apiHandler = apiVerifier.configure(context, epc);
let apiHandler = apiVerifier.configure({
endpoints,
configuration,
router: methodRouter
}, epc);
let versionHandler = versionVerifier.parseVersion.bind({
configuration,
acceptVersion: epc.version,
router: methodRouter
});
if (!(epc.path instanceof RegExp)) {
methodRouter[epc.method]('/v:' + configuration.param + epc.path, apiHandler, ...epc.handlers);
epc.original.call(router, '/v:' + configuration.param + epc.path, versionHandler);
methodRouter[epc.method](epc.versionedPath, apiHandler, ...epc.handlers);
epc.original.call(router, epc.versionedPath, versionHandler);
}
methodRouter[method](path, apiHandler, ...epc.handlers);
original.call(router, epc.path, versionHandler);
}
}
router.__defineGetter__('endpoints', prefixEndpoints.bind(context));
router.api = api.bind(context);
router.__defineGetter__('endpoints', prefixEndpoints.bind({ configuration, endpoints }));
router.api = api.bind({ configuration, endpoints });
return router;
}

Expand All @@ -145,8 +144,17 @@ function Router(configuration = {}) {
* @param {Array} args
* @param {EndpointConfig} [config]
* @returns {EndpointConfig}
* @this {RouterConfig}
*/
function parseParams(original, method, path, args, config = {original, method, path, version: [], handlers: []}) {
function parseParams(original, method, path, args, config) {
config = config || {
original,
method,
path,
versionedPath: '/v:' + this.param + path,
version: [], // TODO rename to versionS
handlers: []
};
for (let arg of args) {
if (arg instanceof Array) {
parseParams(original, method, path, arg, config);
Expand Down Expand Up @@ -220,12 +228,8 @@ function normalizePrefix(prefix) {
* @returns {Object.<string, Object.<string, EndpointConfig>>} Api map with endpoint config nested in path and method.
* @this Context
*/
function prefixEndpoints(prefix = this.configuration.prefix) {
var map = {};
for (let prop in this.endpoints) {
map[path.join(prefix, prop)] = Object.assign({}, this.endpoints[prop]);
}
return map;
function prefixEndpoints(prefix) {
return this.endpoints.list(prefix);
}

/**
Expand Down
10 changes: 6 additions & 4 deletions lib/apiVerifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ exports.configure = function(context, { path, method, api, version}) {
api.params[param] = parsed;
}
}
context.endpoints[path] = context.endpoints[path] || {};
context.endpoints[path][method.toUpperCase()] = api;
context.endpoints.add(path, method, version, api);
return exports.verify.bind(context);
};

Expand Down Expand Up @@ -147,7 +146,10 @@ exports.parseValue = function(type, value, array) {
* @this Context
*/
exports.verify = function(req, res, next) {
var config = this.endpoints[req.route.path][req.method];
var config = this.endpoints.get(req.route.path, req.method, req.incomingVersion);
if (!config) {
return next();
}
try {
var params = exports.getParams(config, req);
} catch (e) {
Expand Down Expand Up @@ -190,7 +192,7 @@ exports.getParams = function(config, req) {
for (let prop of config.paramOrder) {
if (req[prop]) {
for (let param in req[prop]) {
if (params[param] === undefined) {
if (params[param] === undefined && config.params[param]) {
params[param] = exports.parseValue(config.params[param].type, req[prop][param], config.params[param].array);
}
}
Expand Down
88 changes: 88 additions & 0 deletions lib/endpoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
var path = require('path');
var versionVerifier = require('./versionVerifier');


class Endpoints {
/**
* @param {RouterConfig} configuration
*/
constructor(configuration) {
this._mapping = {};
this._config = configuration;
}

/**
* Add a new configuration to the endpoint mapping.
* @param {string} path
* @param {string} method
* @param {Array.<number|string|RegExp>} [versions]
* @param {RouterConfig} config
*/
add(path, method, versions, config) {
versions = versions.length ? versions : [ 0 ];
path = this._unversion(path);
config.versions = versions;
for (let version of versions) {
this._mapping[version] = this._mapping[version] || {};
this._mapping[version][path] = this._mapping[version][path] || {};
this._mapping[version][path][method.toUpperCase()] = config;
}
}

/**
* Retrieve the mapping configuration for an endpoint.
* @param {string} path
* @param {string} method
* @param {string} incomingVersion
* @returns {*}
*/
get(path, method, incomingVersion = 0) {
path = this._unversion(path);
try {
let config = {};
for (let version in this._mapping) {
if (versionVerifier.validateVersion(incomingVersion, version)) {
config[version] = config[version] || {};
config[version][path] = config[version][path] || {};
config[version][path][method] = this._mapping[version][path][method];
}
}
if (Object.keys(config).length == 1) {
return config[Object.keys(config)[0]][path][method];
}
// TODO not sure where and how to use this, but the format is wrong.
return config;
} catch (e) {
return null;
}
}

/**
* Removes the version prefix from a given path (if there is one)
* @param {string} path
* @returns {string}
* @private
*/
_unversion(path) {
if (path.startsWith('/v:' + this._config.param)) {
return path.substr(('/v:' + this._config.param).length);
}
return path;
}

/**
*
* @param {string} [prefix]
* @param {string|number|RegExp} [version]
*/
list(prefix = this._config.prefix, version = 0) {
let map = {};
for (let prop in this._mapping[version]) {
map[path.join(prefix, prop)] = this._mapping[version][prop];
}
return map;

}
}

module.exports = Endpoints;
13 changes: 8 additions & 5 deletions lib/versionVerifier.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
var _ = require('lodash');
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.
* @param {versionResponseCb} [cb] A callback that should, be called in case your verifier works
* async, otherwise you can just return the truth value.
* @property {ClientRequest} req The http request object
* @property {ServerResponse} res The http response object
* @returns {boolean|*} Should return a boolean if no callback has been provided, otherwise this value is ignored.
*/

/**
Expand Down Expand Up @@ -52,6 +54,7 @@ exports.parseVersion = function (req, res, next) {
* @type versionCb
*/
exports.validateVersion = function(incomingVersion, acceptVersions, cb) {
acceptVersions = _.isArray(acceptVersions) ? acceptVersions : [ acceptVersions ];
let acceptRequest = true;
for (let acceptVersion of acceptVersions) {
acceptRequest = false;
Expand All @@ -70,10 +73,10 @@ exports.validateVersion = function(incomingVersion, acceptVersions, cb) {
}
}
if (acceptRequest) {
return cb(true);
return cb ? cb(true) : true;
}
}
cb(acceptRequest);
cb ? cb(acceptRequest) : acceptRequest;
};

/**
Expand All @@ -92,4 +95,4 @@ exports.semverizeVersion = function(version) {
default:
return version;
}
};
};
15 changes: 10 additions & 5 deletions test/api-router.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ describe('Api Router', () => {
required: true,
type: 'number'
}
}
},
versions: [ 0 ]
}
}
}).end(done);
Expand Down Expand Up @@ -200,7 +201,8 @@ describe('Api Router', () => {
success: undefined,
validate: undefined
}
}
},
versions: [ 0 ]
}
}
});
Expand All @@ -219,7 +221,8 @@ describe('Api Router', () => {
required: true,
type: 'number'
}
}
},
versions: [ 0 ]
}
}
}).end(done);
Expand Down Expand Up @@ -255,7 +258,8 @@ describe('Api Router', () => {
required: true,
type: 'number'
}
}
},
versions: [ 0 ]
}
}
}).end(done);
Expand Down Expand Up @@ -291,7 +295,8 @@ describe('Api Router', () => {
required: true,
type: 'number'
}
}
},
versions: [ 0 ]
}
}
}).end(done);
Expand Down
28 changes: 28 additions & 0 deletions test/router.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* global describe, it, beforeEach, afterEach */

var async = require('async');
var expect = require('chai').expect;
var express = require('express');
var request = require('supertest');

var Router = require('..');

describe('Router', () => {
it('should support both api configuration and versioning in one', done => {
var router = Router();
router.get('/test', 1, {
params: {
var1: 'number',
var2: 'string(foo)'
}
}, (req, res) => res.end('success'));

var app = express();
app.use(router);
async.series([
cb => request(app).get('/test').expect(404).end(cb),
cb => request(app).get('/v1/test').expect(422).end(cb),
cb => request(app).get('/v1/test?var1=25').expect(200, 'success').end(cb)
], done);
});
});
Loading

0 comments on commit 4a3ebe0

Please sign in to comment.