Fold conversion into quote

This commit is contained in:
Hakan Ensari 2016-10-11 23:37:29 +01:00
parent 1754efd0cb
commit 60f980a6ee
6 changed files with 42 additions and 104 deletions

View File

@ -4,7 +4,6 @@ require 'oj'
require 'sinatra' require 'sinatra'
require 'rack/cors' require 'rack/cors'
require 'quote' require 'quote'
require 'converter'
configure :development do configure :development do
set :show_exceptions, :after_handler set :show_exceptions, :after_handler
@ -32,12 +31,8 @@ helpers do
end end
end end
def converter
@converter ||= Converter.new(params)
end
def symbols def symbols
@symbols ||= params.values_at('symbols', 'currencies').first @symbols ||= params.values_at('symbols', 'to').compact.first
end end
def jsonp(data) def jsonp(data)
@ -83,11 +78,6 @@ get(/(?<date>\d{4}-\d{2}-\d{2})/) do
jsonp quote_attributes jsonp quote_attributes
end end
get '/converter' do
params[:base] = params[:from]
jsonp Hash(amount: converter.convert(quote))
end
not_found do not_found do
halt 404, encode_json(error: 'Not found') halt 404, encode_json(error: 'Not found')
end end

View File

@ -1,32 +0,0 @@
# frozen_string_literal: true
require 'bigdecimal'
require 'quote'
class Converter
attr_reader :to, :from
def initialize(params = {})
@amount = params[:amount]
@from = params[:from]
@to = params[:to]
end
# Converts an amount into a new currency
# @param quote [Quote] the quote object
# @return amount [BigDecimal] the converted amount
def convert(quote = latest_quote)
return amount if quote.base == to
amount * quote.rates.fetch(to) { raise Quote::Invalid, 'Invalid to' }
end
def amount
BigDecimal(@amount.to_s)
end
private
def latest_quote
@latest_quote ||= Quote.new(base: from)
end
end

View File

@ -5,12 +5,14 @@ require 'currency'
class Quote class Quote
Invalid = Class.new(StandardError) Invalid = Class.new(StandardError)
DEFAULT_AMOUNT = 1
DEFAULT_BASE = 'EUR' DEFAULT_BASE = 'EUR'
attr_reader :base, :date attr_reader :amount, :base, :date
def initialize(params = {}) def initialize(params = {})
self.base = params[:base] self.amount = params['amount']
self.base = params.values_at(:base, :from).compact.first
self.date = params[:date] self.date = params[:date]
end end
@ -26,14 +28,18 @@ class Quote
private private
def base=(base) def amount=(value)
@base = base&.upcase || DEFAULT_BASE @amount = (value || DEFAULT_AMOUNT).to_f
raise Invalid, 'Invalid amount' if @amount.zero?
end end
def date=(date) def base=(value)
current_date = date ? Currency.current_date_before(date) : Currency.current_date @base = value&.upcase || DEFAULT_BASE
raise Invalid, 'Date too old' unless current_date end
@date = current_date
def date=(value)
@date = value ? Currency.current_date_before(value) : Currency.current_date
raise Invalid, 'Date too old' unless @date
rescue Sequel::DatabaseError => ex rescue Sequel::DatabaseError => ex
raise Invalid, 'Invalid date' if ex.wrapped_exception.is_a?(PG::DataException) raise Invalid, 'Invalid date' if ex.wrapped_exception.is_a?(PG::DataException)
raise raise
@ -49,16 +55,16 @@ class Quote
def find_default_rates def find_default_rates
Currency.where(date: date).reduce({}) do |rates, currency| Currency.where(date: date).reduce({}) do |rates, currency|
rates.update(currency.to_h) rates.update(Hash[currency.to_h.map { |k, v| [k, round_rate(v * amount)] }])
end end
end end
def find_rebased_rates def find_rebased_rates
rates = find_default_rates rates = find_default_rates
denominator = rates.update(DEFAULT_BASE => 1.0).delete(base) denominator = rates.update(DEFAULT_BASE => amount).delete(base)
raise Invalid, 'Invalid base' unless denominator raise Invalid, 'Invalid base' unless denominator
rates.each do |iso_code, rate| rates.each do |iso_code, rate|
rates[iso_code] = round_rate(rate / denominator) rates[iso_code] = round_rate(amount * rate / denominator)
end end
rates rates

View File

@ -26,11 +26,26 @@ describe 'the API' do
json['base'].must_equal 'USD' json['base'].must_equal 'USD'
end end
it 'sets base amount' do
get '/latest?amount=10'
json['rates']['USD'].must_be :>, 10
end
it 'filters symbols' do it 'filters symbols' do
get '/latest?symbols=USD' get '/latest?symbols=USD'
json['rates'].keys.must_equal %w(USD) json['rates'].keys.must_equal %w(USD)
end end
it 'aliases base as from' do
get '/latest?from=USD'
json['base'].must_equal 'USD'
end
it 'aliases symbols as to' do
get '/latest?to=USD'
json['rates'].keys.must_equal %w(USD)
end
it 'returns historical quotes' do it 'returns historical quotes' do
get '/2012-11-20' get '/2012-11-20'
json['rates'].wont_be :empty? json['rates'].wont_be :empty?
@ -67,8 +82,8 @@ describe 'the API' do
end end
end end
it 'returns converted amount' do it 'converts an amount' do
get '/converter?to=USD&amount=100' get '/latest?from=GBP&to=USD&amount=100'
json['amount'].wont_be :nil? json['rates']['USD'].must_be :>, 100
end end
end end

View File

@ -1,47 +0,0 @@
# frozen_string_literal: true
require_relative 'helper'
require 'converter'
describe Converter do
let(:to) { 'USD' }
let(:from) { 'EUR' }
let(:amount) { '100.0' }
let(:converter) { Converter.new to: to, from: from, amount: amount }
let(:quote) { Quote.new }
describe '#amount' do
it 'casts to BigDecimal' do
converter.amount.must_be_kind_of BigDecimal
end
end
describe '#convert' do
it 'should return the amount' do
quote.stub :base, to do
converter.convert(quote).must_equal BigDecimal(amount)
end
end
it 'should use the rates to convert' do
quote.stub :rates, Hash(to => 1.2) do
converter.convert(quote).must_equal BigDecimal('120.0')
end
end
it 'should raise with invalid currency' do
quote.stub :rates, {} do
-> { converter.convert(quote) }.must_raise Quote::Invalid, 'Invalid to'
end
end
it 'should use latest quote as default' do
quote.stub :rates, Hash(to => 1.3) do
converter.stub :latest_quote, quote do
converter.convert.must_equal 130.0
end
end
end
end
end

View File

@ -34,6 +34,12 @@ describe 'the API' do
json.wont_be_empty json.wont_be_empty
end end
it 'will not process an invalid amount' do
get '/latest?amount=foo'
last_response.must_be :unprocessable?
json.wont_be_empty
end
it 'handles malformed queries' do it 'handles malformed queries' do
get '/latest?base=USD?callback=?' get '/latest?base=USD?callback=?'
last_response.must_be :unprocessable? last_response.must_be :unprocessable?