Browse Source

Refactor controllers to be simple midddleware

Refactor search and doc controllers to allow for post-processing middleware
to handle geojson and response sending. Allows for a much more flexible routing scheme.
pull/210/head
Diana Shkolnikov 9 years ago
parent
commit
db0e063d8c
  1. 76
      app.js
  2. 14
      controller/place.js
  3. 12
      controller/search.js
  4. 73
      middleware/geocodeJSON.js
  5. 6
      middleware/sendJSON.js
  6. 30
      sanitiser/coarse.js
  7. 29
      sanitiser/suggest.js
  8. 14
      test/unit/controller/place.js
  9. 14
      test/unit/controller/search.js
  10. 2
      test/unit/run.js
  11. 72
      test/unit/sanitiser/coarse.js

76
app.js

@ -1,52 +1,76 @@
var Router = require('express').Router;
var app = require('express')();
var peliasConfig = require( 'pelias-config' ).generate().api;
if( peliasConfig.accessLog ){
app.use( require( './middleware/access_log' )( peliasConfig.accessLog ) );
}
/** ----------------------- middleware ----------------------- **/
/** ----------------------- pre-processing-middleware ----------------------- **/
app.use( require('./middleware/headers') );
app.use( require('./middleware/cors') );
app.use( require('./middleware/jsonp') );
/** ----------------------- sanitisers ----------------------- **/
/**
* Helper function for creating routers
*
* @param {[{function}]} functions
* @returns {express.Router}
*/
function createRouter(functions) {
var router = Router(); // jshint ignore:line
functions.forEach(function (f) {
router.use(f);
});
return router;
}
var routers = {};
var sanitisers = {};
sanitisers.place = require('./sanitiser/place');
sanitisers.suggest = require('./sanitiser/suggest');
sanitisers.search = require('./sanitiser/search');
sanitisers.coarse = require('./sanitiser/coarse');
sanitisers.reverse = require('./sanitiser/reverse');
routers.search = createRouter([
require('./sanitiser/search').middleware,
require('./controller/search')()
]);
/** ----------------------- controllers ----------------------- **/
routers.reverse = createRouter([
require('./sanitiser/reverse').middleware,
require('./controller/search')(undefined, require('./query/reverse'))
]);
var controllers = {};
controllers.index = require('./controller/index');
controllers.place = require('./controller/place');
controllers.search = require('./controller/search');
routers.place = createRouter([
require('./sanitiser/place').middleware,
require('./controller/place')()
]);
routers.index = createRouter([
require('./controller/index')()
]);
/** ----------------------- routes ----------------------- **/
// api root
app.get( '/', controllers.index() );
app.get( '/', routers.index );
app.get( '/place', routers.place );
app.get( '/autocomplete', routers.search );
app.get( '/search', routers.search);
app.post( '/search', routers.search);
app.get( '/reverse', routers.reverse );
// place API
app.get( '/place', sanitisers.place.middleware, controllers.place() );
// suggest APIs
app.get( '/suggest', sanitisers.search.middleware, controllers.search() );
app.get( '/suggest/nearby', sanitisers.suggest.middleware, controllers.search() );
app.get( '/suggest/coarse', sanitisers.coarse.middleware, controllers.search() );
/** -------------------- post-processing-middleware ------------------**/
// search APIs
app.get( '/search', sanitisers.search.middleware, controllers.search() );
app.get( '/search/coarse', sanitisers.coarse.middleware, controllers.search() );
// TODO: name mapping for admin values (admin0 => country, etc)
app.use(require('./middleware/geocodeJSON')());
app.use(require('./middleware/sendJSON'));
// reverse API
app.get( '/reverse', sanitisers.reverse.middleware, controllers.search(undefined, require('./query/reverse')) );
/** ----------------------- error middleware ----------------------- **/

14
controller/place.js

@ -1,5 +1,4 @@
var service = { mget: require('../service/mget') };
var geojsonify = require('../helper/geojsonify').search;
function setup( backend ){
// allow overriding of dependencies
@ -14,18 +13,15 @@ function setup( backend ){
};
});
service.mget( backend, query, function( err, docs ){
service.mget( backend, query, function( err, docs ) {
// error handler
if( err ){ return next( err ); }
// convert docs to geojson
var geojson = geojsonify( docs, req.clean );
// response envelope
geojson.date = new Date().getTime();
req.results = {
data: docs
};
// respond
return res.status(200).json( geojson );
next();
});
}

12
controller/search.js

@ -1,6 +1,5 @@
var service = { search: require('../service/search') };
var geojsonify = require('../helper/geojsonify').search;
function setup( backend, query ){
@ -32,14 +31,11 @@ function setup( backend, query ){
// error handler
if( err ){ return next( err ); }
// convert docs to geojson
var geojson = geojsonify( docs, req.clean );
// response envelope
geojson.date = new Date().getTime();
req.results = {
data: docs
};
// respond
return res.status(200).json( geojson );
next();
});
}

