mirror of
https://github.com/hakanensari/frankfurter.git
synced 2024-11-22 02:52:49 +01:00
Repack app
I'm moving my company's server to a private location now that I have sold the domain. While prepping for this, I've done some cleanup and also threw in changes I had lingering on my hard drive. - Run a single database query instead of two - Fold the gem into the app and use Ox instead of REXML - Simplify error handling logic - Relax throttling
This commit is contained in:
parent
9d0d22e504
commit
cfbb4ac4ac
@ -1,5 +1,5 @@
|
|||||||
|
APP_ENV=production
|
||||||
HTTPS_METHOD=noredirect
|
HTTPS_METHOD=noredirect
|
||||||
LETSENCRYPT_EMAIL=foo@bar.com
|
LETSENCRYPT_EMAIL=foo@bar.com
|
||||||
LETSENCRYPT_HOST=example.com,www.example.com
|
LETSENCRYPT_HOST=example.com,www.example.com
|
||||||
RACK_ENV=production
|
|
||||||
VIRTUAL_HOST=example.com,www.example.com
|
VIRTUAL_HOST=example.com,www.example.com
|
||||||
|
17
.travis.yml
17
.travis.yml
@ -1,10 +1,11 @@
|
|||||||
before_install:
|
language: ruby
|
||||||
- gem update --system
|
sudo: false
|
||||||
- gem install bundler
|
|
||||||
- psql -c 'create database fixer_test;' -U postgres
|
|
||||||
- cd app
|
|
||||||
env:
|
|
||||||
- RACK_ENV=test
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.5.0
|
- 2.5.0
|
||||||
sudo: false
|
env:
|
||||||
|
- APP_ENV=test
|
||||||
|
|
||||||
|
bundler_args: "--without development"
|
||||||
|
before_install:
|
||||||
|
- "psql -c 'create database fixer_test;' -U postgres"
|
||||||
|
@ -4,8 +4,8 @@ source 'http://rubygems.org'
|
|||||||
|
|
||||||
ruby '2.5.0'
|
ruby '2.5.0'
|
||||||
|
|
||||||
gem 'fixer'
|
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
|
gem 'ox'
|
||||||
gem 'rack-cors'
|
gem 'rack-cors'
|
||||||
gem 'rake'
|
gem 'rake'
|
||||||
gem 'rufus-scheduler'
|
gem 'rufus-scheduler'
|
||||||
@ -14,8 +14,14 @@ gem 'sinatra'
|
|||||||
gem 'unicorn'
|
gem 'unicorn'
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
gem 'pry'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :test do
|
||||||
gem 'minitest'
|
gem 'minitest'
|
||||||
gem 'minitest-around'
|
gem 'minitest-around'
|
||||||
gem 'rack-test'
|
gem 'rack-test'
|
||||||
gem 'rubocop'
|
gem 'rubocop'
|
||||||
|
gem 'vcr'
|
||||||
|
gem 'webmock'
|
||||||
end
|
end
|
@ -1,33 +1,44 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: http://rubygems.org/
|
remote: http://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
ast (2.3.0)
|
addressable (2.5.2)
|
||||||
et-orbi (1.0.8)
|
public_suffix (>= 2.0.2, < 4.0)
|
||||||
|
ast (2.4.0)
|
||||||
|
coderay (1.1.2)
|
||||||
|
crack (0.4.3)
|
||||||
|
safe_yaml (~> 1.0.0)
|
||||||
|
et-orbi (1.0.9)
|
||||||
tzinfo
|
tzinfo
|
||||||
fixer (1.0.0)
|
hashdiff (0.3.7)
|
||||||
kgio (2.11.1)
|
kgio (2.11.2)
|
||||||
minitest (5.11.1)
|
method_source (0.9.0)
|
||||||
minitest-around (0.4.0)
|
minitest (5.11.3)
|
||||||
|
minitest-around (0.4.1)
|
||||||
minitest (~> 5.0)
|
minitest (~> 5.0)
|
||||||
mustermann (1.0.1)
|
mustermann (1.0.2)
|
||||||
oj (3.3.10)
|
oj (3.5.0)
|
||||||
|
ox (2.8.4)
|
||||||
parallel (1.12.1)
|
parallel (1.12.1)
|
||||||
parser (2.4.0.2)
|
parser (2.5.0.3)
|
||||||
ast (~> 2.3)
|
ast (~> 2.4.0)
|
||||||
pg (0.21.0)
|
pg (1.0.0)
|
||||||
powerpack (0.1.1)
|
powerpack (0.1.1)
|
||||||
rack (2.0.3)
|
pry (0.11.3)
|
||||||
|
coderay (~> 1.1.0)
|
||||||
|
method_source (~> 0.9.0)
|
||||||
|
public_suffix (3.0.2)
|
||||||
|
rack (2.0.4)
|
||||||
rack-cors (1.0.2)
|
rack-cors (1.0.2)
|
||||||
rack-protection (2.0.0)
|
rack-protection (2.0.1)
|
||||||
rack
|
rack
|
||||||
rack-test (0.8.2)
|
rack-test (0.8.3)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
raindrops (0.19.0)
|
raindrops (0.19.0)
|
||||||
rake (12.3.0)
|
rake (12.3.0)
|
||||||
rubocop (0.52.1)
|
rubocop (0.53.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.4.0.2, < 3.0)
|
parser (>= 2.5)
|
||||||
powerpack (~> 0.1)
|
powerpack (~> 0.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
@ -35,32 +46,39 @@ GEM
|
|||||||
ruby-progressbar (1.9.0)
|
ruby-progressbar (1.9.0)
|
||||||
rufus-scheduler (3.4.2)
|
rufus-scheduler (3.4.2)
|
||||||
et-orbi (~> 1.0)
|
et-orbi (~> 1.0)
|
||||||
sequel (5.4.0)
|
safe_yaml (1.0.4)
|
||||||
|
sequel (5.6.0)
|
||||||
sequel_pg (1.8.1)
|
sequel_pg (1.8.1)
|
||||||
pg (>= 0.18.0)
|
pg (>= 0.18.0)
|
||||||
sequel (>= 4.34.0)
|
sequel (>= 4.34.0)
|
||||||
sinatra (2.0.0)
|
sinatra (2.0.1)
|
||||||
mustermann (~> 1.0)
|
mustermann (~> 1.0)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
rack-protection (= 2.0.0)
|
rack-protection (= 2.0.1)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.8)
|
tilt (2.0.8)
|
||||||
tzinfo (1.2.4)
|
tzinfo (1.2.5)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
unicode-display_width (1.3.0)
|
unicode-display_width (1.3.0)
|
||||||
unicorn (5.4.0)
|
unicorn (5.4.0)
|
||||||
kgio (~> 2.6)
|
kgio (~> 2.6)
|
||||||
raindrops (~> 0.7)
|
raindrops (~> 0.7)
|
||||||
|
vcr (4.0.0)
|
||||||
|
webmock (3.3.0)
|
||||||
|
addressable (>= 2.3.6)
|
||||||
|
crack (>= 0.3.2)
|
||||||
|
hashdiff
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
fixer
|
|
||||||
minitest
|
minitest
|
||||||
minitest-around
|
minitest-around
|
||||||
oj
|
oj
|
||||||
|
ox
|
||||||
|
pry
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-test
|
rack-test
|
||||||
rake
|
rake
|
||||||
@ -69,6 +87,8 @@ DEPENDENCIES
|
|||||||
sequel_pg
|
sequel_pg
|
||||||
sinatra
|
sinatra
|
||||||
unicorn
|
unicorn
|
||||||
|
vcr
|
||||||
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.5.0p0
|
ruby 2.5.0p0
|
22
README.md
22
README.md
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
Fixer is a free API for current and historical foreign exchange rates [published by the European Central Bank](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html).
|
Fixer is a free API for current and historical foreign exchange rates [published by the European Central Bank](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html).
|
||||||
|
|
||||||
A public instance of the API lives at [https://api.fixer.io](https://api.fixer.io). Alternatively, you can run privately with the provided [Docker image](https://hub.docker.com/r/hakanensari/fixer/).
|
|
||||||
|
|
||||||
Rates are updated around 4PM CET every working day.
|
Rates are updated around 4PM CET every working day.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -22,16 +20,22 @@ Get historical rates for any day since 1999.
|
|||||||
GET /2000-01-03
|
GET /2000-01-03
|
||||||
```
|
```
|
||||||
|
|
||||||
Rates are quoted against the Euro by default. Quote against a different currency by setting the base parameter in your request.
|
Rates quote against the Euro by default. Quote against a different currency.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /latest?base=USD
|
GET /latest?from=USD
|
||||||
```
|
```
|
||||||
|
|
||||||
Request specific exchange rates by setting the symbols parameter.
|
Request specific exchange rates.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /latest?symbols=USD,GBP
|
GET /latest?to=GBP
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the amount requested.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /latest?amount=100
|
||||||
```
|
```
|
||||||
|
|
||||||
The primary use case is client side. For instance, with [money.js](https://openexchangerates.github.io/money.js/) in the browser
|
The primary use case is client side. For instance, with [money.js](https://openexchangerates.github.io/money.js/) in the browser
|
||||||
@ -42,7 +46,7 @@ let demo = () => {
|
|||||||
alert("£1 = $" + rate.toFixed(4))
|
alert("£1 = $" + rate.toFixed(4))
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('https://api.fixer.io/latest')
|
fetch('https://api.example.com/latest')
|
||||||
.then((resp) => resp.json())
|
.then((resp) => resp.json())
|
||||||
.then((data) => fx.rates = data.rates)
|
.then((data) => fx.rates = data.rates)
|
||||||
.then(demo)
|
.then(demo)
|
||||||
@ -50,8 +54,6 @@ fetch('https://api.fixer.io/latest')
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
I have included a sample Docker Compose configuration in the repo.
|
|
||||||
|
|
||||||
To build locally, type
|
To build locally, type
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -64,7 +66,7 @@ Now you can access the API at
|
|||||||
http://localhost:8080
|
http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
In production, create a [`.env`](.env.example) file in the project root and run with
|
In production, create a [`.env`](.env.example) file and run with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Currency < Sequel::Model
|
|
||||||
dataset_module do
|
|
||||||
def recent
|
|
||||||
order(Sequel.desc(:date))
|
|
||||||
end
|
|
||||||
|
|
||||||
def before(value)
|
|
||||||
where { date <= value }
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_date
|
|
||||||
recent.first&.date
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_date_before(value)
|
|
||||||
before(value).current_date
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_h
|
|
||||||
{ iso_code => rate }
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,80 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'currency'
|
|
||||||
|
|
||||||
class Quote
|
|
||||||
Invalid = Class.new(StandardError)
|
|
||||||
|
|
||||||
DEFAULT_AMOUNT = 1
|
|
||||||
DEFAULT_BASE = 'EUR'
|
|
||||||
|
|
||||||
attr_reader :amount, :base, :date, :symbols
|
|
||||||
|
|
||||||
def initialize(params = {})
|
|
||||||
self.amount = params['amount']
|
|
||||||
self.base = params.values_at(:base, :from).compact.first
|
|
||||||
self.symbols = params.values_at(:symbols, :to).compact.first
|
|
||||||
self.date = params[:date]
|
|
||||||
end
|
|
||||||
|
|
||||||
def rates
|
|
||||||
@rates ||= find_rates
|
|
||||||
end
|
|
||||||
|
|
||||||
def attributes
|
|
||||||
{ base: base, date: date, rates: rates }
|
|
||||||
end
|
|
||||||
|
|
||||||
alias to_h attributes
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
attr_writer :symbols
|
|
||||||
|
|
||||||
def amount=(value)
|
|
||||||
@amount = (value || DEFAULT_AMOUNT).to_f
|
|
||||||
raise Invalid, 'Invalid amount' if @amount.zero?
|
|
||||||
end
|
|
||||||
|
|
||||||
def base=(value)
|
|
||||||
@base = value&.upcase || DEFAULT_BASE
|
|
||||||
end
|
|
||||||
|
|
||||||
def date=(value)
|
|
||||||
@date = value ? Currency.current_date_before(value) : Currency.current_date
|
|
||||||
raise Invalid, 'Date too old' unless @date
|
|
||||||
rescue Sequel::DatabaseError => ex
|
|
||||||
raise Invalid, 'Invalid date' if ex.wrapped_exception.is_a?(PG::DataException)
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_rates
|
|
||||||
rates = quoted_against_default_base? ? find_default_rates : find_rebased_rates
|
|
||||||
symbols ? rates.keep_if { |k, _| symbols.include?(k) } : rates
|
|
||||||
end
|
|
||||||
|
|
||||||
def quoted_against_default_base?
|
|
||||||
base == DEFAULT_BASE
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_default_rates
|
|
||||||
Currency.where(date: date).order(:iso_code).reduce({}) do |rates, currency|
|
|
||||||
rates.update(Hash[currency.to_h.map { |k, v| [k, round_rate(v * amount)] }])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_rebased_rates
|
|
||||||
rates = find_default_rates
|
|
||||||
denominator = rates.update(DEFAULT_BASE => amount).delete(base)
|
|
||||||
raise Invalid, 'Invalid base' unless denominator
|
|
||||||
rates
|
|
||||||
.map { |(iso_code, rate)| [iso_code, round_rate(amount * rate / denominator)] }
|
|
||||||
.sort_by(&:first)
|
|
||||||
.to_h
|
|
||||||
end
|
|
||||||
|
|
||||||
# I'm mimicking the apparent convention of the ECB here.
|
|
||||||
def round_rate(rate)
|
|
||||||
Float(format('%.5g', rate))
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,35 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative 'helper'
|
|
||||||
require 'currency'
|
|
||||||
|
|
||||||
describe Currency do
|
|
||||||
around do |test|
|
|
||||||
Currency.db.transaction do
|
|
||||||
test.call
|
|
||||||
raise Sequel::Rollback
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
Currency.dataset.delete
|
|
||||||
@first = Currency.create(date: '2014-01-01')
|
|
||||||
@last = Currency.create(date: '2015-01-01')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns current date' do
|
|
||||||
Currency.current_date.must_equal @last.date
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns current date before given date' do
|
|
||||||
Currency.current_date_before(@last.date - 1).must_equal @first.date
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns nil if there is no current date before given date' do
|
|
||||||
Currency.current_date_before(@first.date - 1).must_be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'casts to hash' do
|
|
||||||
@first.to_h.must_be_kind_of Hash
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,5 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require './config/environment'
|
|
||||||
require 'minitest/autorun'
|
|
||||||
require 'minitest/around/spec'
|
|
@ -1,104 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative 'helper'
|
|
||||||
require 'quote'
|
|
||||||
|
|
||||||
describe Quote do
|
|
||||||
def stub_rates(default_rates = {})
|
|
||||||
quote.stub :find_default_rates, default_rates do
|
|
||||||
yield quote
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'by default' do
|
|
||||||
let(:quote) { Quote.new }
|
|
||||||
|
|
||||||
it 'quotes rates against the euro' do
|
|
||||||
stub_rates 'USD' => 1.25 do |quote|
|
|
||||||
rate = quote.rates['USD']
|
|
||||||
rate.must_equal 1.25
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not quote the euro' do
|
|
||||||
stub_rates do |quote|
|
|
||||||
quote.rates.keys.wont_include 'EUR'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'casts to hash' do
|
|
||||||
stub_rates do |quote|
|
|
||||||
%i[base date rates].each do |key|
|
|
||||||
quote.to_h.keys.must_include key
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given a custom base' do
|
|
||||||
let(:quote) { Quote.new(base: 'FOO') }
|
|
||||||
|
|
||||||
it 'quotes rates against that currency' do
|
|
||||||
stub_rates 'FOO' => 2 do |quote|
|
|
||||||
quote.rates.keys.must_include 'EUR'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not quote the base currency' do
|
|
||||||
stub_rates 'FOO' => 2 do |quote|
|
|
||||||
quote.rates.keys.wont_include 'FOO'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rounds to five significant digits' do
|
|
||||||
stub_rates 'FOO' => 0.6995 do |quote|
|
|
||||||
rate = quote.rates['EUR']
|
|
||||||
rate.must_equal 1.4296
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the currencies sorted' do
|
|
||||||
stub_rates 'BAZ' => 1.5, 'FOO' => 2, 'BAR' => 3, 'QUX' => 4 do |quote|
|
|
||||||
quote.rates.keys.must_equal quote.rates.keys.sort
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given an invalid base' do
|
|
||||||
let(:quote) { Quote.new(base: 'FOO') }
|
|
||||||
|
|
||||||
it 'raises validation error' do
|
|
||||||
proc { quote.to_h }.must_raise Quote::Invalid
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given a date that is too old' do
|
|
||||||
let(:quote) { Quote.new(date: '1900-01-01') }
|
|
||||||
|
|
||||||
it 'raises validation error' do
|
|
||||||
Currency.stub :current_date_before, nil do
|
|
||||||
proc { quote }.must_raise Quote::Invalid
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given a bad date' do
|
|
||||||
let(:quote) { Quote.new(date: '2000-31-01') }
|
|
||||||
|
|
||||||
it 'raises validation error' do
|
|
||||||
proc { quote }.must_raise Quote::Invalid
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given custom symbols' do
|
|
||||||
let(:quote) { Quote.new(symbols: 'FOO,BAR') }
|
|
||||||
|
|
||||||
it 'quotes rates only for given symbols' do
|
|
||||||
stub_rates 'FOO' => 1, 'BAR' => 2, 'BAZ' => 3 do |quote|
|
|
||||||
quote.rates.keys.must_include 'FOO'
|
|
||||||
quote.rates.keys.must_include 'BAR'
|
|
||||||
quote.rates.keys.wont_include 'BAZ'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -6,7 +6,7 @@ require 'pathname'
|
|||||||
module App
|
module App
|
||||||
class << self
|
class << self
|
||||||
def env
|
def env
|
||||||
ENV['RACK_ENV'] || 'development'
|
ENV['APP_ENV'] || 'development'
|
||||||
end
|
end
|
||||||
|
|
||||||
def root
|
def root
|
@ -1,15 +1,13 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build:
|
build: .
|
||||||
context: ./app
|
|
||||||
environment:
|
environment:
|
||||||
RACK_ENV: development
|
APP_ENV: development
|
||||||
VIRTUAL_HOST: localhost
|
VIRTUAL_HOST: localhost
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
scheduler:
|
scheduler:
|
||||||
build:
|
build: .
|
||||||
context: ./app
|
|
||||||
environment:
|
environment:
|
||||||
RACK_ENV: development
|
APP_ENV: development
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
AllCops:
|
|
||||||
TargetRubyVersion: 2.5
|
|
||||||
Metrics/BlockLength:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/**/*'
|
|
@ -1,4 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
|
||||||
gemspec
|
|
@ -1,52 +0,0 @@
|
|||||||
PATH
|
|
||||||
remote: .
|
|
||||||
specs:
|
|
||||||
fixer (1.0.0)
|
|
||||||
|
|
||||||
GEM
|
|
||||||
remote: https://rubygems.org/
|
|
||||||
specs:
|
|
||||||
addressable (2.5.2)
|
|
||||||
public_suffix (>= 2.0.2, < 4.0)
|
|
||||||
ast (2.3.0)
|
|
||||||
crack (0.4.3)
|
|
||||||
safe_yaml (~> 1.0.0)
|
|
||||||
hashdiff (0.3.7)
|
|
||||||
minitest (5.10.3)
|
|
||||||
parallel (1.12.1)
|
|
||||||
parser (2.4.0.2)
|
|
||||||
ast (~> 2.3)
|
|
||||||
powerpack (0.1.1)
|
|
||||||
public_suffix (3.0.1)
|
|
||||||
rainbow (3.0.0)
|
|
||||||
rake (12.3.0)
|
|
||||||
rubocop (0.52.1)
|
|
||||||
parallel (~> 1.10)
|
|
||||||
parser (>= 2.4.0.2, < 3.0)
|
|
||||||
powerpack (~> 0.1)
|
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
|
||||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
|
||||||
ruby-progressbar (1.9.0)
|
|
||||||
safe_yaml (1.0.4)
|
|
||||||
unicode-display_width (1.3.0)
|
|
||||||
vcr (4.0.0)
|
|
||||||
webmock (3.1.1)
|
|
||||||
addressable (>= 2.3.6)
|
|
||||||
crack (>= 0.3.2)
|
|
||||||
hashdiff
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
ruby
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
bundler
|
|
||||||
fixer!
|
|
||||||
minitest
|
|
||||||
rake
|
|
||||||
rubocop
|
|
||||||
vcr
|
|
||||||
webmock
|
|
||||||
|
|
||||||
BUNDLED WITH
|
|
||||||
1.16.1
|
|
@ -1,9 +0,0 @@
|
|||||||
# Fixer
|
|
||||||
|
|
||||||
Fixer wraps the XML feeds of Euro foreign exchange reference provided by the European Central Bank:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
Fixer.current
|
|
||||||
Fixer.ninety_days
|
|
||||||
Fixer.historical
|
|
||||||
```
|
|
14
gem/Rakefile
14
gem/Rakefile
@ -1,14 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'bundler/gem_tasks'
|
|
||||||
require 'rake/testtask'
|
|
||||||
require 'rubocop/rake_task'
|
|
||||||
|
|
||||||
Rake::TestTask.new do |t|
|
|
||||||
t.ruby_opts += ['-W1']
|
|
||||||
t.pattern = 'spec/**/*_spec.rb'
|
|
||||||
end
|
|
||||||
|
|
||||||
RuboCop::RakeTask.new
|
|
||||||
|
|
||||||
task default: %i[test rubocop]
|
|
@ -1,26 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
lib = File.expand_path('../lib', __FILE__)
|
|
||||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
||||||
require 'fixer/version'
|
|
||||||
|
|
||||||
Gem::Specification.new do |spec|
|
|
||||||
spec.name = 'fixer'
|
|
||||||
spec.version = Fixer::VERSION
|
|
||||||
spec.author = ['Hakan Ensari']
|
|
||||||
spec.email = ['hakanensari@gmail.com']
|
|
||||||
spec.summary = <<-SUMMARY
|
|
||||||
A wrapper to the exchange rate feeds of the European Central Bank
|
|
||||||
SUMMARY
|
|
||||||
spec.homepage = 'https://github.com/hakanensari/fixer'
|
|
||||||
spec.license = 'MIT'
|
|
||||||
spec.files = Dir.glob('lib/**/*') + %w[README.md]
|
|
||||||
spec.require_paths = ['lib']
|
|
||||||
|
|
||||||
spec.add_development_dependency 'bundler'
|
|
||||||
spec.add_development_dependency 'minitest'
|
|
||||||
spec.add_development_dependency 'rake'
|
|
||||||
spec.add_development_dependency 'rubocop'
|
|
||||||
spec.add_development_dependency 'vcr'
|
|
||||||
spec.add_development_dependency 'webmock'
|
|
||||||
end
|
|
@ -1,17 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'fixer/feed'
|
|
||||||
|
|
||||||
# ECB exchange rate datasets
|
|
||||||
#
|
|
||||||
# The three available short hands here are `.current`, `.ninety_days`, and
|
|
||||||
# `.historical`.
|
|
||||||
module Fixer
|
|
||||||
class << self
|
|
||||||
Feed::SCOPES.each_key do |scope|
|
|
||||||
define_method(scope) do
|
|
||||||
Feed.new(scope)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,48 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'net/http'
|
|
||||||
require 'rexml/document'
|
|
||||||
|
|
||||||
module Fixer
|
|
||||||
# Wraps ECB's data feed
|
|
||||||
class Feed
|
|
||||||
include Enumerable
|
|
||||||
|
|
||||||
SCOPES = {
|
|
||||||
current: 'daily',
|
|
||||||
ninety_days: 'hist-90d',
|
|
||||||
historical: 'hist'
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def initialize(scope = :current)
|
|
||||||
@scope = SCOPES.fetch(scope) { raise ArgumentError }
|
|
||||||
end
|
|
||||||
|
|
||||||
def each
|
|
||||||
REXML::XPath.each(document, '/gesmes:Envelope/Cube/Cube[@time]') do |day|
|
|
||||||
date = Date.parse(day.attribute('time').value)
|
|
||||||
REXML::XPath.each(day, './Cube') do |currency|
|
|
||||||
yield(
|
|
||||||
date: date,
|
|
||||||
iso_code: currency.attribute('currency').value,
|
|
||||||
rate: Float(currency.attribute('rate').value)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def document
|
|
||||||
REXML::Document.new(xml)
|
|
||||||
end
|
|
||||||
|
|
||||||
def xml
|
|
||||||
Net::HTTP.get(url)
|
|
||||||
end
|
|
||||||
|
|
||||||
def url
|
|
||||||
URI("http://www.ecb.europa.eu/stats/eurofxref/eurofxref-#{@scope}.xml")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,5 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Fixer
|
|
||||||
VERSION = '1.0.0'
|
|
||||||
end
|
|
@ -1,34 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative 'helper'
|
|
||||||
|
|
||||||
describe Fixer do
|
|
||||||
before do
|
|
||||||
@mock = MiniTest::Mock.new
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
@mock.verify
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns current rates' do
|
|
||||||
@mock.expect(:call, nil, [:current])
|
|
||||||
Fixer::Feed.stub(:new, @mock) do
|
|
||||||
Fixer.current
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns ninety-day rates' do
|
|
||||||
@mock.expect(:call, nil, [:ninety_days])
|
|
||||||
Fixer::Feed.stub(:new, @mock) do
|
|
||||||
Fixer.ninety_days
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns historical rates' do
|
|
||||||
@mock.expect(:call, nil, [:historical])
|
|
||||||
Fixer::Feed.stub(:new, @mock) do
|
|
||||||
Fixer.historical
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -3,7 +3,9 @@
|
|||||||
require 'oj'
|
require 'oj'
|
||||||
require 'sinatra'
|
require 'sinatra'
|
||||||
require 'rack/cors'
|
require 'rack/cors'
|
||||||
require 'quote'
|
|
||||||
|
require 'query'
|
||||||
|
require 'quotation'
|
||||||
|
|
||||||
use Rack::Cors do
|
use Rack::Cors do
|
||||||
allow do
|
allow do
|
||||||
@ -20,13 +22,20 @@ configure :production do
|
|||||||
disable :dump_errors
|
disable :dump_errors
|
||||||
end
|
end
|
||||||
|
|
||||||
|
configure :test do
|
||||||
|
set :raise_errors, false
|
||||||
|
end
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
def quote
|
def quotation
|
||||||
@quote ||= Quote.new(params)
|
@quotation ||= begin
|
||||||
|
query = Query.new(params)
|
||||||
|
Quotation.new(query.to_h)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def jsonp(data)
|
def jsonp(data)
|
||||||
json = encode_json(data)
|
json = Oj.dump(data, mode: :compat)
|
||||||
callback = params.delete('callback')
|
callback = params.delete('callback')
|
||||||
if callback
|
if callback
|
||||||
content_type :js
|
content_type :js
|
||||||
@ -36,10 +45,6 @@ helpers do
|
|||||||
json
|
json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def encode_json(data)
|
|
||||||
Oj.dump(data, mode: :compat)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
options '*' do
|
options '*' do
|
||||||
@ -52,23 +57,23 @@ get '*' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
get '/' do
|
get '/' do
|
||||||
jsonp details: 'http://fixer.io'
|
jsonp source: 'https://github.com/hakanensari/fixer'
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/latest' do
|
get '/latest' do
|
||||||
last_modified quote.date
|
last_modified quotation.date
|
||||||
jsonp quote.to_h
|
jsonp quotation.quote
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/(?<date>\d{4}-\d{2}-\d{2})', mustermann_opts: { type: :regexp } do
|
get '/(?<date>\d{4}-\d{2}-\d{2})', mustermann_opts: { type: :regexp } do
|
||||||
last_modified quote.date
|
last_modified quotation.date
|
||||||
jsonp quote.to_h
|
jsonp quotation.quote
|
||||||
end
|
end
|
||||||
|
|
||||||
not_found do
|
not_found do
|
||||||
halt 404, encode_json(error: 'Not found')
|
halt 404
|
||||||
end
|
end
|
||||||
|
|
||||||
error Quote::Invalid do |ex|
|
error do
|
||||||
halt 422, encode_json(error: ex.message)
|
halt 422
|
||||||
end
|
end
|
@ -1,20 +1,19 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'currency'
|
require 'currency'
|
||||||
require 'fixer'
|
require 'bank/feed'
|
||||||
|
|
||||||
module Bank
|
module Bank
|
||||||
def self.fetch_all_rates!
|
def self.fetch_all_rates!
|
||||||
Currency.db.transaction do
|
Currency.db.transaction do
|
||||||
Currency.dataset.delete
|
Currency.dataset.delete
|
||||||
data = Fixer.historical
|
Currency.multi_insert(Feed.historical.to_a)
|
||||||
Currency.multi_insert(data.to_a)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.fetch_current_rates!
|
def self.fetch_current_rates!
|
||||||
Currency.db.transaction do
|
Currency.db.transaction do
|
||||||
Fixer.current.each do |hsh|
|
Feed.current.each do |hsh|
|
||||||
Currency.find_or_create(hsh)
|
Currency.find_or_create(hsh)
|
||||||
end
|
end
|
||||||
end
|
end
|
53
lib/bank/feed.rb
Normal file
53
lib/bank/feed.rb
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'ox'
|
||||||
|
|
||||||
|
module Bank
|
||||||
|
class Feed
|
||||||
|
include Enumerable
|
||||||
|
|
||||||
|
def self.current
|
||||||
|
new('daily')
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.ninety_days
|
||||||
|
new('hist-90d')
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.historical
|
||||||
|
new('hist')
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(scope)
|
||||||
|
@scope = scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def each
|
||||||
|
document.locate('gesmes:Envelope/Cube/Cube').each do |day|
|
||||||
|
date = Date.parse(day['time'])
|
||||||
|
day.locate('Cube').each do |record|
|
||||||
|
yield(
|
||||||
|
date: date,
|
||||||
|
iso_code: record['currency'],
|
||||||
|
rate: Float(record['rate'])
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def document
|
||||||
|
Ox.load(xml)
|
||||||
|
end
|
||||||
|
|
||||||
|
def xml
|
||||||
|
Net::HTTP.get(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
URI("http://www.ecb.europa.eu/stats/eurofxref/eurofxref-#{@scope}.xml")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
lib/currency.rb
Normal file
11
lib/currency.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Currency < Sequel::Model
|
||||||
|
dataset_module do
|
||||||
|
def latest(date = Date.today)
|
||||||
|
where(date: select(:date).where(Sequel.lit('date <= ?', date))
|
||||||
|
.order(Sequel.desc(:date))
|
||||||
|
.limit(1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
lib/query.rb
Normal file
27
lib/query.rb
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Query
|
||||||
|
def initialize(params = {})
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def amount
|
||||||
|
@params[:amount].to_f if @params[:amount] # rubocop:disable Style/SafeNavigation
|
||||||
|
end
|
||||||
|
|
||||||
|
def base
|
||||||
|
@params.values_at(:base, :from).compact.first&.upcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def symbols
|
||||||
|
@params.values_at(:symbols, :to).compact.first&.split(',')
|
||||||
|
end
|
||||||
|
|
||||||
|
def date
|
||||||
|
@params[:date]
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{ amount: amount, base: base, date: date, symbols: symbols }.compact
|
||||||
|
end
|
||||||
|
end
|
61
lib/quotation.rb
Normal file
61
lib/quotation.rb
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'currency'
|
||||||
|
|
||||||
|
class Quotation
|
||||||
|
DEFAULT_BASE = 'EUR'
|
||||||
|
|
||||||
|
def initialize(amount: 1.0,
|
||||||
|
base: DEFAULT_BASE,
|
||||||
|
date: Date.today.to_s,
|
||||||
|
symbols: nil)
|
||||||
|
@amount = amount
|
||||||
|
@base = base
|
||||||
|
@date = date
|
||||||
|
@symbols = symbols
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote
|
||||||
|
{ date: date, rates: calculate_rates }
|
||||||
|
end
|
||||||
|
|
||||||
|
def date
|
||||||
|
currencies.first[:date].to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def calculate_rates # rubocop:disable Metrics/AbcSize
|
||||||
|
rates = currencies.each_with_object({}) do |currency, hsh|
|
||||||
|
hsh[currency[:iso_code]] = currency[:rate]
|
||||||
|
end
|
||||||
|
|
||||||
|
return rates if @base == DEFAULT_BASE && @amount == 1.0
|
||||||
|
|
||||||
|
if @symbols.nil? || @symbols.include?(DEFAULT_BASE)
|
||||||
|
rates.update(DEFAULT_BASE => 1.0)
|
||||||
|
end
|
||||||
|
divisor = rates.delete(@base)
|
||||||
|
|
||||||
|
rates.sort.map! { |ic, rate| [ic, round(@amount * rate / divisor)] }.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
def currencies
|
||||||
|
@currencies ||= begin
|
||||||
|
scope = Currency.latest(@date)
|
||||||
|
scope = scope.where(iso_code: @symbols + [@base]) if @symbols
|
||||||
|
|
||||||
|
scope.order(:iso_code).naked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# To paraphrase Wikipedia, most currency pairs are quoted to four decimal
|
||||||
|
# places. An exception to this is exchange rates with a value of less than
|
||||||
|
# 1.000, which are quoted to five or six decimal places. Exchange rates
|
||||||
|
# greater than around 20 are usually quoted to three decimal places and
|
||||||
|
# exchange rates greater than 80 are quoted to two decimal places.
|
||||||
|
# Currencies over 5000 are usually quoted with no decimal places.
|
||||||
|
def round(rate)
|
||||||
|
Float(format('%.5g', rate))
|
||||||
|
end
|
||||||
|
end
|
@ -1,3 +1,3 @@
|
|||||||
limit_req_zone $binary_remote_addr zone=api:10m rate=1r/s;
|
limit_req_zone $binary_remote_addr zone=api:10m rate=50r/s;
|
||||||
limit_req_status 429;
|
limit_req_status 429;
|
||||||
limit_req zone=api burst=50 nodelay;
|
limit_req zone=api burst=500 nodelay;
|
||||||
|
@ -22,8 +22,10 @@ describe 'the API' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'sets base currency' do
|
it 'sets base currency' do
|
||||||
|
get '/latest'
|
||||||
|
res = Oj.load(last_response.body)
|
||||||
get '/latest?base=USD'
|
get '/latest?base=USD'
|
||||||
json['base'].must_equal 'USD'
|
json.wont_equal res
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets base amount' do
|
it 'sets base amount' do
|
||||||
@ -36,16 +38,6 @@ describe 'the API' do
|
|||||||
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?
|
@ -1,47 +1,49 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative '../helper'
|
require_relative '../helper'
|
||||||
|
require 'bank/feed'
|
||||||
|
|
||||||
module Fixer
|
module Bank
|
||||||
describe Feed do
|
describe Feed do
|
||||||
before { VCR.insert_cassette 'fixer' }
|
before do
|
||||||
after { VCR.eject_cassette }
|
VCR.insert_cassette 'feed'
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
VCR.eject_cassette
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fetches current rates' do
|
||||||
|
feed = Feed.current
|
||||||
|
feed.count.must_be :<, 40
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fetches rates for the past 90 days' do
|
||||||
|
feed = Feed.ninety_days
|
||||||
|
feed.count.must_be :>, 33 * 60
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fetches historical rates' do
|
||||||
|
feed = Feed.historical
|
||||||
|
feed.count.must_be :>, 33 * 3000
|
||||||
|
end
|
||||||
|
|
||||||
it 'parses the date of a currency' do
|
it 'parses the date of a currency' do
|
||||||
feed = Feed.new(:current)
|
feed = Feed.current
|
||||||
currency = feed.first
|
currency = feed.first
|
||||||
currency[:date].must_be_kind_of Date
|
currency[:date].must_be_kind_of Date
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parse the ISO code of a currency' do
|
it 'parse the ISO code of a currency' do
|
||||||
feed = Feed.new(:current)
|
feed = Feed.current
|
||||||
currency = feed.first
|
currency = feed.first
|
||||||
currency[:iso_code].must_be_kind_of String
|
currency[:iso_code].must_be_kind_of String
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parses the rate of a currency' do
|
it 'parses the rate of a currency' do
|
||||||
feed = Feed.new(:current)
|
feed = Feed.current
|
||||||
currency = feed.first
|
currency = feed.first
|
||||||
currency[:rate].must_be_kind_of Float
|
currency[:rate].must_be_kind_of Float
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'fetches current rates' do
|
|
||||||
feed = Feed.new(:current)
|
|
||||||
feed.count.must_be :<, 40
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fetches rates for the past 90 days' do
|
|
||||||
feed = Feed.new(:ninety_days)
|
|
||||||
feed.count.must_be :>, 33 * 60
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fetches historical rates' do
|
|
||||||
feed = Feed.new(:historical)
|
|
||||||
feed.count.must_be :>, 33 * 3000
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'raises error when scope is not valid' do
|
|
||||||
-> { Feed.new(:invalid) }.must_raise ArgumentError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -12,9 +12,19 @@ describe Bank do
|
|||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
VCR.insert_cassette 'feed'
|
||||||
Currency.dataset.delete
|
Currency.dataset.delete
|
||||||
end
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
VCR.eject_cassette
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fetches all rates' do
|
||||||
|
Bank.fetch_all_rates!
|
||||||
|
Currency.count.must_be :positive?
|
||||||
|
end
|
||||||
|
|
||||||
it 'fetches current rates' do
|
it 'fetches current rates' do
|
||||||
Bank.fetch_current_rates!
|
Bank.fetch_current_rates!
|
||||||
Currency.count.must_be :positive?
|
Currency.count.must_be :positive?
|
40
spec/currency_spec.rb
Normal file
40
spec/currency_spec.rb
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'helper'
|
||||||
|
require 'currency'
|
||||||
|
|
||||||
|
describe Currency do
|
||||||
|
around do |test|
|
||||||
|
Currency.db.transaction do
|
||||||
|
test.call
|
||||||
|
raise Sequel::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Currency.dataset.delete
|
||||||
|
@earlier = [
|
||||||
|
Currency.create(iso_code: 'EUR', rate: 1, date: '2014-01-01'),
|
||||||
|
Currency.create(iso_code: 'USD', rate: 2, date: '2014-01-01')
|
||||||
|
]
|
||||||
|
@later = [
|
||||||
|
Currency.create(iso_code: 'EUR', rate: 1, date: '2015-01-01'),
|
||||||
|
Currency.create(iso_code: 'USD', rate: 2, date: '2015-01-01')
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns latest rates' do
|
||||||
|
data = Currency.latest.to_a
|
||||||
|
data.sample.date.must_equal @later.sample.date
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns latest rates before given date' do
|
||||||
|
data = Currency.latest(@later.sample.date - 1).to_a
|
||||||
|
data.sample.date.must_equal @earlier.sample.date
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nothing if there are no rates before given date' do
|
||||||
|
data = Currency.latest(@earlier.sample.date - 1).to_a
|
||||||
|
data.must_be_empty
|
||||||
|
end
|
||||||
|
end
|
@ -13,42 +13,31 @@ describe 'the API' do
|
|||||||
it 'handles unfound pages' do
|
it 'handles unfound pages' do
|
||||||
get '/foo'
|
get '/foo'
|
||||||
last_response.status.must_equal 404
|
last_response.status.must_equal 404
|
||||||
json.wont_be_empty
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'will not process an invalid date' do
|
it 'will not process an invalid date' do
|
||||||
get '/2010-31-01'
|
get '/2010-31-01'
|
||||||
last_response.must_be :unprocessable?
|
last_response.must_be :unprocessable?
|
||||||
json.wont_be_empty
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'will not process a date before 2000' do
|
it 'will not process a date before 2000' do
|
||||||
get '/1999-01-01'
|
get '/1999-01-01'
|
||||||
last_response.must_be :unprocessable?
|
last_response.must_be :unprocessable?
|
||||||
json.wont_be_empty
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'will not process an invalid base' do
|
it 'will not process an invalid base' do
|
||||||
get '/latest?base=UAH'
|
get '/latest?base=UAH'
|
||||||
last_response.must_be :unprocessable?
|
last_response.must_be :unprocessable?
|
||||||
json.wont_be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'will not process an invalid amount' do
|
|
||||||
get '/latest?amount=foo'
|
|
||||||
last_response.must_be :unprocessable?
|
|
||||||
json.wont_be_empty
|
|
||||||
end
|
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?
|
||||||
json.wont_be_empty
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns fresh dates' do
|
it 'returns fresh dates' do
|
||||||
Currency.db.transaction do
|
Currency.db.transaction do
|
||||||
new_date = Currency.current_date + 1
|
new_date = Currency.order(Sequel.desc(:date)).first.date + 1
|
||||||
Currency.create(date: new_date, iso_code: 'FOO', rate: 1)
|
Currency.create(date: new_date, iso_code: 'FOO', rate: 1)
|
||||||
get '/latest'
|
get '/latest'
|
||||||
json['date'].must_equal new_date.to_s
|
json['date'].must_equal new_date.to_s
|
@ -1,12 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
|
require_relative '../config/environment'
|
||||||
|
|
||||||
require 'minitest/autorun'
|
require 'minitest/autorun'
|
||||||
|
require 'minitest/around/spec'
|
||||||
require 'vcr'
|
require 'vcr'
|
||||||
require 'webmock'
|
require 'webmock'
|
||||||
|
|
||||||
require 'fixer'
|
begin
|
||||||
|
require 'pry'
|
||||||
|
rescue LoadError # rubocop:disable Lint/HandleExceptions
|
||||||
|
end
|
||||||
|
|
||||||
VCR.configure do |c|
|
VCR.configure do |c|
|
||||||
c.cassette_library_dir = 'spec/vcr_cassettes'
|
c.cassette_library_dir = 'spec/vcr_cassettes'
|
56
spec/query_spec.rb
Normal file
56
spec/query_spec.rb
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'helper'
|
||||||
|
require 'query'
|
||||||
|
|
||||||
|
describe Query do
|
||||||
|
it 'returns given amount' do
|
||||||
|
query = Query.new(amount: '100')
|
||||||
|
query.amount.must_equal 100.0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults amount to nothin' do
|
||||||
|
query = Query.new
|
||||||
|
query.amount.must_be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns given base' do
|
||||||
|
query = Query.new(base: 'USD')
|
||||||
|
query.base.must_equal 'USD'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults base to nothing' do
|
||||||
|
query = Query.new
|
||||||
|
query.base.must_be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'aliases base as from' do
|
||||||
|
query = Query.new(from: 'USD')
|
||||||
|
query.base.must_equal 'USD'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns given symbols' do
|
||||||
|
query = Query.new(symbols: 'USD,GBP')
|
||||||
|
query.symbols.must_equal %w[USD GBP]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'aliases symbols to to' do
|
||||||
|
query = Query.new(to: 'USD')
|
||||||
|
query.symbols.must_equal ['USD']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults symbols to nothing' do
|
||||||
|
query = Query.new
|
||||||
|
query.symbols.must_be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns given date' do
|
||||||
|
query = Query.new(date: '2014-01-01')
|
||||||
|
query.date.must_equal '2014-01-01'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults date to nothing' do
|
||||||
|
query = Query.new
|
||||||
|
query.date.must_be_nil
|
||||||
|
end
|
||||||
|
end
|
45
spec/quotation_spec.rb
Normal file
45
spec/quotation_spec.rb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'helper'
|
||||||
|
require 'quotation'
|
||||||
|
|
||||||
|
describe Quotation do
|
||||||
|
describe 'by default' do
|
||||||
|
it 'quotes against the Euro' do
|
||||||
|
quotation = Quotation.new
|
||||||
|
rates = quotation.quote[:rates]
|
||||||
|
rates.keys.wont_include 'EUR'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when given a base' do
|
||||||
|
it 'quotes against that base' do
|
||||||
|
quotation = Quotation.new(base: 'USD')
|
||||||
|
rates = quotation.quote[:rates]
|
||||||
|
rates.keys.wont_include 'USD'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts rates' do
|
||||||
|
quotation = Quotation.new(base: 'USD')
|
||||||
|
rates = quotation.quote[:rates]
|
||||||
|
rates.keys.must_equal rates.keys.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when given symbols' do
|
||||||
|
it 'quotes rates only for given symbols' do
|
||||||
|
quotation = Quotation.new(symbols: ['USD'])
|
||||||
|
rates = quotation.quote[:rates]
|
||||||
|
rates.keys.must_include 'USD'
|
||||||
|
rates.keys.wont_include 'GBP'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when given an amount' do
|
||||||
|
it 'quotes for that amount' do
|
||||||
|
quotation = Quotation.new(amount: 100)
|
||||||
|
rates = quotation.quote[:rates]
|
||||||
|
rates['USD'].must_be :>, 10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user