-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
7,421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import sorter from './sorter'; | ||
|
||
sorter.start(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); | ||
}); |
Oops, something went wrong.