73
middleware/geocodeJSON.js

@ -0,0 +1,73 @@
var extend = require('extend');
var geojsonify = require('../helper/geojsonify').search;
function setup(peliasConfig) {
peliasConfig = peliasConfig || require( 'pelias-config' ).generate().api;
function middleware(req, res, next) {
return convertToGeocodeJSON(peliasConfig, req, next);
}
return middleware;
}
function convertToGeocodeJSON(peliasConfig, req, next) {
req.results.geojson = { geocoding: {} };
// REQUIRED. A semver.org compliant version number. Describes the version of
// the GeocodeJSON spec that is implemented by this instance.
req.results.geojson.geocoding.version = '0.1';
// OPTIONAL. Default: null. The licence of the data. In case of multiple sources,
// and then multiple licences, can be an object with one key by source.
// Can be a freeform text property describing the licensing details.
// Can be a URI on the server, which outlines licensing details.
req.results.geojson.geocoding.license = peliasConfig.host + '/license'; // TODO: add to config
// OPTIONAL. Default: null. The attribution of the data. In case of multiple sources,
// and then multiple attributions, can be an object with one key by source.
// Can be a URI on the server, which outlines attribution details.
req.results.geojson.geocoding.attribution = peliasConfig.host + '/attribution'; // TODO: add to config
// OPTIONAL. Default: null. The query that has been issued to trigger the
// search.
// Freeform object.
// This is the equivalent of how the engine interpreted the incoming request.
// Helpful for debugging and understanding how the input impacts results.
req.results.geojson.geocoding.query = req.clean;
// OPTIONAL. Warnings and errors.
addMessages(req.results, 'warnings', req.results.geojson.geocoding);
addMessages(req.results, 'errors', req.results.geojson.geocoding);
// OPTIONAL
// Freeform
addEngine(peliasConfig, req.results.geojson.geocoding);
// response envelope
req.results.geojson.geocoding.timestamp = new Date().getTime();
// convert docs to geojson and merge with geocoding block
extend(req.results.geojson, geojsonify(req.results.data, req.clean));
next();
}
function addMessages(results, msgType, geocoding) {
if (results.hasOwnProperty(msgType)) {
geocoding.messages = geocoding.messages || {};
geocoding.messages[msgType] = results[msgType];
}
}
function addEngine(peliasConfig, geocoding) {
geocoding.engine = {
name: 'Pelias',
author: 'Mapzen',
version: peliasConfig.version // TODO: add to config
};
}
module.exports = setup;

6
middleware/sendJSON.js

@ -0,0 +1,6 @@
function sendJSONResponse(req, res) {
// respond
return res.status(200).json(req.results.geojson);
}
module.exports = sendJSONResponse;

30
sanitiser/coarse.js

@ -1,30 +0,0 @@
var _sanitize = require('../sanitiser/_sanitize'),
sanitizers = {
input: require('../sanitiser/_input'),
size: require('../sanitiser/_size'),
layers: function( req ) {
req.query.layers = 'admin';
var layers = require('../sanitiser/_layers');
return layers(req);
},
latlonzoom: require('../sanitiser/_geo'),
details: require('../sanitiser/_details')
};
var sanitize = function(req, cb) { _sanitize(req, sanitizers, cb); };
// export sanitize for testing
module.exports.sanitize = sanitize;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
req.clean = clean;
next();
});
};

29
sanitiser/suggest.js

@ -1,29 +0,0 @@
var _sanitize = require('../sanitiser/_sanitize'),
sanitizers = {
input: require('../sanitiser/_input'),
size: require('../sanitiser/_size'),
layers: require('../sanitiser/_layers'),
details: require('../sanitiser/_details'),
latlonzoom: function( req ) {
var geo = require('../sanitiser/_geo');
return geo(req, true);
}
};
var sanitize = function(req, cb) { _sanitize(req, sanitizers, cb); };
// export sanitize for testing
module.exports.sanitize = sanitize;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
req.clean = clean;
next();
});
};

