Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanl17 authored Sep 25, 2019
1 parent d8c46f3 commit ab8f2a9
Show file tree
Hide file tree
Showing 10 changed files with 7,421 additions and 0 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Sorter

Service to sort countries by population

## Scripts:

- start app: `npm start -- --countries <country A> <country B>`
NOTE: countries with a space may be passed in quotes or with hyphens, eg: United Kingdom can be "United Kingdom" or United-Kingdom
- test app: `npm t [-- <file name>]`

## Startup

1. `npm i`
2. Run above script
7,000 changes: 7,000 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "countries-sorter",
"version": "0.0.1",
"description": "Service to sort countries by population",
"main": "index.js",
"scripts": {
"test": "jest --coverage --no-cache",
"start": "npx babel-node ./src/index.js"
},
"author": "Jordan Lawrence",
"license": "ISC",
"dependencies": {
"@babel/cli": "^7.4.3",
"@babel/core": "^7.4.3",
"@babel/node": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.4.3",
"@babel/runtime": "^7.4.4",
"axios": "^0.19.0",
"babel-eslint": "^10.0.1"
},
"devDependencies": {
"eslint": "^5.3.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.17.1",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"jest": "^24.7.1",
"prettier": "^1.17.0"
}
}
23 changes: 23 additions & 0 deletions src/argumentsHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const processArgs = processArguments =>
processArguments.reduce((paramKeys, currentarg) => {
const paramRegExp = new RegExp(/^--/g);
if (paramRegExp.test(currentarg)) {
// slice from 2nd index due to '--' param identifer
return { ...paramKeys, [currentarg.slice(2)]: [] };
}
const numberOfAggArgs = Object.keys(paramKeys).length;

return Object.entries(paramKeys).reduce((paramVals, [key, val], index) => {
if (index !== numberOfAggArgs - 1) {
// not the last param key so set param object to the existing paramKey, paramVal pairs
return { ...paramVals, [key]: val };
}
// replace '-' with a space char
return {
...paramVals,
[key]: [...val, currentarg.replace('-', ' ')]
};
}, {});
}, {});

export default processArgs;
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import sorter from './sorter';

sorter.start();
26 changes: 26 additions & 0 deletions src/services/populationService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import axios from 'axios';

const API_URL = 'http://54.72.28.201/1.0/';

const getPopulationToDate = country => {
const formattedQueryDate = new Date().toISOString().split('T')[0];
const formattedFullQuery = `population/${country}/${formattedQueryDate}`;

return axios
.get(API_URL + formattedFullQuery)
.then(({ data: { total_population: { population } } }) => ({ country, population }))
.catch(
({
response: { data: { detail = 'unknown error' } = { detail: 'unknown error' } } = {
data: { detail: 'unknown error' }
}
}) => {
if (detail.includes('is an invalid value for the parameter "country"')) {
return Promise.resolve({ country, invalidCountry: true });
}
return Promise.reject(new Error(detail));
}
);
};

export default { getPopulationToDate };
47 changes: 47 additions & 0 deletions src/sorter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import populationService from './services/populationService';
import processArgs from './argumentsHelper';

const start = async () => {
// data clensing
const processedArgs = processArgs(process.argv);

// if countries exists then capitalise first char, and remove duplicates
const inputCountries = processedArgs.countries && [
...new Set(
processedArgs.countries.map(country => country.charAt(0).toUpperCase() + country.slice(1))
)
];

if (!inputCountries || inputCountries.length <= 1) {
console.error(
"You should enter at least 2 distinct countries to compare in the format '--countries <country_one> <country_two>'"
);
return;
}

// data processing
await Promise.all(inputCountries.map(populationService.getPopulationToDate))
.then(populationForCountriesResults => {
const successfulSortedCountries = populationForCountriesResults
.filter(({ invalidCountry }) => !invalidCountry)
.sort(
({ population: populationA }, { population: populationB }) => populationB - populationA
);
const invalidCountries = populationForCountriesResults.filter(
({ invalidCountry }) => invalidCountry
);

if (successfulSortedCountries.length > 0)
console.table(successfulSortedCountries, ['country', 'population']);

if (invalidCountries.length > 0)
console.warn(
`The following countries were not valid: ${invalidCountries
.map(({ country }) => country)
.join(', ')}`
);
})
.catch(err => console.error(`Some API requests were not successful, error=${err}`));
};

