Skip to content

Commit

Permalink
feat: 🎸 initial working implementation of getAutocomplete()
Browse files Browse the repository at this point in the history
This change introduces the getAutocomplete() operation and a
listCompletions() method. The listCompletions() method can be used to
produce a list of options that can be easily used to refine further
getAutocomplete() calls.

✅ Closes: #43
  • Loading branch information
davidkelley committed Apr 10, 2020
1 parent d8c982d commit 4775b42
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 0 deletions.
45 changes: 45 additions & 0 deletions lib/operations/getAutocomplete.js
@@ -0,0 +1,45 @@
const { listCompletions } = require('./methods');
const { ParameterValidationError } = require('./errors');

const OPERATION_URL = 'https://docs.firstclasspostcodes.com/operation/getAutocomplete';

const OPERATION_PATH = '/autocomplete';

module.exports = {
async getAutocomplete(search) {
let errorObj;

if (!search || typeof search !== 'string' || search.length === 0) {
errorObj = {
message: `Unexpected search parameter: "${search}"`,
docUrl: OPERATION_URL,
};
}

const params = {
path: OPERATION_PATH,
qs: {
search,
},
};

this.debug(`Executing operation getAutocomplete (${OPERATION_PATH}): %o`, params);

this.emit('operation:getAutocomplete', params);

if (errorObj) {
const error = new ParameterValidationError(errorObj);
this.debug('Encountered ParameterValidationError: %o', errorObj);
this.emit('error', error);
throw error;
}

const responseObject = await this.request(params);

const isCompleted = responseObject.length === 1 && responseObject[0][1].length <= 1;

Object.assign(responseObject, { isCompleted, listCompletions });

return responseObject;
},
};
117 changes: 117 additions & 0 deletions lib/operations/getAutocomplete.test.js
@@ -0,0 +1,117 @@
const callable = require('./getAutocomplete');
const { ParameterValidationError } = require('./errors');

describe('#getAutocomplete', () => {
let testClass;

beforeEach(() => {
const TestClass = class {};

const classMethods = {
emit: jest.fn(),
debug: jest.fn(),
request: jest.fn(),
};

Object.assign(TestClass.prototype, classMethods, callable);

testClass = new TestClass();
});

it('it is a function', () => (
expect(testClass.getAutocomplete).toEqual(expect.any(Function))
));

describe('when the request is valid', () => {
const search = 'test';

let testResponseBody;

describe('when there are multiple results', () => {
beforeEach(() => {
testResponseBody = [
[
'TEST 1',
[
'STREET A',
'STREET B',
],
],
[
'TEST 2',
[
'STREET A',
'STREET B',
],
],
];
});

it('resolves with the correct response', async () => {
const expectedParams = expect.objectContaining({
path: expect.stringMatching(/^\/[a-z]+$/),
qs: {
search,
},
});
testClass.request.mockImplementationOnce(async (params) => {
expect(params).toEqual(expectedParams);
return testResponseBody;
});
const response = await testClass.getAutocomplete(search);
expect(response).toBe(testResponseBody);
expect(response.listCompletions).toEqual(expect.any(Function));
expect(response.isCompleted).toBe(false);
});
});

describe('when there is a single result with multiple streets', () => {
beforeEach(() => {
testResponseBody = [
[
'TEST',
[
'STREET A',
'STREET B',
],
],
];
});

it('resolves with the correct response', async () => {
testClass.request.mockImplementationOnce(async () => testResponseBody);
const response = await testClass.getAutocomplete(search);
expect(response).toBe(testResponseBody);
expect(response.isCompleted).toBe(false);
});
});

describe('when there is only a single result', () => {
beforeEach(() => {
testResponseBody = [
[
'TEST',
[
'STREET A',
],
],
];
});

it('resolves with the correct response', async () => {
testClass.request.mockImplementationOnce(async () => testResponseBody);
const response = await testClass.getAutocomplete(search);
expect(response).toBe(testResponseBody);
expect(response.isCompleted).toBe(true);
});
});
});

describe('when the request is invalid', () => {
it('throws the correct error', async () => {
await expect(testClass.getAutocomplete()).rejects.toThrow(ParameterValidationError);
expect(testClass.emit).toHaveBeenNthCalledWith(1, 'operation:getAutocomplete', expect.any(Object));
expect(testClass.emit).toHaveBeenNthCalledWith(2, 'error', expect.any(ParameterValidationError));
});
});
});
2 changes: 2 additions & 0 deletions lib/operations/index.js
@@ -1,7 +1,9 @@
const { getAutocomplete } = require('./getAutocomplete');
const { getPostcode } = require('./getPostcode');
const { getLookup } = require('./getLookup');

module.exports = {
getAutocomplete,
getPostcode,
getLookup,
};
2 changes: 2 additions & 0 deletions lib/operations/methods/index.js
@@ -1,7 +1,9 @@
const { listCompletions } = require('./listCompletions');
const { listAddresses } = require('./listAddresses');
const { formatAddress } = require('./formatAddress');

module.exports = {
listAddresses,
listCompletions,
formatAddress,
};
15 changes: 15 additions & 0 deletions lib/operations/methods/listCompletions.js
@@ -0,0 +1,15 @@
module.exports = {
listCompletions() {
return this.reduce((completions, [postcode, streets]) => {
if (!streets || streets.length === 0) {
completions.push(postcode);
}

streets.forEach((street) => {
completions.push(`${street}, ${postcode}`);
});

return completions;
}, []);
},
};
58 changes: 58 additions & 0 deletions lib/operations/methods/listCompletions.test.js
@@ -0,0 +1,58 @@
const callable = require('./listCompletions');

describe('#listCompletions', () => {
let testResponse = [];

beforeEach(() => {
Object.assign(testResponse, callable);
});

it('it is a function', () => (
expect(testResponse.listCompletions).toEqual(expect.any(Function))
));

describe('when there are no matching completions', () => {
it('returns an empty list', () => {
expect(testResponse.listCompletions()).toEqual([]);
});
});

describe('when there are matching completions', () => {
describe('when there are matching streets', () => {
beforeEach(() => {
testResponse = Object.assign([
[
'TEST',
[
'STREET A',
'STREET B',
],
],
], callable);
});

it('returns a list containing streets', () => {
expect(testResponse.listCompletions()).toEqual([
'STREET A, TEST',
'STREET B, TEST',
]);
});
});

describe('when there are no matching streets', () => {
beforeEach(() => {
testResponse = Object.assign([
[
'TEST', [],
],
], callable);
});

it('returns a list containing the postcode', () => {
expect(testResponse.listCompletions()).toEqual([
'TEST',
]);
});
});
});
});

0 comments on commit 4775b42

Please sign in to comment.