14
test/unit/controller/place.js

@ -55,10 +55,13 @@ module.exports.tests.functional_success = function(test, common) {
t.equal(json.type, 'FeatureCollection', 'valid geojson');
t.true(Array.isArray(json.features), 'features is array');
t.deepEqual(json.features, expected, 'values correctly mapped');
t.end();
}
};
controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ] } }, res );
var next = function next() {
t.equal(arguments.length, 0, 'next was called without error');
t.end();
};
controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ] } }, res, next );
});
var detailed_expectation = [{
@ -109,10 +112,13 @@ module.exports.tests.functional_success = function(test, common) {
t.equal(json.type, 'FeatureCollection', 'valid geojson');
t.true(Array.isArray(json.features), 'features is array');
t.deepEqual(json.features, detailed_expectation, 'values correctly mapped along with details');
t.end();
}
};
controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ], details: true } }, res );
var next = function next() {
t.equal(arguments.length, 0, 'next was called without error');
t.end();
};
controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ], details: true } }, res, next );
});
};

14
test/unit/controller/search.js

@ -57,10 +57,13 @@ module.exports.tests.functional_success = function(test, common) {
t.equal(json.type, 'FeatureCollection', 'valid geojson');
t.true(Array.isArray(json.features), 'features is array');
t.deepEqual(json.features, expected, 'values correctly mapped');
t.end();
}
};
controller( { clean: { a: 'b' } }, res );
var next = function next() {
t.equal(arguments.length, 0, 'next was called without error');
t.end();
};
controller( { clean: { a: 'b' } }, res, next );
});
var detailed_expectation = [{
@ -111,10 +114,13 @@ module.exports.tests.functional_success = function(test, common) {
t.equal(json.type, 'FeatureCollection', 'valid geojson');
t.true(Array.isArray(json.features), 'features is array');
t.deepEqual(json.features, detailed_expectation, 'values with details correctly mapped');
t.end();
}
};
controller( { clean: { a: 'b', details: true } }, res );
var next = function next() {
t.equal(arguments.length, 0, 'next was called without error');
t.end();
};
controller( { clean: { a: 'b', details: true } }, res, next );
});
};

2
test/unit/run.js

@ -8,11 +8,9 @@ var tests = [
require('./controller/search'),
require('./service/mget'),
require('./service/search'),
require('./sanitiser/suggest'),
require('./sanitiser/search'),
require('./sanitiser/reverse'),
require('./sanitiser/place'),
require('./sanitiser/coarse'),
require('./query/indeces'),
require('./query/sort'),
require('./query/search'),

72
test/unit/sanitiser/coarse.js

@ -1,72 +0,0 @@
var coarse = require('../../../sanitiser/coarse'),
_sanitize = coarse.sanitize,
middleware = coarse.middleware,
valid_layers = [ 'admin0', 'admin1', 'admin2', 'neighborhood', 'locality', 'local_admin' ],
defaultClean = require('../sanitiser/_input').defaultClean,
sanitize = function(query, cb) { _sanitize({'query':query}, cb); };
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('sanitize interface', function(t) {
t.equal(typeof sanitize, 'function', 'sanitize is a function');
t.equal(sanitize.length, 2, 'sanitize interface');
t.end();
});
test('middleware interface', function(t) {
t.equal(typeof middleware, 'function', 'middleware is a function');
t.equal(middleware.length, 3, 'sanitizee has a valid middleware');
t.end();
});
};
module.exports.tests.layers = function(test, common) {
test('valid layers', function(t) {
sanitize({ input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(err, undefined, 'no error');
t.deepEqual(clean.layers, valid_layers, 'layers set correctly');
});
t.end();
});
};
module.exports.tests.middleware_failure = function(test, common) {
test('middleware failure', function(t) {
var res = { status: function( code ){
t.equal(code, 400, 'status set');
}};
var next = function( message ){
var defaultError = 'invalid param \'input\': text length, must be >0';
t.equal(message, defaultError);
t.end();
};
middleware( {}, res, next );
});
};
module.exports.tests.middleware_success = function(test, common) {
test('middleware success', function(t) {
var req = { query: { input: 'test', lat: 0, lon: 0 }};
var clean = defaultClean;
clean.layers = valid_layers;
var next = function( message ){
t.equal(message, undefined, 'no error message set');
t.deepEqual(req.clean, clean);
t.end();
};
middleware( req, undefined, next );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SANTIZE /coarse ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};
Loading…
Cancel
Save