money-openexchangerates-bank 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +225 -0
- data/lib/money/bank/openexchangerates_bank.rb +353 -0
- data/test/TEST_ACCESS_KEY +1 -0
- data/test/live.json +198 -0
- data/test/openexchangerates_bank_test.rb +305 -0
- data/test/test_helper.rb +17 -0
- metadata +195 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4d1de65480f09a588a4682745d9134a7a1200442
|
4
|
+
data.tar.gz: ceedd43fa6d2ab3c856d6adeffdabe43b4342b83
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db4fedb4010411e85ca593135944e679c35e879401694c107ecc10848b4bbf048035ffec3a6d508162ff334711ce86ca8552425c27cbd6ed19a27d147c5afd97
|
7
|
+
data.tar.gz: 1ae9b5d341306846427c7094b6c6507c56130190c6db7122cb0c2ce4996c7c391db2819b0be3436b072b1843dd6bf57ff61d59dda7a4e97ec93c83497046dd17
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Phlegx Systems OG
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# Money Openexchangerates Bank
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/money-openexchangerates-bank.svg)](https://rubygems.org/gems/money-openexchangerates-bank)
|
4
|
+
[![Gem](https://img.shields.io/gem/dt/money-openexchangerates-bank.svg?maxAge=2592000)](https://rubygems.org/gems/money-openexchangerates-bank)
|
5
|
+
[![Build Status](https://secure.travis-ci.org/phlegx/money-openexchangerates-bank.svg?branch=master)](https://travis-ci.org/phlegx/money-openexchangerates-bank)
|
6
|
+
[![Code Climate](http://img.shields.io/codeclimate/github/phlegx/money-openexchangerates-bank.svg)](https://codeclimate.com/github/phlegx/money-openexchangerates-bank)
|
7
|
+
[![Inline Docs](http://inch-ci.org/github/phlegx/money-openexchangerates-bank.svg?branch=master)](http://inch-ci.org/github/phlegx/money-openexchangerates-bank)
|
8
|
+
[![Dependency Status](https://gemnasium.com/phlegx/money-openexchangerates-bank.svg)](https://gemnasium.com/phlegx/money-openexchangerates-bank)
|
9
|
+
[![License](https://img.shields.io/github/license/phlegx/money-openexchangerates-bank.svg)](http://opensource.org/licenses/MIT)
|
10
|
+
|
11
|
+
A gem that calculates the exchange rate using published rates from
|
12
|
+
[openexchangerates.org](https://openexchangerates.org/)
|
13
|
+
|
14
|
+
## Openexchangerates API
|
15
|
+
|
16
|
+
~~~ json
|
17
|
+
{
|
18
|
+
"timestamp": 1441101909,
|
19
|
+
"base": "USD",
|
20
|
+
"rates": {
|
21
|
+
/* 190 currencies */
|
22
|
+
"AUD": 1.413637,
|
23
|
+
"CAD": 1.316495,
|
24
|
+
"CHF": 0.96355,
|
25
|
+
"EUR": 0.888466,
|
26
|
+
"BTC": 0.004322, /* Includes cryptocurrencies! */
|
27
|
+
...
|
28
|
+
}
|
29
|
+
}
|
30
|
+
~~~
|
31
|
+
|
32
|
+
See more about Openexchangerates product plans on https://openexchangerates.org/signup.
|
33
|
+
|
34
|
+
## Features
|
35
|
+
|
36
|
+
* supports 190 currencies
|
37
|
+
* includes cryptocurrencies
|
38
|
+
* precision of rates up to 6 digits after point
|
39
|
+
* uses fast and reliable json api
|
40
|
+
* average response time < 20ms
|
41
|
+
* supports caching currency rates
|
42
|
+
* calculates every pair rate calculating inverse rate or using base currency rate
|
43
|
+
* supports multi threading
|
44
|
+
|
45
|
+
## Installation
|
46
|
+
|
47
|
+
Add this line to your application's Gemfile:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
gem 'money-openexchangerates-bank'
|
51
|
+
```
|
52
|
+
|
53
|
+
And then execute:
|
54
|
+
|
55
|
+
$ bundle
|
56
|
+
|
57
|
+
Or install it yourself as:
|
58
|
+
|
59
|
+
$ gem install money-openexchangerates-bank
|
60
|
+
|
61
|
+
## Usage
|
62
|
+
|
63
|
+
~~~ ruby
|
64
|
+
# Minimal requirements
|
65
|
+
require 'money/bank/openexchangerates_bank'
|
66
|
+
moxb = Money::Bank::OpenexchangeratesBank.new
|
67
|
+
moxb.access_key = 'your access_key from https://openexchangerates.com/signup'
|
68
|
+
|
69
|
+
# Update rates (get new rates from remote if expired or access rates from cache)
|
70
|
+
moxb.update_rates
|
71
|
+
|
72
|
+
# Force update rates from remote and store in cache
|
73
|
+
# moxb.update_rates(true)
|
74
|
+
|
75
|
+
# (optional)
|
76
|
+
# Set the base currency for all rates. By default, USD is used.
|
77
|
+
# OpenexchangeratesBank only allows USD as base currency for the free plan users.
|
78
|
+
moxb.source = 'EUR'
|
79
|
+
|
80
|
+
# (optional)
|
81
|
+
# Set the seconds after than the current rates are automatically expired
|
82
|
+
# by default, they never expire, in this example 1 day.
|
83
|
+
moxb.ttl_in_seconds = 86400
|
84
|
+
|
85
|
+
# (optional)
|
86
|
+
# Use https to fetch rates from OpenexchangeratesBank
|
87
|
+
# OpenexchangeratesBank only allows http as connection for the free plan users.
|
88
|
+
moxb.secure_connection = true
|
89
|
+
|
90
|
+
# Define cache (string or pathname)
|
91
|
+
moxb.cache = 'path/to/file/cache'
|
92
|
+
|
93
|
+
# Set money default bank to Openexchangerates bank
|
94
|
+
Money.default_bank = moxb
|
95
|
+
~~~
|
96
|
+
|
97
|
+
### More methods
|
98
|
+
|
99
|
+
~~~ ruby
|
100
|
+
moxb = Money::Bank::OpenexchangeratesBank.new
|
101
|
+
|
102
|
+
# Returns the base currency set for all rates.
|
103
|
+
moxb.source
|
104
|
+
|
105
|
+
# Include also alternative rates.
|
106
|
+
moxb.alternatives = :all
|
107
|
+
|
108
|
+
# Get only alternative rates.
|
109
|
+
moxb.alternatives = :only
|
110
|
+
|
111
|
+
# Default is nil to get only regular rates.
|
112
|
+
# moxb.alternatives = nil
|
113
|
+
|
114
|
+
# Expires rates if the expiration time is reached.
|
115
|
+
moxb.expire_rates!
|
116
|
+
|
117
|
+
# Returns true if the expiration time is reached.
|
118
|
+
moxb.expired?
|
119
|
+
|
120
|
+
# Get the API source url.
|
121
|
+
moxb.source_url
|
122
|
+
|
123
|
+
# Get the rates timestamp of the last API request.
|
124
|
+
moxb.rates_timestamp
|
125
|
+
|
126
|
+
# Get the rates timestamp of loaded rates in memory.
|
127
|
+
moxb.rates_mem_timestamp
|
128
|
+
~~~
|
129
|
+
|
130
|
+
### How to exchange
|
131
|
+
|
132
|
+
~~~ ruby
|
133
|
+
# Exchange 1000 cents (10.0 USD) to EUR
|
134
|
+
Money.new(1000, 'USD').exchange_to('EUR') # => #<Money fractional:89 currency:EUR>
|
135
|
+
Money.new(1000, 'USD').exchange_to('EUR').to_f # => 8.9
|
136
|
+
|
137
|
+
# Format
|
138
|
+
Money.new(1000, 'USD').exchange_to('EUR').format # => €8.90
|
139
|
+
|
140
|
+
# Get the rate
|
141
|
+
Money.default_bank.get_rate('USD', 'CAD') # => 0.9
|
142
|
+
~~~
|
143
|
+
|
144
|
+
See more on https://github.com/RubyMoney/money.
|
145
|
+
|
146
|
+
### Using gem money-rails
|
147
|
+
|
148
|
+
You can also use it in Rails with the gem [money-rails](https://github.com/RubyMoney/money-rails).
|
149
|
+
|
150
|
+
~~~ ruby
|
151
|
+
require 'money/bank/openexchangerates_bank'
|
152
|
+
|
153
|
+
MoneyRails.configure do |config|
|
154
|
+
moxb = Money::Bank::OpenexchangeratesBank.new
|
155
|
+
moxb.access_key = 'your access_key from https://openexchangerates.com/signup'
|
156
|
+
moxb.update_rates
|
157
|
+
|
158
|
+
config.default_bank = moxb
|
159
|
+
end
|
160
|
+
~~~
|
161
|
+
|
162
|
+
### Cache
|
163
|
+
|
164
|
+
You can also provide a Proc as a cache to provide your own caching mechanism
|
165
|
+
perhaps with Redis or just a thread safe `Hash` (global). For example:
|
166
|
+
|
167
|
+
~~~ ruby
|
168
|
+
moxb.cache = Proc.new do |v|
|
169
|
+
key = 'money:openexchangerates_bank'
|
170
|
+
if v
|
171
|
+
Thread.current[key] = v
|
172
|
+
else
|
173
|
+
Thread.current[key]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
~~~
|
177
|
+
|
178
|
+
## Process
|
179
|
+
|
180
|
+
The gem fetches all rates in a cache with USD as base currency. It's possible to compute the rate between any of the currencies by calculating a pair rate using base USD rate.
|
181
|
+
|
182
|
+
## Tests
|
183
|
+
|
184
|
+
You can place your own key on a file or environment
|
185
|
+
variable named TEST_ACCESS_KEY and then run:
|
186
|
+
|
187
|
+
~~~
|
188
|
+
bundle exec rake
|
189
|
+
~~~
|
190
|
+
|
191
|
+
## Refs
|
192
|
+
|
193
|
+
* Gem [money](https://github.com/RubyMoney/money)
|
194
|
+
* Gem [money-currencylayer-bank](https://github.com/phlegx/money-currencylayer-bank)
|
195
|
+
* Gem [money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates)
|
196
|
+
* Gem [money-historical-bank](https://github.com/atwam/money-historical-bank)
|
197
|
+
|
198
|
+
## Other Implementations
|
199
|
+
|
200
|
+
* Gem [money-currencylayer-bank](https://github.com/phlegx/money-currencylayer-bank)
|
201
|
+
* Gem [money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates)
|
202
|
+
* Gem [money-historical-bank](https://github.com/atwam/money-historical-bank)
|
203
|
+
* Gem [currencylayer](https://github.com/askuratovsky/currencylayer)
|
204
|
+
* Gem [eu_central_bank](https://github.com/RubyMoney/eu_central_bank)
|
205
|
+
* Gem [nordea](https://github.com/matiaskorhonen/nordea)
|
206
|
+
* Gem [google_currency](https://github.com/RubyMoney/google_currency)
|
207
|
+
|
208
|
+
## Contributors
|
209
|
+
|
210
|
+
* See [github.com/phlegx/money-openexchangerates-bank](https://github.com/phlegx/money-openexchangerates-bank/graphs/contributors).
|
211
|
+
* Inspired by [github.com/phlegx/money-currencylayer-bank](https://github.com/phlegx/money-currencylayer-bank/graphs/contributors).
|
212
|
+
|
213
|
+
## Contributing
|
214
|
+
|
215
|
+
1. Fork it ( https://github.com/[your-username]/money-openexchangerates-bank/fork )
|
216
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
217
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
218
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
219
|
+
5. Create a new Pull Request
|
220
|
+
|
221
|
+
## License
|
222
|
+
|
223
|
+
The MIT License
|
224
|
+
|
225
|
+
Copyright (c) 2017 Phlegx Systems OG
|
@@ -0,0 +1,353 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'open-uri'
|
5
|
+
require 'money'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
# Money gem class
|
9
|
+
class Money
|
10
|
+
# Build in memory rates store
|
11
|
+
module RatesStore
|
12
|
+
# Memory class
|
13
|
+
class Memory
|
14
|
+
# Add method to reset the build in memory store
|
15
|
+
# @param [Hash] rt Optional initial exchange rate data.
|
16
|
+
# @return [Object] store.
|
17
|
+
def reset!(rt = {})
|
18
|
+
transaction { @index = rt }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# https://github.com/RubyMoney/money#exchange-rate-stores
|
24
|
+
module Bank
|
25
|
+
# Invalid cache, file not found or cache empty
|
26
|
+
class InvalidCache < StandardError; end
|
27
|
+
|
28
|
+
# App id not set error
|
29
|
+
class NoAccessKey < StandardError; end
|
30
|
+
|
31
|
+
# CurrencylayerBank base class
|
32
|
+
class OpenexchangeratesBank < Money::Bank::VariableExchange
|
33
|
+
# CurrencylayerBank url
|
34
|
+
CL_URL = 'http://openexchangerates.org/api/latest.json'.freeze
|
35
|
+
# CurrencylayerBank secure url
|
36
|
+
CL_SECURE_URL = CL_URL.sub('http:', 'https:')
|
37
|
+
# Default base currency
|
38
|
+
CL_SOURCE = 'USD'.freeze
|
39
|
+
|
40
|
+
# Use https to fetch rates from CurrencylayerBank
|
41
|
+
# CurrencylayerBank only allows http as connection
|
42
|
+
# for the free plan users.
|
43
|
+
#
|
44
|
+
# @param value [Boolean] true for secure connection
|
45
|
+
# @return [Boolean] chosen secure connection
|
46
|
+
attr_accessor :secure_connection
|
47
|
+
|
48
|
+
# API must have a valid access_key
|
49
|
+
#
|
50
|
+
# @param value [String] API access key
|
51
|
+
# @return [String] chosen API access key
|
52
|
+
attr_accessor :access_key
|
53
|
+
|
54
|
+
# Cache accessor, can be a String or a Proc
|
55
|
+
#
|
56
|
+
# @param value [String,Pathname,Proc] cache system
|
57
|
+
# @return [String,Pathname,Proc] chosen cache system
|
58
|
+
attr_accessor :cache
|
59
|
+
|
60
|
+
# Parsed CurrencylayerBank result as Hash
|
61
|
+
attr_reader :rates
|
62
|
+
|
63
|
+
# Get the timestamp of rates in memory
|
64
|
+
# @return [Time] time object or nil
|
65
|
+
attr_reader :rates_mem_timestamp
|
66
|
+
|
67
|
+
# Set the seconds after than the current rates are automatically expired
|
68
|
+
# by default, they never expire.
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# ttl_in_seconds = 86400 # will expire the rates in one day
|
72
|
+
#
|
73
|
+
# @param value [Integer] time to live in seconds
|
74
|
+
# @return [Integer] chosen time to live in seconds
|
75
|
+
attr_writer :ttl_in_seconds
|
76
|
+
|
77
|
+
# Set the base currency for all rates. By default, USD is used.
|
78
|
+
# CurrencylayerBank only allows USD as base currency
|
79
|
+
# for the free plan users.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# source = 'USD'
|
83
|
+
#
|
84
|
+
# @param value [String] Currency code, ISO 3166-1 alpha-3
|
85
|
+
# @return [String] chosen base currency
|
86
|
+
def source=(value)
|
87
|
+
@source = Money::Currency.find(value.to_s).try(:iso_code) || CL_SOURCE
|
88
|
+
end
|
89
|
+
|
90
|
+
# Get the base currency for all rates. By default, USD is used.
|
91
|
+
# @return [String] base currency
|
92
|
+
def source
|
93
|
+
@source ||= CL_SOURCE
|
94
|
+
end
|
95
|
+
|
96
|
+
# Set alternative system, to request alternative rates
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
# alternatives = :all
|
100
|
+
# alternatives = :only
|
101
|
+
# alternatives = nil
|
102
|
+
#
|
103
|
+
# @param value [String,Symbol] alternatives
|
104
|
+
# @return [String,nil] chosen alternative system
|
105
|
+
def alternatives=(system)
|
106
|
+
@alternatives = case system.to_sym
|
107
|
+
when :all
|
108
|
+
'show_alternative'
|
109
|
+
when :only
|
110
|
+
'only_alternative'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get alternative system. Default is nil
|
115
|
+
# @return [String,nil] chosen alternative system
|
116
|
+
def alternatives
|
117
|
+
@alternatives || nil
|
118
|
+
end
|
119
|
+
|
120
|
+
# Get the seconds after than the current rates are automatically expired
|
121
|
+
# by default, they never expire.
|
122
|
+
# @return [Integer] chosen time to live in seconds
|
123
|
+
def ttl_in_seconds
|
124
|
+
@ttl_in_seconds ||= 0
|
125
|
+
end
|
126
|
+
|
127
|
+
# Update all rates from CurrencylayerBank JSON
|
128
|
+
# @return [Array] array of exchange rates
|
129
|
+
def update_rates(straight = false)
|
130
|
+
store.reset!
|
131
|
+
exchange_rates(straight).each do |exchange_rate|
|
132
|
+
currency = exchange_rate.first
|
133
|
+
rate = exchange_rate.last
|
134
|
+
next unless Money::Currency.find(currency)
|
135
|
+
add_rate(source, currency, rate)
|
136
|
+
add_rate(currency, source, 1.0 / rate)
|
137
|
+
end
|
138
|
+
@rates_mem_timestamp = rates_timestamp
|
139
|
+
end
|
140
|
+
|
141
|
+
# Override Money `add_rate` method for caching
|
142
|
+
# @param [String] from_currency Currency ISO code. ex. 'USD'
|
143
|
+
# @param [String] to_currency Currency ISO code. ex. 'CAD'
|
144
|
+
# @param [Numeric] rate Rate to use when exchanging currencies.
|
145
|
+
# @return [Numeric] rate.
|
146
|
+
def add_rate(from_currency, to_currency, rate)
|
147
|
+
super
|
148
|
+
end
|
149
|
+
|
150
|
+
# Alias super method
|
151
|
+
alias super_get_rate get_rate
|
152
|
+
|
153
|
+
# Override Money `get_rate` method for caching
|
154
|
+
# @param [String] from_currency Currency ISO code. ex. 'USD'
|
155
|
+
# @param [String] to_currency Currency ISO code. ex. 'CAD'
|
156
|
+
# @param [Hash] opts Options hash to set special parameters.
|
157
|
+
# @return [Numeric] rate.
|
158
|
+
def get_rate(from_currency, to_currency, opts = {})
|
159
|
+
expire_rates!
|
160
|
+
rate = get_rate_or_calc_inverse(from_currency, to_currency, opts)
|
161
|
+
rate || calc_pair_rate_using_base(from_currency, to_currency, opts)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Fetch new rates if cached rates are expired or stale
|
165
|
+
# @return [Boolean] true if rates are expired and updated from remote
|
166
|
+
def expire_rates!
|
167
|
+
if expired?
|
168
|
+
update_rates(true)
|
169
|
+
true
|
170
|
+
elsif stale?
|
171
|
+
update_rates
|
172
|
+
true
|
173
|
+
else
|
174
|
+
false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Check if rates are expired
|
179
|
+
# @return [Boolean] true if rates are expired
|
180
|
+
def expired?
|
181
|
+
Time.now > rates_expiration
|
182
|
+
end
|
183
|
+
|
184
|
+
# Check if rates are stale
|
185
|
+
# Stale is true if rates are updated straight by another thread.
|
186
|
+
# The actual thread has always old rates in memory store.
|
187
|
+
# @return [Boolean] true if rates are stale
|
188
|
+
def stale?
|
189
|
+
rates_timestamp != rates_mem_timestamp
|
190
|
+
end
|
191
|
+
|
192
|
+
# Source url of CurrencylayerBank
|
193
|
+
# defined with access_key and secure_connection
|
194
|
+
# @return [String] the remote API url
|
195
|
+
def source_url
|
196
|
+
raise NoAccessKey if access_key.nil? || access_key.empty?
|
197
|
+
cl_url = CL_URL
|
198
|
+
cl_url = CL_SECURE_URL if secure_connection
|
199
|
+
params = "#{cl_url}?base=#{source}&app_id=#{access_key}&prettyprint=0"
|
200
|
+
params += "&#{alternatives}=1" unless alternatives.nil?
|
201
|
+
params
|
202
|
+
end
|
203
|
+
|
204
|
+
# Get rates expiration time based on ttl
|
205
|
+
# @return [Time] rates expiration time
|
206
|
+
def rates_expiration
|
207
|
+
rates_timestamp + ttl_in_seconds
|
208
|
+
end
|
209
|
+
|
210
|
+
# Get the timestamp of rates
|
211
|
+
# @return [Time] time object or nil
|
212
|
+
def rates_timestamp
|
213
|
+
raw = raw_rates_careful
|
214
|
+
raw.key?('timestamp') ? Time.at(raw['timestamp']) : Time.at(0)
|
215
|
+
end
|
216
|
+
|
217
|
+
protected
|
218
|
+
|
219
|
+
# Store the provided text data by calling the proc method provided
|
220
|
+
# for the cache, or write to the cache file.
|
221
|
+
#
|
222
|
+
# @example
|
223
|
+
# store_in_cache("{\"rates\": {\"AED\": 3.67304}}")
|
224
|
+
#
|
225
|
+
# @param text [String] parsed JSON content
|
226
|
+
# @return [String,Integer]
|
227
|
+
def store_in_cache(text)
|
228
|
+
if cache.is_a?(Proc)
|
229
|
+
cache.call(text)
|
230
|
+
elsif cache.is_a?(String) || cache.is_a?(Pathname)
|
231
|
+
write_to_file(text)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Writes content to file cache
|
236
|
+
# @param text [String] parsed JSON content
|
237
|
+
# @return [String,Integer]
|
238
|
+
def write_to_file(text)
|
239
|
+
open(cache, 'w') do |f|
|
240
|
+
f.write(text)
|
241
|
+
end
|
242
|
+
rescue Errno::ENOENT
|
243
|
+
raise InvalidCache
|
244
|
+
end
|
245
|
+
|
246
|
+
# Read from cache when exist
|
247
|
+
# @return [Proc,String] parsed JSON content
|
248
|
+
def read_from_cache
|
249
|
+
if cache.is_a?(Proc)
|
250
|
+
cache.call(nil)
|
251
|
+
elsif (cache.is_a?(String) || cache.is_a?(Pathname)) &&
|
252
|
+
File.exist?(cache)
|
253
|
+
open(cache).read
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Get remote content and store in cache
|
258
|
+
# @return [String] unparsed JSON content
|
259
|
+
def read_from_url
|
260
|
+
text = open_url
|
261
|
+
store_in_cache(text) if valid_rates?(text) && cache
|
262
|
+
text
|
263
|
+
end
|
264
|
+
|
265
|
+
# Opens an url and reads the content
|
266
|
+
# @return [String] unparsed JSON content
|
267
|
+
def open_url
|
268
|
+
open(source_url).read
|
269
|
+
rescue OpenURI::HTTPError
|
270
|
+
''
|
271
|
+
end
|
272
|
+
|
273
|
+
# Check validity of rates response only for store in cache
|
274
|
+
#
|
275
|
+
# @example
|
276
|
+
# valid_rates?("{\"rates\": {\"AED\": 3.67304}}")
|
277
|
+
#
|
278
|
+
# @param [String] text is JSON content
|
279
|
+
# @return [Boolean] valid or not
|
280
|
+
def valid_rates?(text)
|
281
|
+
parsed = JSON.parse(text)
|
282
|
+
parsed && parsed.key?('rates')
|
283
|
+
rescue JSON::ParserError
|
284
|
+
false
|
285
|
+
end
|
286
|
+
|
287
|
+
# Get exchange rates with different strategies
|
288
|
+
#
|
289
|
+
# @example
|
290
|
+
# exchange_rates(true)
|
291
|
+
# exchange_rates
|
292
|
+
#
|
293
|
+
# @param straight [Boolean] true for straight, default is careful
|
294
|
+
# @return [Hash] key is country code (ISO 3166-1 alpha-3) value Float
|
295
|
+
def exchange_rates(straight = false)
|
296
|
+
@rates = if straight
|
297
|
+
raw_rates_straight['rates']
|
298
|
+
else
|
299
|
+
raw_rates_careful['rates']
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Get raw exchange rates from cache and then from url
|
304
|
+
# @param rescue_straight [Boolean] true for rescue straight, default true
|
305
|
+
# @return [String] JSON content
|
306
|
+
def raw_rates_careful(rescue_straight = true)
|
307
|
+
JSON.parse(read_from_cache.to_s)
|
308
|
+
rescue JSON::ParserError
|
309
|
+
rescue_straight ? raw_rates_straight : { 'rates' => {} }
|
310
|
+
end
|
311
|
+
|
312
|
+
# Get raw exchange rates from url
|
313
|
+
# @return [String] JSON content
|
314
|
+
def raw_rates_straight
|
315
|
+
JSON.parse(read_from_url)
|
316
|
+
rescue JSON::ParserError
|
317
|
+
raw_rates_careful(false)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Get rate or calculate it as inverse rate
|
321
|
+
# @param [String] from_currency Currency ISO code. ex. 'USD'
|
322
|
+
# @param [String] to_currency Currency ISO code. ex. 'CAD'
|
323
|
+
# @return [Numeric] rate or rate calculated as inverse rate.
|
324
|
+
def get_rate_or_calc_inverse(from_currency, to_currency, opts = {})
|
325
|
+
rate = super_get_rate(from_currency, to_currency, opts)
|
326
|
+
unless rate
|
327
|
+
# Tries to calculate an inverse rate
|
328
|
+
inverse_rate = super_get_rate(to_currency, from_currency, opts)
|
329
|
+
if inverse_rate
|
330
|
+
rate = 1.0 / inverse_rate
|
331
|
+
add_rate(from_currency, to_currency, rate)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
rate
|
335
|
+
end
|
336
|
+
|
337
|
+
# Tries to calculate a pair rate using base currency rate
|
338
|
+
# @param [String] from_currency Currency ISO code. ex. 'USD'
|
339
|
+
# @param [String] to_currency Currency ISO code. ex. 'CAD'
|
340
|
+
# @return [Numeric] rate or nil if cannot calculate rate.
|
341
|
+
def calc_pair_rate_using_base(from_currency, to_currency, opts = {})
|
342
|
+
from_base_rate = get_rate_or_calc_inverse(source, from_currency, opts)
|
343
|
+
to_base_rate = get_rate_or_calc_inverse(source, to_currency, opts)
|
344
|
+
if to_base_rate && from_base_rate
|
345
|
+
rate = to_base_rate / from_base_rate
|
346
|
+
add_rate(from_currency, to_currency, rate)
|
347
|
+
return rate
|
348
|
+
end
|
349
|
+
nil
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
your access_key from https://openexchangerates.org/signup
|
data/test/live.json
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
{
|
2
|
+
"disclaimer": "Usage subject to terms: https://openexchangerates.org/terms",
|
3
|
+
"license": "https://openexchangerates.org/license",
|
4
|
+
"timestamp": 1440755169,
|
5
|
+
"base": "USD",
|
6
|
+
"rates": {
|
7
|
+
"AED": 3.673035,
|
8
|
+
"AFN": 68.7835,
|
9
|
+
"ALL": 120.3,
|
10
|
+
"AMD": 485.02,
|
11
|
+
"ANG": 1.789597,
|
12
|
+
"AOA": 165.9145,
|
13
|
+
"ARS": 16.0905,
|
14
|
+
"AUD": 1.337896,
|
15
|
+
"AWG": 1.800506,
|
16
|
+
"AZN": 1.685,
|
17
|
+
"BAM": 1.74995,
|
18
|
+
"BBD": 2,
|
19
|
+
"BDT": 81.152488,
|
20
|
+
"BGN": 1.749004,
|
21
|
+
"BHD": 0.37695,
|
22
|
+
"BIF": 1708.65,
|
23
|
+
"BMD": 1,
|
24
|
+
"BND": 1.389105,
|
25
|
+
"BOB": 6.987637,
|
26
|
+
"BRL": 3.269899,
|
27
|
+
"BSD": 1,
|
28
|
+
"BTC": 0.000424652342,
|
29
|
+
"BTN": 64.758477,
|
30
|
+
"BTS": 10.6793407974,
|
31
|
+
"BWP": 10.39206,
|
32
|
+
"BYN": 1.863063,
|
33
|
+
"BZD": 2.020847,
|
34
|
+
"CAD": 1.350885,
|
35
|
+
"CDF": 1428.863033,
|
36
|
+
"CHF": 0.97632,
|
37
|
+
"CLF": 0.025113,
|
38
|
+
"CLP": 674.325,
|
39
|
+
"CNH": 6.879369,
|
40
|
+
"CNY": 6.8903,
|
41
|
+
"COP": 2910.479832,
|
42
|
+
"CRC": 578.11,
|
43
|
+
"CUC": 1,
|
44
|
+
"CUP": 25.5,
|
45
|
+
"CVE": 99,
|
46
|
+
"CZK": 23.642313,
|
47
|
+
"DASH": 0.006760512,
|
48
|
+
"DJF": 178.77,
|
49
|
+
"DKK": 6.648526,
|
50
|
+
"DOGE": 269.551968987,
|
51
|
+
"DOP": 47.51621,
|
52
|
+
"DZD": 108.444,
|
53
|
+
"EAC": 4732.13456667,
|
54
|
+
"EGP": 18.0813,
|
55
|
+
"EMC": 0.837052105,
|
56
|
+
"ERN": 15.335585,
|
57
|
+
"ETB": 23.124034,
|
58
|
+
"ETH": 0.005376611,
|
59
|
+
"EUR": 0.886584,
|
60
|
+
"FCT": 0.0634141713,
|
61
|
+
"FJD": 2.074747,
|
62
|
+
"FKP": 0.771523,
|
63
|
+
"FTC": 25.1412108028,
|
64
|
+
"GBP": 0.771523,
|
65
|
+
"GEL": 2.40678,
|
66
|
+
"GGP": 0.771523,
|
67
|
+
"GHS": 4.345,
|
68
|
+
"GIP": 0.771523,
|
69
|
+
"GMD": 46.15,
|
70
|
+
"GNF": 9204.45,
|
71
|
+
"GTQ": 7.37438,
|
72
|
+
"GYD": 209.141431,
|
73
|
+
"HKD": 7.788739,
|
74
|
+
"HNL": 23.621643,
|
75
|
+
"HRK": 6.6359,
|
76
|
+
"HTG": 68.95723,
|
77
|
+
"HUF": 275.216,
|
78
|
+
"IDR": 13305.1241,
|
79
|
+
"ILS": 3.59376,
|
80
|
+
"IMP": 0.771523,
|
81
|
+
"INR": 64.7775,
|
82
|
+
"IQD": 1175.736141,
|
83
|
+
"IRR": 32454.938798,
|
84
|
+
"ISK": 100.464841,
|
85
|
+
"JEP": 0.771523,
|
86
|
+
"JMD": 130.676848,
|
87
|
+
"JOD": 0.709503,
|
88
|
+
"JPY": 111.794375,
|
89
|
+
"KES": 103.285,
|
90
|
+
"KGS": 67.885152,
|
91
|
+
"KHR": 4074.45,
|
92
|
+
"KMF": 440.2,
|
93
|
+
"KPW": 899.91,
|
94
|
+
"KRW": 1124.165,
|
95
|
+
"KWD": 0.303677,
|
96
|
+
"KYD": 0.837868,
|
97
|
+
"KZT": 310.93,
|
98
|
+
"LAK": 8237.85,
|
99
|
+
"LBP": 1517.2,
|
100
|
+
"LD": 251.7,
|
101
|
+
"LKR": 152.74,
|
102
|
+
"LRD": 94.497383,
|
103
|
+
"LSL": 13.310652,
|
104
|
+
"LTC": 0.031068873,
|
105
|
+
"LYD": 1.404403,
|
106
|
+
"MAD": 9.76775,
|
107
|
+
"MDL": 18.284966,
|
108
|
+
"MGA": 3086.6,
|
109
|
+
"MKD": 55.054704,
|
110
|
+
"MMK": 1377.4,
|
111
|
+
"MNT": 2410.880742,
|
112
|
+
"MOP": 8.065265,
|
113
|
+
"MRO": 362.423894,
|
114
|
+
"MUR": 34.746,
|
115
|
+
"MVR": 15.570203,
|
116
|
+
"MWK": 726.14,
|
117
|
+
"MXN": 18.589873,
|
118
|
+
"MYR": 4.290006,
|
119
|
+
"MZN": 70.932906,
|
120
|
+
"NAD": 13.02095,
|
121
|
+
"NGN": 320,
|
122
|
+
"NIO": 29.939182,
|
123
|
+
"NMC": 0.4415677667,
|
124
|
+
"NOK": 8.39117,
|
125
|
+
"NPR": 104.278757,
|
126
|
+
"NVC": 0.2067437432,
|
127
|
+
"NXT": 12.0649323229,
|
128
|
+
"NZD": 1.422657,
|
129
|
+
"OMR": 0.384999,
|
130
|
+
"PAB": 1,
|
131
|
+
"PEN": 3.281961,
|
132
|
+
"PGK": 3.191399,
|
133
|
+
"PHP": 49.99,
|
134
|
+
"PKR": 105.39,
|
135
|
+
"PLN": 3.74213,
|
136
|
+
"PPC": 0.4604239038,
|
137
|
+
"PYG": 5622.8,
|
138
|
+
"QAR": 3.6415,
|
139
|
+
"RON": 4.0667,
|
140
|
+
"RSD": 109.81,
|
141
|
+
"RUB": 56.454783,
|
142
|
+
"RWF": 848.135575,
|
143
|
+
"SAR": 3.750449,
|
144
|
+
"SBD": 7.852215,
|
145
|
+
"SCR": 13.488435,
|
146
|
+
"SDG": 6.712818,
|
147
|
+
"SEK": 8.704256,
|
148
|
+
"SGD": 1.388248,
|
149
|
+
"SHP": 0.771523,
|
150
|
+
"SLL": 7502.169019,
|
151
|
+
"SOS": 581.655,
|
152
|
+
"SRD": 7.5375,
|
153
|
+
"SSP": 118.5751,
|
154
|
+
"STD": 21882.388901,
|
155
|
+
"STR": 17.4617511685,
|
156
|
+
"SVC": 8.797503,
|
157
|
+
"SYP": 214.35,
|
158
|
+
"SZL": 13.312834,
|
159
|
+
"THB": 34.37,
|
160
|
+
"TJS": 8.824018,
|
161
|
+
"TMT": 3.504979,
|
162
|
+
"TND": 2.407735,
|
163
|
+
"TOP": 2.311691,
|
164
|
+
"TRY": 3.564739,
|
165
|
+
"TTD": 6.765558,
|
166
|
+
"TWD": 30.17,
|
167
|
+
"TZS": 2237.3,
|
168
|
+
"UAH": 26.265,
|
169
|
+
"UGX": 3631.55,
|
170
|
+
"USD": 1,
|
171
|
+
"UYU": 28.265527,
|
172
|
+
"UZS": 3841.15,
|
173
|
+
"VEF": 9.985022,
|
174
|
+
"VEF_BLKMKT": 5890.73,
|
175
|
+
"VEF_DICOM": 721.33,
|
176
|
+
"VEF_DIPRO": 10,
|
177
|
+
"VND": 22679.057152,
|
178
|
+
"VTC": 2.3128712447,
|
179
|
+
"VUV": 108.260636,
|
180
|
+
"WST": 2.568925,
|
181
|
+
"XAF": 586.424407,
|
182
|
+
"XAG": 0.05858746,
|
183
|
+
"XAU": 0.00079841,
|
184
|
+
"XCD": 2.70255,
|
185
|
+
"XDR": 0.722859,
|
186
|
+
"XMR": 0.0177529017,
|
187
|
+
"XOF": 586.982338,
|
188
|
+
"XPD": 0.00129134,
|
189
|
+
"XPF": 106.897736,
|
190
|
+
"XPM": 1.4437020712,
|
191
|
+
"XPT": 0.00105933,
|
192
|
+
"XRP": 3.054084697,
|
193
|
+
"YER": 250.294142,
|
194
|
+
"ZAR": 12.963878,
|
195
|
+
"ZMW": 9.315,
|
196
|
+
"ZWL": 322.387247
|
197
|
+
}
|
198
|
+
}
|
@@ -0,0 +1,305 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
5
|
+
|
6
|
+
describe Money::Bank::OpenexchangeratesBank do
|
7
|
+
subject { Money::Bank::OpenexchangeratesBank.new }
|
8
|
+
let(:url) { Money::Bank::OpenexchangeratesBank::CL_URL }
|
9
|
+
let(:secure_url) { Money::Bank::OpenexchangeratesBank::CL_SECURE_URL }
|
10
|
+
let(:source) { Money::Bank::OpenexchangeratesBank::CL_SOURCE }
|
11
|
+
let(:temp_cache_path) do
|
12
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'temp.json'))
|
13
|
+
end
|
14
|
+
let(:data_path) do
|
15
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'live.json'))
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'exchange' do
|
19
|
+
before do
|
20
|
+
subject.access_key = TEST_ACCESS_KEY
|
21
|
+
subject.cache = temp_cache_path
|
22
|
+
stub(subject).source_url { data_path }
|
23
|
+
subject.update_rates
|
24
|
+
end
|
25
|
+
|
26
|
+
after do
|
27
|
+
File.unlink(temp_cache_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'without rates' do
|
31
|
+
it 'able to exchange a money to its own currency even without rates' do
|
32
|
+
money = Money.new(0, 'USD')
|
33
|
+
subject.exchange_with(money, 'USD').must_equal money
|
34
|
+
end
|
35
|
+
|
36
|
+
it "raise if it can't find an exchange rate" do
|
37
|
+
money = Money.new(0, 'USD')
|
38
|
+
proc { subject.exchange_with(money, 'LTL') }
|
39
|
+
.must_raise Money::Bank::UnknownRate
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'with rates' do
|
44
|
+
before do
|
45
|
+
subject.update_rates
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should be able to exchange money from USD to a known exchange rate' do
|
49
|
+
money = Money.new(100, 'USD')
|
50
|
+
subject.exchange_with(money, 'BBD').must_equal Money.new(200, 'BBD')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should be able to exchange money from a known exchange rate to USD' do
|
54
|
+
money = Money.new(200, 'BBD')
|
55
|
+
subject.exchange_with(money, 'USD').must_equal Money.new(100, 'USD')
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should raise if it can't find an exchange rate" do
|
59
|
+
money = Money.new(0, 'USD')
|
60
|
+
proc { subject.exchange_with(money, 'LTL') }
|
61
|
+
.must_raise Money::Bank::UnknownRate
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'cache rates' do
|
67
|
+
before do
|
68
|
+
subject.access_key = TEST_ACCESS_KEY
|
69
|
+
subject.cache = temp_cache_path
|
70
|
+
stub(subject).source_url { data_path }
|
71
|
+
subject.update_rates
|
72
|
+
end
|
73
|
+
|
74
|
+
after do
|
75
|
+
File.delete(temp_cache_path) if File.exist?(temp_cache_path)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should allow update after save' do
|
79
|
+
begin
|
80
|
+
subject.update_rates
|
81
|
+
rescue
|
82
|
+
assert false, 'Should allow updating after saving'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should not break an existing file if save fails to read' do
|
87
|
+
initial_size = File.read(temp_cache_path).size
|
88
|
+
stub(subject).open_url { '' }
|
89
|
+
subject.update_rates
|
90
|
+
File.read(temp_cache_path).size.must_equal initial_size
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should not break an existing file if save returns json without rates' do
|
94
|
+
initial_size = File.read(temp_cache_path).size
|
95
|
+
stub(subject).open_url { '{ "error": "An error" }' }
|
96
|
+
subject.update_rates
|
97
|
+
File.read(temp_cache_path).size.must_equal initial_size
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should not break an existing file if save returns a invalid json' do
|
101
|
+
initial_size = File.read(temp_cache_path).size
|
102
|
+
stub(subject).open_url { '{ invalid_json: "An error" }' }
|
103
|
+
subject.update_rates
|
104
|
+
File.read(temp_cache_path).size.must_equal initial_size
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'no cache' do
|
109
|
+
before do
|
110
|
+
subject.cache = nil
|
111
|
+
subject.access_key = TEST_ACCESS_KEY
|
112
|
+
stub(subject).source_url { data_path }
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should get from url' do
|
116
|
+
subject.update_rates
|
117
|
+
subject.rates.wont_be_empty
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'no valid file for cache' do
|
122
|
+
before do
|
123
|
+
subject.cache = "space_dir#{rand(999_999_999)}/out_space_file.json"
|
124
|
+
subject.access_key = TEST_ACCESS_KEY
|
125
|
+
stub(subject).source_url { data_path }
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should raise an error if invalid path is given' do
|
129
|
+
proc { subject.update_rates }.must_raise Money::Bank::InvalidCache
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe 'using proc for cache' do
|
134
|
+
before :each do
|
135
|
+
@global_rates = nil
|
136
|
+
subject.cache = proc { |v|
|
137
|
+
if v
|
138
|
+
@global_rates = v
|
139
|
+
else
|
140
|
+
@global_rates
|
141
|
+
end
|
142
|
+
}
|
143
|
+
subject.access_key = TEST_ACCESS_KEY
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should get from url normally' do
|
147
|
+
stub(subject).source_url { data_path }
|
148
|
+
subject.update_rates
|
149
|
+
subject.rates.wont_be_empty
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should save from url and get from cache' do
|
153
|
+
stub(subject).source_url { data_path }
|
154
|
+
subject.update_rates
|
155
|
+
@global_rates.wont_be_empty
|
156
|
+
dont_allow(subject).source_url
|
157
|
+
subject.update_rates
|
158
|
+
subject.rates.wont_be_empty
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe '#secure_connection' do
|
163
|
+
it "should use the non-secure http url if secure_connection isn't set" do
|
164
|
+
subject.secure_connection = nil
|
165
|
+
subject.access_key = TEST_ACCESS_KEY
|
166
|
+
subject.source_url.must_equal "#{url}?base=#{source}&"\
|
167
|
+
"app_id=#{TEST_ACCESS_KEY}&"\
|
168
|
+
'prettyprint=0'
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should use the non-secure http url if secure_connection is false' do
|
172
|
+
subject.secure_connection = false
|
173
|
+
subject.access_key = TEST_ACCESS_KEY
|
174
|
+
subject.source_url.must_equal "#{url}?base=#{source}&"\
|
175
|
+
"app_id=#{TEST_ACCESS_KEY}&"\
|
176
|
+
'prettyprint=0'
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'should use the secure https url if secure_connection is set to true' do
|
180
|
+
subject.secure_connection = true
|
181
|
+
subject.access_key = TEST_ACCESS_KEY
|
182
|
+
subject.source_url.must_equal "#{secure_url}?base=#{source}&"\
|
183
|
+
"app_id=#{TEST_ACCESS_KEY}&"\
|
184
|
+
'prettyprint=0'
|
185
|
+
subject.source_url.must_include 'https://'
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe '#update_rates' do
|
190
|
+
before do
|
191
|
+
subject.access_key = TEST_ACCESS_KEY
|
192
|
+
subject.cache = data_path
|
193
|
+
stub(subject).source_url { data_path }
|
194
|
+
subject.update_rates
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'should update itself with exchange rates from OpenexchangeratesBank' do
|
198
|
+
subject.rates.keys.each do |currency|
|
199
|
+
next unless Money::Currency.find(currency)
|
200
|
+
subject.get_rate('USD', currency).must_be :>, 0
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should not return 0 with integer rate' do
|
205
|
+
wtf = {
|
206
|
+
priority: 1,
|
207
|
+
iso_code: 'WTF',
|
208
|
+
name: 'WTF',
|
209
|
+
symbol: 'WTF',
|
210
|
+
subunit: 'Cent',
|
211
|
+
subunit_to_unit: 1000,
|
212
|
+
separator: '.',
|
213
|
+
delimiter: ','
|
214
|
+
}
|
215
|
+
Money::Currency.register(wtf)
|
216
|
+
Timecop.freeze(subject.rates_timestamp) do
|
217
|
+
subject.add_rate('USD', 'WTF', 2)
|
218
|
+
subject.add_rate('WTF', 'USD', 2)
|
219
|
+
subject.exchange_with(5000.to_money('WTF'), 'USD').cents
|
220
|
+
subject.exchange_with(5000.to_money('WTF'), 'USD').cents.wont_equal 0
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
describe '#access_key' do
|
226
|
+
before do
|
227
|
+
subject.cache = temp_cache_path
|
228
|
+
stub(OpenURI::OpenRead).open(url) { File.read data_path }
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'should raise an error if no access key is set' do
|
232
|
+
proc { subject.update_rates }.must_raise Money::Bank::NoAccessKey
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe '#expire_rates!' do
|
237
|
+
before do
|
238
|
+
subject.access_key = TEST_ACCESS_KEY
|
239
|
+
subject.ttl_in_seconds = 1000
|
240
|
+
@old_usd_eur_rate = 0.655
|
241
|
+
# see test/live.json +59
|
242
|
+
@new_usd_eur_rate = 0.886584
|
243
|
+
subject.cache = temp_cache_path
|
244
|
+
stub(subject).source_url { data_path }
|
245
|
+
subject.update_rates
|
246
|
+
subject.add_rate('USD', 'EUR', @old_usd_eur_rate)
|
247
|
+
end
|
248
|
+
|
249
|
+
after do
|
250
|
+
File.delete(temp_cache_path) if File.exist?(temp_cache_path)
|
251
|
+
end
|
252
|
+
|
253
|
+
describe 'when the ttl has expired' do
|
254
|
+
it 'should update the rates' do
|
255
|
+
Timecop.freeze(subject.rates_timestamp + 1000) do
|
256
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
257
|
+
end
|
258
|
+
Timecop.freeze(subject.rates_timestamp + 1001) do
|
259
|
+
subject.get_rate('USD', 'EUR').wont_equal @old_usd_eur_rate
|
260
|
+
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'updates the next expiration time' do
|
265
|
+
Timecop.freeze(subject.rates_timestamp + 1001) do
|
266
|
+
exp_time = subject.rates_timestamp + 1000
|
267
|
+
subject.expire_rates!
|
268
|
+
subject.rates_expiration.must_equal exp_time
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
describe 'when the ttl has not expired' do
|
274
|
+
it 'not should update the rates' do
|
275
|
+
subject.update_rates
|
276
|
+
exp_time = subject.rates_expiration
|
277
|
+
subject.expire_rates!
|
278
|
+
subject.rates_expiration.must_equal exp_time
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe '#rates_timestamp' do
|
284
|
+
before do
|
285
|
+
subject.access_key = TEST_ACCESS_KEY
|
286
|
+
subject.cache = temp_cache_path
|
287
|
+
stub(subject).source_url { data_path }
|
288
|
+
end
|
289
|
+
|
290
|
+
after do
|
291
|
+
File.delete(temp_cache_path) if File.exist?(temp_cache_path)
|
292
|
+
end
|
293
|
+
|
294
|
+
it 'should return 1970-01-01 datetime if no rates' do
|
295
|
+
stub(subject).open_url { '' }
|
296
|
+
subject.update_rates
|
297
|
+
subject.rates_timestamp.must_equal Time.at(0)
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'should return a Time object' do
|
301
|
+
subject.update_rates
|
302
|
+
subject.rates_timestamp.class.must_equal Time
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'rr'
|
6
|
+
require 'money/bank/openexchangerates_bank'
|
7
|
+
require 'monetize'
|
8
|
+
require 'timecop'
|
9
|
+
require 'pry'
|
10
|
+
|
11
|
+
TEST_ACCESS_KEY_PATH = File.join(File.dirname(__FILE__), 'TEST_ACCESS_KEY')
|
12
|
+
TEST_ACCESS_KEY = ENV['TEST_ACCESS_KEY'] || File.read(TEST_ACCESS_KEY_PATH)
|
13
|
+
|
14
|
+
if TEST_ACCESS_KEY.nil? || TEST_ACCESS_KEY.empty?
|
15
|
+
raise "Please add a valid access key to file #{TEST_ACCESS_KEY_PATH} or to " \
|
16
|
+
' TEST_ACCESS_KEY environment'
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: money-openexchangerates-bank
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Egon Zemmer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: money
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: monetize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.8'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.8'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-line
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.6'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rr
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '12.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '12.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: timecop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.8.1
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.8.1
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.48.1
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.48.1
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: inch
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.7.1
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.7.1
|
153
|
+
description: A gem that calculates the exchange rate using published rates from openexchangerates.org.
|
154
|
+
Compatible with the money gem.
|
155
|
+
email: office@phlegx.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files:
|
159
|
+
- README.md
|
160
|
+
files:
|
161
|
+
- Gemfile
|
162
|
+
- LICENSE
|
163
|
+
- README.md
|
164
|
+
- lib/money/bank/openexchangerates_bank.rb
|
165
|
+
- test/TEST_ACCESS_KEY
|
166
|
+
- test/live.json
|
167
|
+
- test/openexchangerates_bank_test.rb
|
168
|
+
- test/test_helper.rb
|
169
|
+
homepage: http://github.com/phlegx/money-openexchangerates-bank
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: 1.9.3
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.5.2
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: A gem that calculates the exchange rate using published rates from openexchangerates.org.
|
193
|
+
test_files:
|
194
|
+
- test/openexchangerates_bank_test.rb
|
195
|
+
has_rdoc:
|