Description:
Design a service to fetch exchange rate from a remote resource and then calculate the exchange rate for each currency pair.
The remote resource contains the exchange rates of each currency in
Euro
.
This is an interview assignment and I came up with a simple solution.
index.js
'use strict';
const joi = require('joi');
const api = require('./api');
const Exchange = require('./exchange');
const xmlParser = require('./parse-xml');
const schema = joi
.object({
source: joi.string().required().min(3).max(3).example('EUR'),
target: joi.string().required().min(3).max(3).example('GBP')
})
.unknown()
.required();
const defaults = {
timeout: 1000 // 1 sec
};
const exchange = async (pair, options = {}) => {
options = Object.assign({}, defaults, options);
const {source, target} = joi.attempt(pair, schema);
const {requestApi = api, parser = xmlParser} = options;
const exchange = new Exchange(requestApi, parser, options);
const rate = await exchange.convert({source, target});
return {source, target, rate};
};
module.exports = exchange;
exchange.js
'use strict';
const URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';
class Exchange {
constructor(api, parser, options = {}) {
this.api = api;
this.options = options;
this.parser = parser;
}
async convert({source, target}) {
if (!this.xml) {
await this.fetch();
this.euroToAll = this.parser(this.xml);
}
const euroToSource = this.euroToAll[source];
const euroToTarget = this.euroToAll[target];
return exchange(euroToSource, euroToTarget);
}
async fetch() {
const response = await this.api.fetch(URL, this.options);
this.xml = response.body || '';
}
}
function exchange(from, to) {
return round(parseFloat(to) / parseFloat(from));
}
function round(result, digits = 4) {
return Math.round(result * (10 ** digits)) / (10 ** digits);
}
module.exports = Exchange;
parse-xml.js
'use strict';
const xmldoc = require('xmldoc');
const debug = require('debug')('exchange-rate:parse');
const currencies = require('./currencies');
const parse = xml => {
const doc = new xmldoc.XmlDocument(xml);
const cube = doc.childNamed('Cube').childNamed('Cube');
const rates = currencies.reduce(
(accumulator, currency) => {
const exchange = cube.childWithAttribute('currency', currency);
if (exchange) {
const {rate} = exchange.attr;
accumulator[currency] = rate;
} else {
debug(`Node not found for currency: ${currency}`);
}
return accumulator;
},
{}
);
// Add EUR rate to make it consistent
rates.EUR = '1.0';
return rates;
};
module.exports = parse;
api.js
'use strict';
const got = require('got');
module.exports = {
async fetch(url, options = {}) {
return got(url, options);
}
};
test.js
import test from 'ava';
import createExchange from '.';
const xml = require('./fake-response');
test('return exchange rate for valid currency pair', async t => {
const mockParser = _ => ({
EUR: 1.0,
INR: 78.388,
GBP: 0.875,
USD: 1.1632
});
const mockApi = {
async fetch(_) {
return Promise.resolve(xml);
}
};
const options = {
parser: mockParser,
requestApi: mockApi
};
const exchange = createExchange(options);
let source = 'EUR';
let target = 'INR';
let expected = {
source,
target,
rate: 78.388
};
t.deepEqual(await exchange.convert({source, target}, options), expected);
source = 'USD';
target = 'INR';
expected = {
source,
target,
rate: 67.39
};
t.deepEqual(await exchange.convert({source, target}, options), expected);
source = 'INR';
target = 'USD';
expected = {
source,
target,
rate: 0.0148
};
t.deepEqual(await exchange.convert({source, target}, options), expected);
});
test('throw on invalid input', async t => {
const mockParser = _ => ({
EUR: 1.0,
INR: 79.1505,
USD: 1.1675
});
const mockApi = {
async fetch(_) {
return Promise.resolve(xml);
}
};
const options = {
parser: mockParser,
requestApi: mockApi
};
const exchange = createExchange(options);
try {
await exchange.convert('foo');
t.fail();
} catch (err) {
t.pass();
}
try {
await exchange.convert(NaN);
t.fail();
} catch (err) {
t.pass();
}
try {
await exchange.convert(Infinity);
t.fail();
} catch (err) {
t.pass();
}
try {
await exchange.convert({source: 'EUR', target: 'GB'});
t.fail();
} catch (err) {
t.pass();
}
});
Client code
const createExchange = require('exchange-rate');
const exchange = createExchange();
const {source, target, rate} = await exchange.convert({source: 'EUR', target: 'GBP'});
Questions:
- What if in the future we need to add different providers with different representations? How can I make it more flexible and keep the core logic decoupled?
- I am also curious to know if the design of the API from the client perspective is good or it can be improved.
- In NodeJs we can define dependency via require but I found it difficult to mock them for testing so, I have tried at couple of places to pass dependencies via arguments; is this fine?