Skip to content

Commit

Permalink
Combined api router with version router into one
Browse files Browse the repository at this point in the history
  • Loading branch information
mallocator committed Oct 16, 2016
1 parent 037b048 commit 35c75a3
Show file tree
Hide file tree
Showing 7 changed files with 1,454 additions and 16 deletions.
81 changes: 66 additions & 15 deletions index.js
@@ -1,6 +1,9 @@
var apiVerifier = require('./lib/verifier');
var express = require('express');
var path = require('path');
var responder = require('./lib/responder');
var semver = require('semver');
var versionHandler = require('./lib/version');
var versionVerifier = require('./lib/version');


/**
Expand Down Expand Up @@ -42,7 +45,7 @@ var versionHandler = require('./lib/version');
* @type {string[]} The method names npm
*/
var methods = [
'all', 'param', 'get', 'post', 'put', 'head', 'delete', 'options', 'trace', 'copy', 'lock', 'mkcol', 'move',
'all', 'get', 'post', 'put', 'head', 'delete', 'options', 'trace', 'copy', 'lock', 'mkcol', 'move',
'purge', 'propfind', 'proppatch', 'unlock', 'report', 'mkactivity', 'checkout', 'merge', 'm-search', 'notify',
'subscribe', 'unsubscribe', 'patch', 'search', 'connect'
];
Expand All @@ -64,35 +67,42 @@ const defaultConfig = {
*/
function Router(configuration = {}) {
configuration = Object.assign({}, defaultConfig, configuration);
configuration.prefix = normalizePrefix(configuration.prefix);
let router = express.Router(configuration);
let getRouter = generateRouter.bind({routers: [], configuration});
let context = {
endpoints: {},
router: null,
globalConfiguration: configuration
};
for (let method of methods) {
let original = router[method];
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 epc = parseParams(original, method, path, args);
if (epc.path.toString().startsWith('/v:'+ configuration.param)) {
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 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: epc.version,
router: methodRouter
}));
}
methodRouter[method](path, ...epc.handlers);
original.call(router, epc.path, versionHandler.parseVersion.bind({
context.router = methodRouter;
let apiHandler = apiVerifier.configure(context, epc).bind(context);
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[method](path, apiHandler, ...epc.handlers);
original.call(router, epc.path, versionHandler);
}
}
router.__defineGetter__('endpoints', prefixEndpoints.bind(context));
router.api = api.bind(context);
return router;
}

