Skip to content

Commit

Permalink
Merge pull request #73 from notonthehighstreet/substack-pattern
Browse files Browse the repository at this point in the history
Substack pattern
  • Loading branch information
yakovkhalinsky committed Jul 21, 2015
2 parents 1f3c1cc + eae1f95 commit 17bc102
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 135 deletions.
169 changes: 34 additions & 135 deletions lib/middleware/route-handlers.js
Original file line number Diff line number Diff line change
@@ -1,151 +1,50 @@
var glob = require('glob');
var fs = require('fs');
var protagonist = require('protagonist');
var async = require('async');

var pathToRegexp = require('path-to-regexp');
var _ = require('lodash');

var urlParser = require('../url-parser');
var route = require('../route');
var queryComparator = require('../query-comparator');
var buildRouteMap = require('./route-map');

var ROUTE_MAP = null;
var autoOptions;
var autoOptionsAction = require('../json/auto-options-action.json');

var parseAction = function(uriTemplate, action) {
var parsedUrl = urlParser.parse(uriTemplate);
var key = parsedUrl.url;

ROUTE_MAP[key] = ROUTE_MAP[key] || { urlExpression: parsedUrl.url, methods: {} };
ROUTE_MAP[key].methods[action.method] = ROUTE_MAP[key].methods[action.method] || [];

var routeHandlers = route.getRouteHandlers(key, parsedUrl, action);
Array.prototype.push.apply(ROUTE_MAP[key].methods[action.method], routeHandlers);

console.log('[LOG]'.white, 'Setup Route:', action.method.green, key.yellow, action.name.blue);
};
module.exports = function(options, cb) {
buildRouteMap(options, function(err, routeMap) {
if (err) {
cb(err);
}
var middleware = function(req, res, next) {
var handlers = null;

var parseBlueprint = function(filePath) {
return function(cb) {
var data = fs.readFileSync(filePath, {encoding: 'utf8'});
protagonist.parse(data, function(err, result) {
if (err) {
console.log(err);
return;
}
Object.keys(routeMap).forEach(function(urlPattern) {
if (handlers) {
return; // continue if we've already go handlers
}
var regex = pathToRegexp(urlPattern);

var allRoutesList = [];
result.ast.resourceGroups.forEach(function(resourceGroup){
resourceGroup.resources.forEach(function(resource){
resource.actions.forEach(function(action){
parseAction(resource.uriTemplate, action);
saveRouteToTheList(resource, action);
});
});
// req.path allows us to delegate query string handling to the route handler functions
var match = regex.exec(req.path);
if (match) {
handlers = routeMap[urlPattern].methods[req.method.toUpperCase()];
}
});

// add OPTIONS route where its missing - this must be done after all routes are parsed
if (autoOptions) {
addOptionsRoutesWhereMissing(allRoutesList);
}

cb();

/**
* Adds route and its action to the allRoutesList. It appends the action when route already exists in the list.
* @param resource Route URI
* @param action HTTP action
*/
function saveRouteToTheList(resource, action) {
// used to add options routes later
if (typeof allRoutesList[resource.uriTemplate] === 'undefined') {
allRoutesList[resource.uriTemplate] = [];
if (handlers) {
var queryParams = Object.keys(req.query);
if (queryParams.length === 0){
handlers.sort(queryComparator.noParamComparator);
} else {
queryComparator.countMatchingQueryParms(handlers, queryParams);
handlers.sort(queryComparator.queryParameterComparator);
}
allRoutesList[resource.uriTemplate].push(action);
}

function addOptionsRoutesWhereMissing(allRoutes) {
var routesWithoutOptions = [];
// extracts only routes without OPTIONS
_.forIn(allRoutes, function (actions, route) {
var containsOptions = _.reduce(actions, function (previousResult, iteratedAction) {
return previousResult || (iteratedAction.method === 'OPTIONS');
}, false);
if (!containsOptions) {
routesWithoutOptions.push(route);
var requestHandled = false;
handlers.forEach(function (handler) {
if (requestHandled) {
return;
}
requestHandled = handler.execute(req, res);
});

_.forEach(routesWithoutOptions, function (uriTemplate) {
// adds prepared OPTIONS action for route
parseAction(uriTemplate, autoOptionsAction);
});
}
});
};
};

var setup = function(sourceFiles, cb) {
ROUTE_MAP = {};
glob(sourceFiles, {} , function (err, files) {
if (err) {
console.error('Failed to parse contracts path.', err);
process.exit(1);
}

var asyncFunctions = [];

files.forEach(function(filePath) {
asyncFunctions.push(parseBlueprint(filePath));
});

async.series(asyncFunctions, function(err) {
cb(err);
});
});

};

module.exports = function(options, cb) {
var sourceFiles = options.sourceFiles;
autoOptions = options.autoOptions;
var middleware = function(req, res, next) {
var handlers = null;
Object.keys(ROUTE_MAP).forEach(function(urlPattern) {
if (handlers) {
return; // continue if we've already go handlers
}
var regex = pathToRegexp(urlPattern);

// req.path allows us to delegate query string handling to the route handler functions
var match = regex.exec(req.path);
if (match) {
handlers = ROUTE_MAP[urlPattern].methods[req.method.toUpperCase()];
}
});

if (handlers) {
var queryParams = Object.keys(req.query);
if (queryParams.length === 0){
handlers.sort(queryComparator.noParamComparator);
} else {
queryComparator.countMatchingQueryParms(handlers, queryParams);
handlers.sort(queryComparator.queryParameterComparator);
}

var requestHandled = false;
handlers.forEach(function (handler) {
if (requestHandled) {
return;
}
requestHandled = handler.execute(req, res);
});
}
next();
};

setup(sourceFiles, function(err) {
cb(err, middleware);
next();
};
cb(null, middleware);
});
};
26 changes: 26 additions & 0 deletions lib/middleware/route-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
var glob = require('glob');
var async = require('async');
var parseBlueprint = require('../parse-blueprint');

module.exports = function(options, cb) {
var sourceFiles = options.sourceFiles;
var autoOptions = options.autoOptions;
var routeMap = {};

glob(sourceFiles, {} , function (err, files) {
if (err) {
console.error('Failed to parse contracts path.', err);
process.exit(1);
}

var asyncFunctions = [];

files.forEach(function(filePath) {
asyncFunctions.push(parseBlueprint(filePath, autoOptions, routeMap));
});

async.series(asyncFunctions, function(err) {
cb(err, routeMap);
});
});
};
15 changes: 15 additions & 0 deletions lib/parse-action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
var urlParser = require('./url-parser');
var route = require('./route');

module.exports = function(uriTemplate, action, routeMap) {
var parsedUrl = urlParser.parse(uriTemplate);
var key = parsedUrl.url;

routeMap[key] = routeMap[key] || { urlExpression: parsedUrl.url, methods: {} };
routeMap[key].methods[action.method] = routeMap[key].methods[action.method] || [];

var routeHandlers = route.getRouteHandlers(key, parsedUrl, action);
Array.prototype.push.apply(routeMap[key].methods[action.method], routeHandlers);

console.log('[LOG]'.white, 'Setup Route:', action.method.green, key.yellow, action.name.blue);
};
65 changes: 65 additions & 0 deletions lib/parse-blueprint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
var fs = require('fs');
var protagonist = require('protagonist');
var _ = require('lodash');
var parseAction = require('./parse-action');
var autoOptionsAction = require('./json/auto-options-action.json');

module.exports = function(filePath, autoOptions, routeMap) {
return function(cb) {
var data = fs.readFileSync(filePath, {encoding: 'utf8'});
protagonist.parse(data, function(err, result) {
if (err) {
console.log(err);
return;
}

var allRoutesList = [];
result.ast.resourceGroups.forEach(function(resourceGroup){
resourceGroup.resources.forEach(function(resource){
resource.actions.forEach(function(action){
parseAction(resource.uriTemplate, action, routeMap);
saveRouteToTheList(resource, action);
});
});
});

// add OPTIONS route where its missing - this must be done after all routes are parsed
if (autoOptions) {
addOptionsRoutesWhereMissing(allRoutesList);
}

cb();

/**
* Adds route and its action to the allRoutesList. It appends the action when route already exists in the list.
* @param resource Route URI
* @param action HTTP action
*/
function saveRouteToTheList(resource, action) {
// used to add options routes later
if (typeof allRoutesList[resource.uriTemplate] === 'undefined') {
allRoutesList[resource.uriTemplate] = [];
}
allRoutesList[resource.uriTemplate].push(action);
}

function addOptionsRoutesWhereMissing(allRoutes) {
var routesWithoutOptions = [];
// extracts only routes without OPTIONS
_.forIn(allRoutes, function (actions, route) {
var containsOptions = _.reduce(actions, function (previousResult, iteratedAction) {
return previousResult || (iteratedAction.method === 'OPTIONS');
}, false);
if (!containsOptions) {
routesWithoutOptions.push(route);
}
});

_.forEach(routesWithoutOptions, function (uriTemplate) {
// adds prepared OPTIONS action for route
parseAction(uriTemplate, autoOptionsAction, routeMap);
});
}
});
};
};

0 comments on commit 17bc102

Please sign in to comment.