From f48239a7d4f7ea8e2caa46894e86484497c67e0a Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Fri, 12 Sep 2014 16:31:23 +0100 Subject: [PATCH 1/3] current dev branch --- .gitignore | 3 +- controller/suggest.js | 32 ++++++++++++++++++ express.js | 7 ++-- index.js | 5 ++- package.json | 4 ++- query/indeces.js | 12 +++++++ query/suggest.js | 31 +++++++++++++++++ sanitiser/suggest.js | 77 +++++++++++++++++++++++++++++++++++++++++++ src/backend.js | 21 ++++++++++++ src/logger.js | 5 +++ src/responder.js | 33 +++++++++++++++++++ 11 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 controller/suggest.js create mode 100644 query/indeces.js create mode 100644 query/suggest.js create mode 100644 sanitiser/suggest.js create mode 100644 src/backend.js create mode 100644 src/logger.js create mode 100644 src/responder.js diff --git a/.gitignore b/.gitignore index b512c09d..ab05030f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +*.log \ No newline at end of file diff --git a/controller/suggest.js b/controller/suggest.js new file mode 100644 index 00000000..f2a2230c --- /dev/null +++ b/controller/suggest.js @@ -0,0 +1,32 @@ + +var logger = require('../src/logger'), + responder = require('../src/responder'), + query = require('../query/suggest'), + backend = require('../src/backend'); + +module.exports = function( req, res, next ){ + + var reply = { + date: new Date().getTime(), + body: [] + }; + + var cmd = { + index: 'pelias', + body: query( req.clean ) // generate query from clean params + }; + + // Proxy request to ES backend & map response to a valid FeatureCollection + backend().client.suggest( cmd, function( err, data ){ + + if( err ){ return responder.error( req, res, next, err ); } + if( data && data.pelias && data.pelias.length ){ + + // map options to reply body + reply.body = data['pelias'][0].options; + } + + return responder.cors( req, res, reply ); + }); + +}; \ No newline at end of file diff --git a/express.js b/express.js index 10621442..b48e0560 100644 --- a/express.js +++ b/express.js @@ -1,6 +1,9 @@ -var express = require('express'); -var app = express(); +var express = require('express'), + app = express(); + +// middleware modules +// app.use( require('cookie-parser')() ); // enable client-side caching of 60s by default app.use(function(req, res, next){ diff --git a/index.js b/index.js index 9bfec708..15543369 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,9 @@ var app = require('./express'); // api root -app.get( '/', require('./controller/index' ) ); +app.get( '/', require('./controller/index') ); + +// suggest API +app.get( '/suggest', require('./sanitiser/suggest'), require('./controller/suggest') ); app.listen( process.env.PORT || 3100 ); \ No newline at end of file diff --git a/package.json b/package.json index 34ca0fb2..c9fb2fc1 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "elasticsearch": ">=1.2.1" }, "dependencies": { - "express": "^4.8.8" + "express": "^4.8.8", + "geopipes-elasticsearch-backend": "0.0.7", + "pelias-esclient": "0.0.25" }, "devDependencies": { "ciao": "^0.3.4", diff --git a/query/indeces.js b/query/indeces.js new file mode 100644 index 00000000..dbeacf43 --- /dev/null +++ b/query/indeces.js @@ -0,0 +1,12 @@ + +// querable indeces + +module.exports = [ + 'geoname', + 'osmnode', + 'osmway', + 'admin0', + 'admin1', + 'admin2', + 'neighborhood' +]; \ No newline at end of file diff --git a/query/suggest.js b/query/suggest.js new file mode 100644 index 00000000..b54e2518 --- /dev/null +++ b/query/suggest.js @@ -0,0 +1,31 @@ + +var logger = require('../src/logger'); + +// Build pelias suggest query +function generate( params ){ + + var cmd = { + 'pelias' : { + 'text' : params.input, + 'completion' : { + 'size' : params.size, + 'field' : 'suggest', + 'context': { + 'dataset': params.layers, + 'location': { + 'value': [ params.lon, params.lat ], + + // // commented out until they fix the precision bug in ES 1.3.3 + 'precision': 2 // params.zoom > 9 ? 3 : (params.zoom > 7 ? 2 : 1) + } + } + } + } + }; + + logger.log( 'cmd', JSON.stringify( cmd, null, 2 ) ); + return cmd; + +} + +module.exports = generate; \ No newline at end of file diff --git a/sanitiser/suggest.js b/sanitiser/suggest.js new file mode 100644 index 00000000..ea5296db --- /dev/null +++ b/sanitiser/suggest.js @@ -0,0 +1,77 @@ + +var logger = require('../src/logger'), + indeces = require('../query/indeces'); + +// validate inputs, convert types and apply defaults +function sanitize( params, cb ){ + + var clean = {}; + + // ensure the input params are a valid object + if( Object.prototype.toString.call( params ) !== '[object Object]' ){ + params = {}; + } + + // input text + if('string' !== typeof params.input || !params.input.length){ + return cb( 'invalid input text length, must be >0' ); + } + clean.input = params.input; + + // total results + var size = parseInt( params.size, 10 ); + if( !isNaN( size ) ){ + clean.size = Math.min( size, 40 ); // max + } else { + clean.size = 10; // default + } + + // which layers to query + if('string' === typeof params.layers && params.layers.length){ + var layers = params.layers.split(',').map( function( layer ){ + return layer.toLowerCase(); // lowercase inputs + }); + for( var x=0; x 90 ){ + return cb( 'invalid lat, must be >0 and <90' ); + } + clean.lat = lat; + + // lon + var lon = parseFloat( params.lon, 10 ); + if( isNaN( lon ) || lon < -180 || lon > 180 ){ + return cb( 'invalid lon, must be >-180 and <180' ); + } + clean.lon = lon; + + // zoom level + var zoom = parseInt( params.zoom, 10 ); + if( !isNaN( zoom ) ){ + clean.zoom = Math.min( zoom, 18 ); // max + } else { + clean.zoom = 10; // default + } + + return cb( undefined, clean ); + +} + +module.exports = function( req, res, next ){ + sanitize( req.query, function( err, clean ){ + if( err ){ next( err ); } + req.clean = clean; + next(); + }); +}; \ No newline at end of file diff --git a/src/backend.js b/src/backend.js new file mode 100644 index 00000000..2a9d60ae --- /dev/null +++ b/src/backend.js @@ -0,0 +1,21 @@ + +var Backend = require('geopipes-elasticsearch-backend'), + backends = {}, + client; + +// set env specific client +if( process.env.NODE_ENV === 'test' ){ + client = require('./pelias-mockclient'); +} else { + client = require('pelias-esclient')(); +} + +function getBackend( index, type ){ + var key = ( index + ':' + type ); + if( !backends[key] ){ + backends[key] = new Backend( client, index, type ); + } + return backends[key]; +} + +module.exports = getBackend; \ No newline at end of file diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 00000000..205313c0 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,5 @@ +module.exports = { + log: console.log.bind( console ), + warn: console.warn.bind( console ), + error: console.error.bind( console ) +}; \ No newline at end of file diff --git a/src/responder.js b/src/responder.js new file mode 100644 index 00000000..80f8d880 --- /dev/null +++ b/src/responder.js @@ -0,0 +1,33 @@ + +// send a reply that is capable of JSON, CORS and JSONP +function cors( req, res, obj ){ + res.header('Charset','utf8'); + res.header('Cache-Control','public,max-age=60'); + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET'); + res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + res.header('Access-Control-Allow-Credentials', true); + res.header('X-Powered-By', 'pelias'); + + // jsonp + if( req.query && req.query.callback ){ + res.header('Content-type','application/javascript'); + return res.send( req.query.callback + '('+ JSON.stringify( obj ) + ');' ); + } + + // regular json + res.header('Content-type','application/json'); + return res.json( obj ); +} + +// send an error +function error( req, res, next, err ){ + console.error( 'application error:', err ); + // mask error from user (contains paths) + return cors( req, res, { error: 'application error' } ); +} + +module.exports = { + cors: cors, + error: error +}; \ No newline at end of file From 4323b0030becbb33f1498149b74eb568565d575a Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Fri, 12 Sep 2014 19:14:03 +0100 Subject: [PATCH 2/3] progress commit --- controller/index.js | 5 +++- controller/suggest.js | 37 ++++++++++++----------- express.js | 14 --------- index.js | 50 +++++++++++++++++++++++++++++++- middleware/404.js | 8 +++++ middleware/500.js | 9 ++++++ package.json | 3 +- sanitiser/suggest.js | 14 +++++---- src/responder.js | 33 --------------------- test/ciao/404.coffee | 17 +++++++++++ test/ciao/cors.coffee | 9 ++++++ test/ciao/index.coffee | 6 ++++ test/ciao/jsonp.coffee | 10 +++++++ test/ciao/suggest/success.coffee | 15 ++++++++++ 14 files changed, 158 insertions(+), 72 deletions(-) delete mode 100644 express.js create mode 100644 middleware/404.js create mode 100644 middleware/500.js delete mode 100644 src/responder.js create mode 100644 test/ciao/404.coffee create mode 100644 test/ciao/cors.coffee create mode 100644 test/ciao/jsonp.coffee create mode 100644 test/ciao/suggest/success.coffee diff --git a/controller/index.js b/controller/index.js index c3267192..6936a982 100644 --- a/controller/index.js +++ b/controller/index.js @@ -1,13 +1,16 @@ var pkg = require('../package'); -function controller( req, res ){ +function controller( req, res, next ){ + + // stats res.json({ name: pkg.name, version: { number: pkg.version } }); + } module.exports = controller; \ No newline at end of file diff --git a/controller/suggest.js b/controller/suggest.js index f2a2230c..c6967186 100644 --- a/controller/suggest.js +++ b/controller/suggest.js @@ -1,32 +1,35 @@ -var logger = require('../src/logger'), - responder = require('../src/responder'), - query = require('../query/suggest'), +var query = require('../query/suggest'), backend = require('../src/backend'); -module.exports = function( req, res, next ){ - - var reply = { - date: new Date().getTime(), - body: [] - }; +function controller( req, res, next ){ + // backend command var cmd = { index: 'pelias', - body: query( req.clean ) // generate query from clean params + body: query( req.clean ) }; - // Proxy request to ES backend & map response to a valid FeatureCollection + // query backend backend().client.suggest( cmd, function( err, data ){ - if( err ){ return responder.error( req, res, next, err ); } - if( data && data.pelias && data.pelias.length ){ + var docs = []; - // map options to reply body - reply.body = data['pelias'][0].options; + // handle backend errors + if( err ){ return next( err ); } + + // map response to a valid FeatureCollection + if( data && Array.isArray( data.pelias ) && data.pelias.length ){ + docs = data['pelias'][0].options || []; } - return responder.cors( req, res, reply ); + // respond + return res.status(200).json({ + date: new Date().getTime(), + body: docs + }); }); -}; \ No newline at end of file +} + +module.exports = controller; \ No newline at end of file diff --git a/express.js b/express.js deleted file mode 100644 index b48e0560..00000000 --- a/express.js +++ /dev/null @@ -1,14 +0,0 @@ - -var express = require('express'), - app = express(); - -// middleware modules -// app.use( require('cookie-parser')() ); - -// enable client-side caching of 60s by default -app.use(function(req, res, next){ - res.header('Cache-Control','public,max-age=60'); - next(); -}); - -module.exports = app; \ No newline at end of file diff --git a/index.js b/index.js index 15543369..ea0ad29a 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,47 @@ -var app = require('./express'); +var app = require('express')(); + +/** ----------------------- middleware ----------------------- **/ + +// generic headers +app.use(function(req, res, next){ + res.header('Charset','utf8'); + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET'); + res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + res.header('Access-Control-Allow-Credentials', true); + res.header('X-Powered-By', 'pelias'); + next(); +}); + +// jsonp middleware +// override json() to handle jsonp +app.use(function(req, res, next){ + + res._json = res.json; + res.json = function( data ){ + + // jsonp + if( req.query && req.query.callback ){ + res.header('Content-type','application/javascript'); + return res.send( req.query.callback + '('+ JSON.stringify( data ) + ');' ); + } + + // regular json + res.header('Content-type','application/json'); + return res._json( data ); + }; + + next(); +}); + +// enable client-side caching of 60s by default +app.use(function(req, res, next){ + res.header('Cache-Control','public,max-age=60'); + next(); +}); + +/** ----------------------- Routes ----------------------- **/ // api root app.get( '/', require('./controller/index') ); @@ -7,4 +49,10 @@ app.get( '/', require('./controller/index') ); // suggest API app.get( '/suggest', require('./sanitiser/suggest'), require('./controller/suggest') ); +/** ----------------------- error middleware ----------------------- **/ + +// handle application errors +app.use( require('./middleware/404') ); +app.use( require('./middleware/500') ); + app.listen( process.env.PORT || 3100 ); \ No newline at end of file diff --git a/middleware/404.js b/middleware/404.js new file mode 100644 index 00000000..e90ed3c3 --- /dev/null +++ b/middleware/404.js @@ -0,0 +1,8 @@ + +// handle not found errors +function middleware(req, res) { + res.header('Cache-Control','public,max-age=300'); // 5 minute cache + res.status(404).json({ error: 'not found: invalid path' }); +} + +module.exports = middleware; \ No newline at end of file diff --git a/middleware/500.js b/middleware/500.js new file mode 100644 index 00000000..d73c5684 --- /dev/null +++ b/middleware/500.js @@ -0,0 +1,9 @@ + +// handle application errors +function middleware(err, req, res, next) { + res.header('Cache-Control','no-cache'); + if( res.statusCode < 400 ){ res.status(500); } + res.json({ error: err }); +} + +module.exports = middleware; \ No newline at end of file diff --git a/package.json b/package.json index c9fb2fc1..559e7aed 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "node index.js", "test": "npm run unit && npm run ciao", "unit": "node test/unit/run.js | tap-spec", - "ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao" + "ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao", + "docs": "cd test/ciao; node ../../node_modules/ciao/bin/ciao -c ../ciao.json . -d ../../docs" }, "repository": { "type": "git", diff --git a/sanitiser/suggest.js b/sanitiser/suggest.js index ea5296db..d7dfa60a 100644 --- a/sanitiser/suggest.js +++ b/sanitiser/suggest.js @@ -14,7 +14,7 @@ function sanitize( params, cb ){ // input text if('string' !== typeof params.input || !params.input.length){ - return cb( 'invalid input text length, must be >0' ); + return cb( 'invalid param \'input\': text length, must be >0' ); } clean.input = params.input; @@ -33,7 +33,7 @@ function sanitize( params, cb ){ }); for( var x=0; x 90 ){ - return cb( 'invalid lat, must be >0 and <90' ); + return cb( 'invalid param \'lat\': must be >0 and <90' ); } clean.lat = lat; // lon var lon = parseFloat( params.lon, 10 ); if( isNaN( lon ) || lon < -180 || lon > 180 ){ - return cb( 'invalid lon, must be >-180 and <180' ); + return cb( 'invalid param \'lon\': must be >-180 and <180' ); } clean.lon = lon; @@ -68,9 +68,13 @@ function sanitize( params, cb ){ } +// middleware module.exports = function( req, res, next ){ sanitize( req.query, function( err, clean ){ - if( err ){ next( err ); } + if( err ){ + res.status(400); // 400 Bad Request + return next(err); + } req.clean = clean; next(); }); diff --git a/src/responder.js b/src/responder.js deleted file mode 100644 index 80f8d880..00000000 --- a/src/responder.js +++ /dev/null @@ -1,33 +0,0 @@ - -// send a reply that is capable of JSON, CORS and JSONP -function cors( req, res, obj ){ - res.header('Charset','utf8'); - res.header('Cache-Control','public,max-age=60'); - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET'); - res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); - res.header('Access-Control-Allow-Credentials', true); - res.header('X-Powered-By', 'pelias'); - - // jsonp - if( req.query && req.query.callback ){ - res.header('Content-type','application/javascript'); - return res.send( req.query.callback + '('+ JSON.stringify( obj ) + ');' ); - } - - // regular json - res.header('Content-type','application/json'); - return res.json( obj ); -} - -// send an error -function error( req, res, next, err ){ - console.error( 'application error:', err ); - // mask error from user (contains paths) - return cors( req, res, { error: 'application error' } ); -} - -module.exports = { - cors: cors, - error: error -}; \ No newline at end of file diff --git a/test/ciao/404.coffee b/test/ciao/404.coffee new file mode 100644 index 00000000..764bc8d5 --- /dev/null +++ b/test/ciao/404.coffee @@ -0,0 +1,17 @@ + +#> invalid path +path: '/notexist' + +#? not found +response.statusCode.should.equal 404 + +#? content-type header correctly set +response.should.have.header 'Content-Type','application/json; charset=utf-8' + +#? cache-control header correctly set +response.should.have.header 'Cache-Control','public,max-age=300' + +#? should respond in json with server info +should.exist json +should.exist json.error +json.error.should.equal 'not found: invalid path' \ No newline at end of file diff --git a/test/ciao/cors.coffee b/test/ciao/cors.coffee new file mode 100644 index 00000000..bd97c5de --- /dev/null +++ b/test/ciao/cors.coffee @@ -0,0 +1,9 @@ + +#> cross-origin resource sharing +path: '/' + +#? access control headers correctly set +response.should.have.header 'Access-Control-Allow-Origin','*' +response.should.have.header 'Access-Control-Allow-Methods','GET' +response.should.have.header 'Access-Control-Allow-Headers','X-Requested-With,content-type' +response.should.have.header 'Access-Control-Allow-Credentials','true' \ No newline at end of file diff --git a/test/ciao/index.coffee b/test/ciao/index.coffee index 960f6551..2a9dba88 100644 --- a/test/ciao/index.coffee +++ b/test/ciao/index.coffee @@ -8,9 +8,15 @@ response.statusCode.should.equal 200 #? content-type header correctly set response.should.have.header 'Content-Type','application/json; charset=utf-8' +#? charset header correctly set +response.should.have.header 'Charset','utf8' + #? cache-control header correctly set response.should.have.header 'Cache-Control','public,max-age=60' +#? vanity header correctly set +response.should.have.header 'X-Powered-By','pelias' + #? should respond in json with server info should.exist json should.exist json.name diff --git a/test/ciao/jsonp.coffee b/test/ciao/jsonp.coffee new file mode 100644 index 00000000..d83f0eaa --- /dev/null +++ b/test/ciao/jsonp.coffee @@ -0,0 +1,10 @@ + +#> jsonp +path: '/?callback=test' + +#? content-type header correctly set +response.should.have.header 'Content-Type','application/javascript; charset=utf-8' + +#? should respond with jsonp +should.exist response.body +response.body.substr(0,5).should.equal 'test('; \ No newline at end of file diff --git a/test/ciao/suggest/success.coffee b/test/ciao/suggest/success.coffee new file mode 100644 index 00000000..b5a58c44 --- /dev/null +++ b/test/ciao/suggest/success.coffee @@ -0,0 +1,15 @@ + +#> valid suggest query +path: '/suggest?input=a&lat=0&lon=0' + +#? 200 ok +response.statusCode.should.equal 200 + +#? valid response +now = new Date().getTime() +should.exist json +should.not.exist json.error +should.exist json.date +json.date.should.be.within now-1000, now+1000 +should.exist json.body +json.body.should.be.instanceof Array \ No newline at end of file From 05fba480e9cb08b861f2ae02a707d8741ef6c4d2 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Fri, 12 Sep 2014 19:14:33 +0100 Subject: [PATCH 3/3] add docs --- docs/404.md | 65 ++++++++++++++++++ docs/cors.md | 54 +++++++++++++++ docs/index.md | 78 ++++++++++++++++++++++ docs/jsonp.md | 52 +++++++++++++++ docs/suggest/success.md | 141 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 390 insertions(+) create mode 100644 docs/404.md create mode 100644 docs/cors.md create mode 100644 docs/index.md create mode 100644 docs/jsonp.md create mode 100644 docs/suggest/success.md diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 00000000..4694f1a2 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,65 @@ +# invalid path + +*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/notexist", + "headers": { + "User-Agent": "Ciao/Client 1.0" + } +} +``` + +## Response +```javascript +Status: 404 +{ + "x-powered-by": "pelias", + "charset": "utf8", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "cache-control": "public,max-age=300", + "content-type": "application/json; charset=utf-8", + "content-length": "35", + "etag": "W/\"23-dfdfa185\"", + "date": "Fri, 12 Sep 2014 18:14:09 GMT", + "connection": "close" +} +``` +```javascript +{ + "error": "not found: invalid path" +} +``` + +## Tests + +### ✓ content-type header correctly set +```javascript +response.should.have.header 'Content-Type','application/json; charset=utf-8' +``` + +### ✓ should respond in json with server info +```javascript +should.exist json +should.exist json.error +json.error.should.equal 'not found: invalid path' +``` + +### ✓ cache-control header correctly set +```javascript +response.should.have.header 'Cache-Control','public,max-age=300' +``` + +### ✓ not found +```javascript +response.statusCode.should.equal 404 +``` + diff --git a/docs/cors.md b/docs/cors.md new file mode 100644 index 00000000..77d10ea7 --- /dev/null +++ b/docs/cors.md @@ -0,0 +1,54 @@ +# cross-origin resource sharing + +*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/", + "headers": { + "User-Agent": "Ciao/Client 1.0" + } +} +``` + +## Response +```javascript +Status: 200 +{ + "x-powered-by": "pelias", + "charset": "utf8", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "cache-control": "public,max-age=60", + "content-type": "application/json; charset=utf-8", + "content-length": "50", + "etag": "W/\"32-85536434\"", + "date": "Fri, 12 Sep 2014 18:14:09 GMT", + "connection": "close" +} +``` +```javascript +{ + "name": "pelias-api", + "version": { + "number": "0.0.0" + } +} +``` + +## Tests + +### ✓ access control headers correctly set +```javascript +response.should.have.header 'Access-Control-Allow-Origin','*' +response.should.have.header 'Access-Control-Allow-Methods','GET' +response.should.have.header 'Access-Control-Allow-Headers','X-Requested-With,content-type' +response.should.have.header 'Access-Control-Allow-Credentials','true' +``` + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..917f36f8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,78 @@ +# api root + +*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/", + "headers": { + "User-Agent": "Ciao/Client 1.0" + } +} +``` + +## Response +```javascript +Status: 200 +{ + "x-powered-by": "pelias", + "charset": "utf8", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "cache-control": "public,max-age=60", + "content-type": "application/json; charset=utf-8", + "content-length": "50", + "etag": "W/\"32-85536434\"", + "date": "Fri, 12 Sep 2014 18:14:09 GMT", + "connection": "close" +} +``` +```javascript +{ + "name": "pelias-api", + "version": { + "number": "0.0.0" + } +} +``` + +## Tests + +### ✓ endpoint available +```javascript +response.statusCode.should.equal 200 +``` + +### ✓ vanity header correctly set +```javascript +response.should.have.header 'X-Powered-By','pelias' +``` + +### ✓ cache-control header correctly set +```javascript +response.should.have.header 'Cache-Control','public,max-age=60' +``` + +### ✓ should respond in json with server info +```javascript +should.exist json +should.exist json.name +should.exist json.version +``` + +### ✓ content-type header correctly set +```javascript +response.should.have.header 'Content-Type','application/json; charset=utf-8' +``` + +### ✓ charset header correctly set +```javascript +response.should.have.header 'Charset','utf8' +``` + diff --git a/docs/jsonp.md b/docs/jsonp.md new file mode 100644 index 00000000..861785cb --- /dev/null +++ b/docs/jsonp.md @@ -0,0 +1,52 @@ +# jsonp + +*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/?callback=test", + "headers": { + "User-Agent": "Ciao/Client 1.0" + } +} +``` + +## Response +```javascript +Status: 200 +{ + "x-powered-by": "pelias", + "charset": "utf8", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "cache-control": "public,max-age=60", + "content-type": "application/javascript; charset=utf-8", + "content-length": "57", + "etag": "W/\"39-b8a2aba1\"", + "date": "Fri, 12 Sep 2014 18:14:09 GMT", + "connection": "close" +} +``` +```html +test({"name":"pelias-api","version":{"number":"0.0.0"}}); +``` + +## Tests + +### ✓ content-type header correctly set +```javascript +response.should.have.header 'Content-Type','application/javascript; charset=utf-8' +``` + +### ✓ should respond with jsonp +```javascript +should.exist response.body +response.body.substr(0,5).should.equal 'test('; +``` + diff --git a/docs/suggest/success.md b/docs/suggest/success.md new file mode 100644 index 00000000..0019f600 --- /dev/null +++ b/docs/suggest/success.md @@ -0,0 +1,141 @@ +# valid suggest query + +*Generated: Fri Sep 12 2014 19:14:09 GMT+0100 (BST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/suggest?input=a&lat=0&lon=0", + "headers": { + "User-Agent": "Ciao/Client 1.0" + } +} +``` + +## Response +```javascript +Status: 200 +{ + "x-powered-by": "pelias", + "charset": "utf8", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "cache-control": "public,max-age=60", + "content-type": "application/json; charset=utf-8", + "content-length": "1248", + "etag": "W/\"jtfnMCXDw5frK6L5eD1thg==\"", + "date": "Fri, 12 Sep 2014 18:14:09 GMT", + "connection": "close" +} +``` +```javascript +{ + "date": 1410545649156, + "body": [ + { + "text": "ACRELÂNDIA, Brazil", + "score": 1, + "payload": { + "id": "admin2/708:adm2:br:bra:acrel__ndia", + "geo": "-66.908143,-9.954353" + } + }, + { + "text": "ALTA FLORESTA, Brazil", + "score": 1, + "payload": { + "id": "admin2/2986:adm2:br:bra:alta_floresta", + "geo": "-56.404593,-10.042071" + } + }, + { + "text": "ALTO ALEGRE, Brazil", + "score": 1, + "payload": { + "id": "admin2/4611:adm2:br:bra:alto_alegre", + "geo": "-62.627879,3.103540" + } + }, + { + "text": "ALTO PARAÍSO, Brazil", + "score": 1, + "payload": { + "id": "admin2/4584:adm2:br:bra:alto_para__so", + "geo": "-63.418743,-9.697774" + } + }, + { + "text": "ALVARÃES, Brazil", + "score": 1, + "payload": { + "id": "admin2/832:adm2:br:bra:alvar__es", + "geo": "-65.296384,-3.674615" + } + }, + { + "text": "AMAJARI, Brazil", + "score": 1, + "payload": { + "id": "admin2/4610:adm2:br:bra:amajari", + "geo": "-62.710104,3.724864" + } + }, + { + "text": "AMAZONAS, Brazil", + "score": 1, + "payload": { + "id": "admin1/3232:adm1:br:bra:amazonas", + "geo": "-64.949558,-3.785708" + } + }, + { + "text": "ANAMÃ, Brazil", + "score": 1, + "payload": { + "id": "admin2/834:adm2:br:bra:anam__", + "geo": "-61.683670,-3.473836" + } + }, + { + "text": "ANORI, Brazil", + "score": 1, + "payload": { + "id": "admin2/835:adm2:br:bra:anori", + "geo": "-62.182138,-4.154809" + } + }, + { + "text": "APIACÁS, Brazil", + "score": 1, + "payload": { + "id": "admin2/2992:adm2:br:bra:apiac__s", + "geo": "-57.803447,-8.583036" + } + } + ] +} +``` + +## Tests + +### ✓ 200 ok +```javascript +response.statusCode.should.equal 200 +``` + +### ✓ valid response +```javascript +now = new Date().getTime() +should.exist json +should.not.exist json.error +should.exist json.date +json.date.should.be.within now-1000, now+1000 +should.exist json.body +json.body.should.be.instanceof Array +``` +