mirror of https://github.com/pelias/api.git
Stephen Hess
7 years ago
9 changed files with 945 additions and 176 deletions
@ -0,0 +1,75 @@ |
|||||||
|
const _ = require('lodash'); |
||||||
|
const Debug = require('../helper/debug'); |
||||||
|
const debugLog = new Debug('controller:libpostal'); |
||||||
|
const logger = require('pelias-logger').get('api'); |
||||||
|
|
||||||
|
// if there's a house_number in the libpostal response, return it
|
||||||
|
// otherwise return the postcode field (which may be undefined)
|
||||||
|
function findHouseNumberField(response) { |
||||||
|
const house_number_field = response.find(f => f.label === 'house_number'); |
||||||
|
|
||||||
|
if (house_number_field) { |
||||||
|
return house_number_field; |
||||||
|
} |
||||||
|
|
||||||
|
return response.find(f => f.label === 'postcode'); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
function setup(libpostalService, should_execute) { |
||||||
|
function controller( req, res, next ){ |
||||||
|
// bail early if req/res don't pass conditions for execution
|
||||||
|
if (!should_execute(req, res)) { |
||||||
|
return next(); |
||||||
|
} |
||||||
|
|
||||||
|
const initialTime = debugLog.beginTimer(req); |
||||||
|
|
||||||
|
libpostalService(req, (err, response) => { |
||||||
|
if (err) { |
||||||
|
// push err.message or err onto req.errors
|
||||||
|
req.errors.push( _.get(err, 'message', err) ); |
||||||
|
|
||||||
|
} else { |
||||||
|
// figure out which field contains the probable house number, prefer house_number
|
||||||
|
// libpostal parses some inputs, like `3370 cobbe ave`, as a postcode+street
|
||||||
|
// so because we're treating the entire field as a street address, it's safe
|
||||||
|
// to assume that an identified postcode is actually a house number.
|
||||||
|
const house_number_field = findHouseNumberField(response); |
||||||
|
|
||||||
|
// if we're fairly certain that libpostal identified a house number
|
||||||
|
// (from either the house_number or postcode field), place it into the
|
||||||
|
// number field and remove the first instance of that value from address
|
||||||
|
// and assign to street
|
||||||
|
// eg - '1090 N Charlotte St' becomes number=1090 and street=N Charlotte St
|
||||||
|
if (house_number_field) { |
||||||
|
req.clean.parsed_text.number = house_number_field.value; |
||||||
|
|
||||||
|
// remove the first instance of the number and trim whitespace
|
||||||
|
req.clean.parsed_text.street = _.trim(_.replace(req.clean.parsed_text.address, req.clean.parsed_text.number, '')); |
||||||
|
|
||||||
|
} else { |
||||||
|
// otherwise no house number was identifiable, so treat the entire input
|
||||||
|
// as a street
|
||||||
|
req.clean.parsed_text.street = req.clean.parsed_text.address; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// the address field no longer means anything since it's been parsed, so remove it
|
||||||
|
delete req.clean.parsed_text.address; |
||||||
|
|
||||||
|
debugLog.push(req, {parsed_text: response}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
debugLog.stopTimer(req, initialTime); |
||||||
|
return next(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return controller; |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = setup; |
@ -0,0 +1,33 @@ |
|||||||
|
'use strict'; |
||||||
|
|
||||||
|
const url = require('url'); |
||||||
|
|
||||||
|
const ServiceConfiguration = require('pelias-microservice-wrapper').ServiceConfiguration; |
||||||
|
|
||||||
|
class Libpostal extends ServiceConfiguration { |
||||||
|
constructor(o, propertyExtractor) { |
||||||
|
super('libpostal', o); |
||||||
|
|
||||||
|
// save off the propertyExtractor function
|
||||||
|
// this is used to extract a single property from req. eg:
|
||||||
|
// * _.property('clean.text')
|
||||||
|
// * _.property('clean.parsed_text.address')
|
||||||
|
// will return those properties from req
|
||||||
|
this.propertyExtractor = propertyExtractor; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
getParameters(req) { |
||||||
|
return { |
||||||
|
address: this.propertyExtractor(req) |
||||||
|
}; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
getUrl(req) { |
||||||
|
return url.resolve(this.baseUrl, 'parse'); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = Libpostal; |
@ -0,0 +1,440 @@ |
|||||||
|
'use strict'; |
||||||
|
|
||||||
|
const proxyquire = require('proxyquire').noCallThru(); |
||||||
|
const libpostal = require('../../../controller/structured_libpostal'); |
||||||
|
const _ = require('lodash'); |
||||||
|
const mock_logger = require('pelias-mock-logger'); |
||||||
|
|
||||||
|
module.exports.tests = {}; |
||||||
|
|
||||||
|
module.exports.tests.interface = (test, common) => { |
||||||
|
test('valid interface', (t) => { |
||||||
|
t.equal(typeof libpostal, 'function', 'libpostal is a function'); |
||||||
|
t.equal(typeof libpostal(), 'function', 'libpostal returns a controller'); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.tests.early_exit_conditions = (test, common) => { |
||||||
|
test('should_execute returning false should not call service', t => { |
||||||
|
const service = () => { |
||||||
|
t.fail('service should not have been called'); |
||||||
|
}; |
||||||
|
|
||||||
|
const should_execute = (req) => { |
||||||
|
// req and res should be passed to should_execute
|
||||||
|
t.deepEquals(req, { |
||||||
|
clean: { |
||||||
|
text: 'original query' |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return false; |
||||||
|
}; |
||||||
|
|
||||||
|
const controller = libpostal(service, should_execute); |
||||||
|
|
||||||
|
const req = { |
||||||
|
clean: { |
||||||
|
text: 'original query' |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
controller(req, undefined, () => { |
||||||
|
t.deepEquals(req, { |
||||||
|
clean: { |
||||||
|
text: 'original query' |
||||||
|
} |
||||||
|
}, 'req should not have been modified'); |
||||||
|
|
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.tests.error_conditions = (test, common) => { |
||||||
|
test('service returning error should append and not modify req.clean', t => { |
||||||
|
const service = (req, callback) => { |
||||||
|
callback('libpostal service error', []); |
||||||
|
}; |
||||||
|
|
||||||
|
const controller = libpostal(service, () => true); |
||||||
|
|
||||||
|
const req = { |
||||||
|
clean: { |
||||||
|
text: 'original query' |
||||||
|
}, |
||||||
|
errors: [] |
||||||
|
}; |
||||||
|
|
||||||
|
controller(req, undefined, () => { |
||||||
|
t.deepEquals(req, { |
||||||
|
clean: { |
||||||
|
text: 'original query' |
||||||
|
}, |
||||||
|
errors: ['libpostal service error'] |
||||||
|
}, 'req should not have been modified'); |
||||||
|
|
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
// module.exports.tests.failure_conditions = (test, common) => {
|
||||||
|
// test('service returning 2 or more of a label should return undefined and log message', t => {
|
||||||
|
// const logger = mock_logger();
|
||||||
|
//
|
||||||
|
// const service = (req, callback) => {
|
||||||
|
// const response = [
|
||||||
|
// {
|
||||||
|
// label: 'road',
|
||||||
|
// value: 'road value 1'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'city',
|
||||||
|
// value: 'city value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'road',
|
||||||
|
// value: 'road value 2'
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// callback(null, response);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const controller = proxyquire('../../../controller/libpostal', {
|
||||||
|
// 'pelias-logger': logger
|
||||||
|
// })(service, () => true);
|
||||||
|
//
|
||||||
|
// const req = {
|
||||||
|
// clean: {
|
||||||
|
// text: 'query value'
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// controller(req, undefined, () => {
|
||||||
|
// t.ok(logger.isWarnMessage('discarding libpostal parse of \'query value\' due to duplicate field assignments'));
|
||||||
|
//
|
||||||
|
// t.deepEquals(req, {
|
||||||
|
// clean: {
|
||||||
|
// text: 'query value'
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// }, 'req should not have been modified');
|
||||||
|
//
|
||||||
|
// t.end();
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test('service returning empty array should not set parsed_text or parser', t => {
|
||||||
|
// const logger = mock_logger();
|
||||||
|
//
|
||||||
|
// const service = (req, callback) => {
|
||||||
|
// callback(null, []);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const controller = proxyquire('../../../controller/libpostal', {
|
||||||
|
// 'pelias-logger': logger
|
||||||
|
// })(service, () => true);
|
||||||
|
//
|
||||||
|
// const req = {
|
||||||
|
// clean: {
|
||||||
|
// text: 'query value'
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// controller(req, undefined, () => {
|
||||||
|
// t.deepEquals(req, {
|
||||||
|
// clean: {
|
||||||
|
// text: 'query value'
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// }, 'req should not have been modified');
|
||||||
|
//
|
||||||
|
// t.end();
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
module.exports.tests.success_conditions = (test, common) => { |
||||||
|
test('service returning house_number should set req.clean.parsed_text.', t => { |
||||||
|
const service = (req, callback) => { |
||||||
|
const response = [ |
||||||
|
{ |
||||||
|
label: 'house_number', |
||||||
|
value: 'house_number value' |
||||||
|
}, |
||||||
|
{ |
||||||
|
label: 'postcode', |
||||||
|
value: 'postcode value' |
||||||
|
} |
||||||
|
]; |
||||||
|
|
||||||
|
callback(null, response); |
||||||
|
}; |
||||||
|
|
||||||
|
const controller = libpostal(service, () => true); |
||||||
|
|
||||||
|
const req = { |
||||||
|
clean: { |
||||||
|
parsed_text: { |
||||||
|
address: 'other value house_number value street value' |
||||||
|
} |
||||||
|
}, |
||||||
|
errors: [] |
||||||
|
}; |
||||||
|
|
||||||
|
controller(req, undefined, () => { |
||||||
|
t.deepEquals(req, { |
||||||
|
clean: { |
||||||
|
parsed_text: { |
||||||
|
number: 'house_number value', |
||||||
|
street: 'other value street value' |
||||||
|
} |
||||||
|
}, |
||||||
|
errors: [] |
||||||
|
}, 'req should not have been modified'); |
||||||
|
|
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
test('service returning postcode should set req.clean.parsed_text.', t => { |
||||||
|
const service = (req, callback) => { |
||||||
|
const response = [ |
||||||
|
{ |
||||||
|
label: 'postcode', |
||||||
|
value: 'postcode value' |
||||||
|
} |
||||||
|
]; |
||||||
|
|
||||||
|
callback(null, response); |
||||||
|
}; |
||||||
|
|
||||||
|
const controller = libpostal(service, () => true); |
||||||
|
|
||||||
|
const req = { |
||||||
|
clean: { |
||||||
|
parsed_text: { |
||||||
|
address: 'other value postcode value street value' |
||||||
|
} |
||||||
|
}, |
||||||
|
errors: [] |
||||||
|
}; |
||||||
|
|
||||||
|
controller(req, undefined, () => { |
||||||
|
t.deepEquals(req, { |
||||||
|
clean: { |
||||||
|
parsed_text: { |
||||||
|
number: 'postcode value', |
||||||
|
street: 'other value street value' |
||||||
|
} |
||||||
|
}, |
||||||
|
errors: [] |
||||||
|
}, 'req should not have been modified'); |
||||||
|
|
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
test('service returning neither house_number nor postcode should not set req.clean.parsed_text.number', t => { |
||||||
|
const service = (req, callback) => { |
||||||
|
const response = [ |
||||||
|
{ |
||||||
|
label: 'city', |
||||||
|
value: 'city value' |
||||||
|
} |
||||||
|
]; |
||||||
|
|
||||||
|
callback(null, response); |
||||||
|
}; |
||||||
|
|
||||||
|
const controller = libpostal(service, () => true); |
||||||
|
|
||||||
|
const req = { |
||||||
|
clean: { |
||||||
|
parsed_text: { |
||||||
|
address: 'street value' |
||||||
|
} |
||||||
|
}, |
||||||
|
errors: [] |
||||||
|
}; |
||||||
|
|
||||||
|
controller(req, undefined, () => { |
||||||
|
t.deepEquals(req, { |
||||||
|
clean: { |
||||||
|
parsed_text: { |
||||||
|
street: 'street value' |
||||||
|
} |
||||||
|
}, |
||||||
|
errors: [] |
||||||
|
}, 'req should not have been modified'); |
||||||
|
|
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
// test('service returning valid response should convert and append', t => {
|
||||||
|
// const service = (req, callback) => {
|
||||||
|
// const response = [
|
||||||
|
// {
|
||||||
|
// label: 'island',
|
||||||
|
// value: 'island value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'category',
|
||||||
|
// value: 'category value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'house',
|
||||||
|
// value: 'house value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'house_number',
|
||||||
|
// value: 'house_number value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'road',
|
||||||
|
// value: 'road value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'suburb',
|
||||||
|
// value: 'suburb value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'city_district',
|
||||||
|
// value: 'city_district value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'city',
|
||||||
|
// value: 'city value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'state_district',
|
||||||
|
// value: 'state_district value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'state',
|
||||||
|
// value: 'state value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'postcode',
|
||||||
|
// value: 'postcode value'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'country',
|
||||||
|
// value: 'country value'
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// callback(null, response);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const controller = libpostal(service, () => true);
|
||||||
|
//
|
||||||
|
// const req = {
|
||||||
|
// clean: {
|
||||||
|
// text: 'original query'
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// controller(req, undefined, () => {
|
||||||
|
// t.deepEquals(req, {
|
||||||
|
// clean: {
|
||||||
|
// text: 'original query',
|
||||||
|
// parser: 'libpostal',
|
||||||
|
// parsed_text: {
|
||||||
|
// island: 'island value',
|
||||||
|
// category: 'category value',
|
||||||
|
// query: 'house value',
|
||||||
|
// number: 'house_number value',
|
||||||
|
// street: 'road value',
|
||||||
|
// neighbourhood: 'suburb value',
|
||||||
|
// borough: 'city_district value',
|
||||||
|
// city: 'city value',
|
||||||
|
// county: 'state_district value',
|
||||||
|
// state: 'state value',
|
||||||
|
// postalcode: 'postcode value',
|
||||||
|
// country: 'country value'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// }, 'req should not have been modified');
|
||||||
|
//
|
||||||
|
// t.end();
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// test('ISO-2 country should be converted to ISO-3', t => {
|
||||||
|
// const service = (req, callback) => {
|
||||||
|
// const response = [
|
||||||
|
// {
|
||||||
|
// label: 'country',
|
||||||
|
// value: 'ca'
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// callback(null, response);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const controller = libpostal(service, () => true);
|
||||||
|
//
|
||||||
|
// const req = {
|
||||||
|
// clean: {
|
||||||
|
// text: 'original query'
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// controller(req, undefined, () => {
|
||||||
|
// t.deepEquals(req, {
|
||||||
|
// clean: {
|
||||||
|
// text: 'original query',
|
||||||
|
// parser: 'libpostal',
|
||||||
|
// parsed_text: {
|
||||||
|
// country: 'CAN'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// errors: []
|
||||||
|
// }, 'req should not have been modified');
|
||||||
|
//
|
||||||
|
// t.end();
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.all = (tape, common) => { |
||||||
|
|
||||||
|
function test(name, testFunction) { |
||||||
|
return tape(`GET /libpostal ${name}`, testFunction); |
||||||
|
} |
||||||
|
|
||||||
|
for( const testCase in module.exports.tests ){ |
||||||
|
module.exports.tests[testCase](test, common); |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,99 @@ |
|||||||
|
const Libpostal = require('../../../../service/configurations/Libpostal'); |
||||||
|
|
||||||
|
module.exports.tests = {}; |
||||||
|
|
||||||
|
module.exports.tests.all = (test, common) => { |
||||||
|
test('getName should return \'libpostal\'', (t) => { |
||||||
|
const configBlob = { |
||||||
|
url: 'http://localhost:1234', |
||||||
|
timeout: 17, |
||||||
|
retries: 19 |
||||||
|
}; |
||||||
|
|
||||||
|
const libpostal = new Libpostal(configBlob); |
||||||
|
|
||||||
|
t.equals(libpostal.getName(), 'libpostal'); |
||||||
|
t.equals(libpostal.getBaseUrl(), 'http://localhost:1234/'); |
||||||
|
t.equals(libpostal.getTimeout(), 17); |
||||||
|
t.equals(libpostal.getRetries(), 19); |
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
test('getUrl should return value passed to constructor', (t) => { |
||||||
|
const configBlob = { |
||||||
|
url: 'http://localhost:1234', |
||||||
|
timeout: 17, |
||||||
|
retries: 19 |
||||||
|
}; |
||||||
|
|
||||||
|
const libpostal = new Libpostal(configBlob); |
||||||
|
|
||||||
|
t.equals(libpostal.getUrl(), 'http://localhost:1234/parse'); |
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
test('getParameters should return object with text and lang from req', (t) => { |
||||||
|
const configBlob = { |
||||||
|
url: 'http://localhost:1234', |
||||||
|
timeout: 17, |
||||||
|
retries: 19 |
||||||
|
}; |
||||||
|
|
||||||
|
const propertyExtractor = (req) => { |
||||||
|
t.deepEquals(req, { a: 1, b: 2}); |
||||||
|
return 'property value'; |
||||||
|
}; |
||||||
|
|
||||||
|
const libpostal = new Libpostal(configBlob, propertyExtractor); |
||||||
|
|
||||||
|
const req = { |
||||||
|
a: 1, |
||||||
|
b: 2 |
||||||
|
}; |
||||||
|
|
||||||
|
t.deepEquals(libpostal.getParameters(req), { address: 'property value' }); |
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
test('getHeaders should return empty object', (t) => { |
||||||
|
const configBlob = { |
||||||
|
url: 'base url', |
||||||
|
timeout: 17, |
||||||
|
retries: 19 |
||||||
|
}; |
||||||
|
|
||||||
|
const libpostal = new Libpostal(configBlob); |
||||||
|
|
||||||
|
t.deepEquals(libpostal.getHeaders(), {}); |
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
test('baseUrl ending in / should not have double /\'s return by getUrl', (t) => { |
||||||
|
const configBlob = { |
||||||
|
url: 'http://localhost:1234/', |
||||||
|
timeout: 17, |
||||||
|
retries: 19 |
||||||
|
}; |
||||||
|
|
||||||
|
const libpostal = new Libpostal(configBlob); |
||||||
|
|
||||||
|
t.deepEquals(libpostal.getUrl(), 'http://localhost:1234/parse'); |
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.all = (tape, common) => { |
||||||
|
function test(name, testFunction) { |
||||||
|
return tape(`SERVICE CONFIGURATION /Libpostal ${name}`, testFunction); |
||||||
|
} |
||||||
|
|
||||||
|
for( var testCase in module.exports.tests ){ |
||||||
|
module.exports.tests[testCase](test, common); |
||||||
|
} |
||||||
|
}; |
Loading…
Reference in new issue