Expand All @@ -115,7 +125,9 @@ function parseParams(original, method, path, args, config = {original, method, p
case 'object':
if (arg instanceof RegExp) {
config.version.push(arg);
break;
}
config.api = arg;
break;
case 'number':
case 'string':
Expand Down Expand Up @@ -160,4 +172,43 @@ function generateRouter(endpoint, method) {
return router;
}

/**
* Returns either an empty string or a normalized path of the prefix
* @param {string} prefix
* @returns {string}
*/
function normalizePrefix(prefix) {
if (!prefix || typeof prefix !== 'string' || !prefix.trim().length) {
return '';
}
return path.normalize(prefix);
}

/**
* Getter implementation that will return the currently configured enpoints.
* @returns {Object.<string, Object.<string, EndpointConfig>>} Api map with endpoint config nested in path and method.
* @this Context
*/
function prefixEndpoints(prefix = this.globalConfiguration.prefix) {
var map = {};
for (let prop in this.endpoints) {
map[path.join(prefix, prop)] = Object.assign({}, this.endpoints[prop]);
}
return map;
}

/**
* A standard request handler implementation that will respond with the currently configured api for this router. Can be used to make
* it easier for developers to work with your API.
* @param {ClientRequest} req An express client request object
* @param {ServerResponse} res An express server response object
* @this Context
*/
function api(req, res) {
var url = req.originalUrl;
var prefix = url.substr(0, url.lastIndexOf(req.route.path));
prefix = prefix.substr(0, prefix.lastIndexOf(this.globalConfiguration.prefix));
responder.respond(req, res, prefixEndpoints.call(this, prefix.length ? prefix : this.globalConfiguration.prefix));
}

module.exports = Router;
142 changes: 142 additions & 0 deletions lib/responder.js
@@ -0,0 +1,142 @@
'use strict';

/**
*
* @param {ClientRequest} req The client request object
* @param {ServerResponse} res The server response object
* @param {Object} payload The data to be send to the client
* @param {number} [status] A status code that will be set if you don't want express to set one for you
*/
exports.respond = function(req, res, payload, status) {
status && res.status(status);
if (!payload || !Object.keys(payload).length) {
status || res.status(204);
return res.end();
}
var format = req.params && req.params['format'] || req.query && req.query['format'] || 'json';
switch(format.toLowerCase()) {
case 'json':
return res.jsonp ? res.jsonp(payload).end() : res.json(payload).end();
case 'tree':
return res.send('<html><body style="white-space: pre">' + JSON.stringify(payload, null, 4) + '</body></html>').end();
case 'table':
return res.send(exports.formatTable(payload)).end();
case 'csv':
return res.send(exports.formatCSV(payload)).end();
case 'xml':
return res.send('<xml version="1.0" encoding="UTF-8">' + exports.formatXML(payload)).end() + '</xml>';
default:
return res.status(422).send('Invalid format requested').end();
}
};

/**
* Creates an HTML table of the json data passed in.
* @param {Object} payload
* @returns {string}
*/
exports.formatTable = function (payload) {
var table = exports.flatten(payload);
var response = '<html><body><h1>' + table.title + '</h1><table><thead>';
for (let header of table.headers) {
response += '<th>' + header + '</th>';
}
response += '</thead><tbody>';
for (let row of table.rows) {
response += '<tr>';
for (let column of row) {
response += '<td>' + column + '</td>';
}
response += '</tr>';
}
response += '</tbody></table></body></html>';
return response;
};

/**
* Creates a CSV table of the json data passed in.
* @param {Object} payload
* @returns {string}
*/
exports.formatCSV = function (payload) {
var table = exports.flatten(payload);
var response = table.headers.join(',') + '\n';
for (let row of table.rows) {
for(let i = 0; i < row.length; i++) {
row[i].replace(/"/g, '\\"');
if (row[i].indexOf(',') !== -1) {
row[i] = '"' + row[i] + '"';
}
}
response += row.join(',') + '\n';
}
return response;
};

/**
* A response that can be used by the different formatters to construct a table response out of the JSON tree structure.
* @typedef {Object} TableResponse
* @property {string} title A title that should be printed at the head of a page
* @property {string[]} headers A list of headers to be printed before the actual data
* @property {Array.<string[]>} rows A list of rows represented by lists of strings to be printed in order
*/

/**
* Converts the known response formats into a list of map like structure to represent a table
* @param {Object} payload
* @returns {TableResponse} The response to be printed
*/
exports.flatten = function(payload) {
var response = {
rows: []
};
if (payload.error) {
response.title = payload.error;
response.headers = [ 'param', 'type', 'error', 'max', 'min'];
for (let param in payload.params) {
response.rows.push([
param,
payload.params[param].type,
payload.params[param].error,
isNaN(payload.params[param].max) ? '' : payload.params[param].max,
isNaN(payload.params[param].min) ? '' : payload.params[param].min
])
}
} else {
response.title = 'Api Map';
response.headers = [ 'path', 'method', 'description', 'param', 'type', 'paramDescription'];
for (let path in payload) {
for (let method in payload[path]) {
for (let param in payload[path][method].params) {
response.rows.push([
path,
method,
payload[path][method].description,
param,
payload[path][method].params[param].type,
payload[path][method].params[param].description
]);
}
}
}
}
return response;
};



/**
* Creates an XML response of the json data passed in.
* @param {Object|string} payload
* @returns {string}
*/
exports.formatXML = function (payload) {
var response = '';
if (typeof payload == 'object') {
for (let prop in payload) {
response += '<' + prop + '>' + exports.formatXML(payload[prop]) + '</' + prop + '>';
}
return response;
}
return payload;
};

0 comments on commit 35c75a3

Please sign in to comment.