export default { start };
44 changes: 44 additions & 0 deletions test/argumentsHelper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import processArguments from '../src/argumentsHelper';

describe('argumentsHelper', () => {
it('should correctly process a single empty param key', () => {
const inputProcessArguments = ['--key'];
const expectResult = { key: [] };
const result = processArguments(inputProcessArguments);

expect(result).toEqual(expectResult);
});

it('should correctly process multiple empty param key', () => {
const inputProcessArguments = ['--keyOne', '--keyTwo'];
const expectResult = { keyOne: [], keyTwo: [] };
const result = processArguments(inputProcessArguments);

expect(result).toEqual(expectResult);
});

it('should correctly process a single param key with multiple values', () => {
// 'value-three' to test hyphen to space replacement
const inputProcessArguments = ['--key', 'value one', 'valueTwo', 'value-three'];
const expectResult = { key: ['value one', 'valueTwo', 'value three'] };
const result = processArguments(inputProcessArguments);

expect(result).toEqual(expectResult);
});

it('should correctly process multiple param keys with multiple values', () => {
const inputProcessArguments = ['--keyOne', 'value one', '--keyTwo', 'valueOne', 'value two'];
const expectResult = { keyOne: ['value one'], keyTwo: ['valueOne', 'value two'] };
const result = processArguments(inputProcessArguments);

expect(result).toEqual(expectResult);
});

it('should not process an invalid param key identifier', () => {
const inputProcessArguments = ['-invalidKey', 'value one', '--keyTwo', 'valueOne', 'value two'];
const expectResult = { keyTwo: ['valueOne', 'value two'] };
const result = processArguments(inputProcessArguments);

expect(result).toEqual(expectResult);
});
});
86 changes: 86 additions & 0 deletions test/services/populationService.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import axios from 'axios';
import populationService from '../../src/services/populationService';

jest.mock('axios', () => ({
get: jest.fn()
}));

global.Date = jest.fn(() => ({ toISOString: jest.fn(() => '2019-06-07T') }));

describe('populationService', () => {
it('should make query to correct URL with correct time and country', () => {
const expectedAxiosGetRequest = 'http://54.72.28.201/1.0/population/test-country/2019-06-07';

axios.get.mockReturnValueOnce(
Promise.resolve({ data: { total_population: { population: 1 } } })
);

populationService.getPopulationToDate('test-country');
expect(axios.get).toHaveBeenCalledWith(expectedAxiosGetRequest);
});

it('should return country and population on successful api response', async () => {
const expectedResult = { country: 'test-country', population: 1 };

axios.get.mockReturnValueOnce(
Promise.resolve({ data: { total_population: { population: 1 } } })
);

const serviceResult = await populationService.getPopulationToDate('test-country');

expect(serviceResult).toEqual(expectedResult);
});

it('should return country and invalidCountry flag on api failure due to invalid country response', async () => {
const expectedResult = { country: 'test-country', invalidCountry: true };

const mockRejectionResponse = {
response: {
data: {
detail: 'some error about response is an invalid value for the parameter "country"'
}
}
};
axios.get.mockReturnValueOnce(Promise.reject(mockRejectionResponse));

const serviceResult = await populationService.getPopulationToDate('test-country');

expect(serviceResult).toEqual(expectedResult);
});

it('should return error on api failure due NOT to invalid country response', async () => {
const mockError = 'some error about response not due to invalid country';

const mockRejectionResponse = {
response: {
data: {
detail: mockError
}
}
};
axios.get.mockReturnValueOnce(Promise.reject(mockRejectionResponse));

try {
await populationService.getPopulationToDate('test-country');
} catch (err) {
// error scope
expect(err.message).toEqual(mockError);
}
});

it('should return "unknown error" on api failure without standard response', async () => {
const mockError = 'unknown error';

const mockRejectionResponse = {
response: 'unexpected structure'
};
axios.get.mockReturnValueOnce(Promise.reject(mockRejectionResponse));

try {
await populationService.getPopulationToDate('test-country');
} catch (err) {
// error scope
expect(err.message).toEqual(mockError);
}
});
});
Loading

0 comments on commit ab8f2a9

Please sign in to comment.