money-open-exchange-rates 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.md +7 -0
- data/README.md +40 -9
- data/lib/money/bank/open_exchange_rates_bank.rb +49 -16
- data/lib/open_exchange_rates_bank/version.rb +1 -1
- data/test/open_exchange_rates_bank_test.rb +53 -35
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 533cde694f8fe852a3c3876b5433871d66aa438184978a676296c93867d58132
|
4
|
+
data.tar.gz: 793cc9510199cd8d4da3ac1e05bbd133e154ed167eeee585feeadeb403b0cc36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b8d3e1d9900ebd8ecd0c9d2c723697c0d0ba9416f9ac82c4dc0ed8264ca8822571c841441d445e55ef655fece32019a5c8c397f83ffd27766689128edbe54f0b
|
7
|
+
data.tar.gz: 78cb56294a55bc7f6b7f7daff77f4f5acae9cacd9516cb9e5b85544674ed3dd56898b2d138b2c5af2623ab3d583dc26af55b79d4d4437e4c915ca68b2586aca3
|
data/History.md
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# Money Open Exchange Rates
|
2
2
|
|
3
3
|
A gem that calculates the exchange rate using published rates from
|
4
|
-
[open-exchange-rates](https://openexchangerates.org/)
|
4
|
+
[open-exchange-rates](https://openexchangerates.org/). Compatible with
|
5
|
+
[Money](https://github.com/RubyMoney/money#currency-exchange) [currency
|
6
|
+
exchange](http://www.rubydoc.info/gems/money/Money/Bank/VariableExchange).
|
5
7
|
|
6
8
|
Check [api documentation](https://docs.openexchangerates.org/)
|
7
9
|
|
@@ -42,37 +44,56 @@ gem install money-open-exchange-rates
|
|
42
44
|
|
43
45
|
~~~ ruby
|
44
46
|
require 'money/bank/open_exchange_rates_bank'
|
47
|
+
|
48
|
+
# Memory store per default; for others just pass as argument a class like
|
49
|
+
# explained in https://github.com/RubyMoney/money#exchange-rate-stores
|
45
50
|
oxr = Money::Bank::OpenExchangeRatesBank.new
|
46
|
-
# see https://github.com/spk/money-open-exchange-rates#cache for more info
|
47
|
-
oxr.cache = 'path/to/file/cache.json'
|
48
51
|
oxr.app_id = 'your app id from https://openexchangerates.org/signup'
|
49
52
|
oxr.update_rates
|
50
53
|
|
51
54
|
# (optional)
|
52
|
-
#
|
55
|
+
# See https://github.com/spk/money-open-exchange-rates#cache for more info
|
56
|
+
# Updated only when `refresh_rates` is called
|
57
|
+
oxr.cache = 'path/to/file/cache.json'
|
58
|
+
|
59
|
+
# (optional)
|
60
|
+
# Set the seconds after than the current rates are automatically expired
|
53
61
|
# by default, they never expire, in this example 1 day.
|
62
|
+
# This ttl is about money store (memory, database ...) passed though
|
63
|
+
# `Money::Bank::OpenExchangeRatesBank` as argument not about `cache` option.
|
64
|
+
# The base time is the timestamp fetched from API.
|
54
65
|
oxr.ttl_in_seconds = 86400
|
66
|
+
|
55
67
|
# (optional)
|
56
|
-
#
|
68
|
+
# Set historical date of the rate
|
57
69
|
# see https://openexchangerates.org/documentation#historical-data
|
58
70
|
oxr.date = '2015-01-01'
|
71
|
+
|
59
72
|
# (optional)
|
60
73
|
# Set the base currency for all rates. By default, USD is used.
|
61
74
|
# OpenExchangeRates only allows USD as base currency
|
62
75
|
# for the free plan users.
|
63
76
|
oxr.source = 'USD'
|
77
|
+
|
64
78
|
# (optional)
|
65
79
|
# Extend returned values with alternative, black market and digital currency
|
66
80
|
# rates. By default, false is used
|
67
81
|
# see: https://docs.openexchangerates.org/docs/alternative-currencies
|
68
82
|
oxr.show_alternative = true
|
83
|
+
|
69
84
|
# (optional)
|
70
|
-
#
|
71
|
-
#
|
85
|
+
# Refresh rates, store in cache and update rates
|
86
|
+
# Should be used on crontab/worker/scheduler `Money.default_bank.refresh_rates`
|
72
87
|
# If you are using unicorn-worker-killer gem or on Heroku like platform,
|
73
88
|
# you should avoid to put this on the initializer of your Rails application,
|
74
89
|
# because will increase your OXR API usage.
|
75
|
-
oxr.
|
90
|
+
oxr.refresh_rates
|
91
|
+
|
92
|
+
# (optional)
|
93
|
+
# Force refresh rates cache and store on the fly when ttl is expired
|
94
|
+
# This will slow down request on get_rate, so use at your on risk, if you don't
|
95
|
+
# want to setup crontab/worker/scheduler for your application
|
96
|
+
oxr.force_refresh_rate_on_expire = true
|
76
97
|
|
77
98
|
Money.default_bank = oxr
|
78
99
|
|
@@ -109,22 +130,32 @@ oxr.cache = Proc.new do |text|
|
|
109
130
|
end
|
110
131
|
~~~
|
111
132
|
|
133
|
+
To update the cache call `Money.default_bank.refresh_rates` on
|
134
|
+
crontab/worker/scheduler. This have to be done this way because the fetch can
|
135
|
+
take some time (HTTP call) and can fail.
|
136
|
+
|
112
137
|
## Full example configuration initializer with Rails and cache
|
113
138
|
|
114
139
|
~~~ ruby
|
115
140
|
require 'money/bank/open_exchange_rates_bank'
|
116
141
|
|
117
142
|
OXR_CACHE_KEY = 'money:exchange_rates'.freeze
|
118
|
-
|
143
|
+
# ExchangeRate is an ActiveRecord model
|
144
|
+
# more info at https://github.com/RubyMoney/money#exchange-rate-stores
|
145
|
+
oxr = Money::Bank::OpenExchangeRatesBank.new(ExchangeRate)
|
119
146
|
oxr.ttl_in_seconds = 86400
|
120
147
|
oxr.cache = Proc.new do |text|
|
121
148
|
if text
|
149
|
+
# only expire when refresh_rates is called or `force_refresh_rate_on_expire`
|
150
|
+
# option is enabled
|
151
|
+
# you can also set `expires_in` option on write to force fetch new rates
|
122
152
|
Rails.cache.write(OXR_CACHE_KEY, text)
|
123
153
|
else
|
124
154
|
Rails.cache.read(OXR_CACHE_KEY)
|
125
155
|
end
|
126
156
|
end
|
127
157
|
oxr.app_id = ENV['OXR_API_KEY']
|
158
|
+
oxr.show_alternative = true
|
128
159
|
oxr.update_rates
|
129
160
|
|
130
161
|
Money.default_bank = oxr
|
@@ -28,6 +28,8 @@ class Money
|
|
28
28
|
|
29
29
|
# Default base currency "base": "USD"
|
30
30
|
OE_SOURCE = 'USD'.freeze
|
31
|
+
RATES_KEY = 'rates'.freeze
|
32
|
+
TIMESTAMP_KEY = 'timestamp'.freeze
|
31
33
|
|
32
34
|
# @deprecated secure_connection is deprecated and has no effect
|
33
35
|
def secure_connection=(*)
|
@@ -64,6 +66,13 @@ class Money
|
|
64
66
|
# @return [String] The requested date in YYYY-MM-DD format
|
65
67
|
attr_accessor :date
|
66
68
|
|
69
|
+
# Force refresh rates cache and store on the fly when ttl is expired
|
70
|
+
# This will slow down request on get_rate, so use at your on risk, if you
|
71
|
+
# don't want to setup crontab/worker/scheduler for your application
|
72
|
+
#
|
73
|
+
# @param [Boolean]
|
74
|
+
attr_accessor :force_refresh_rate_on_expire
|
75
|
+
|
67
76
|
# Rates expiration Time
|
68
77
|
#
|
69
78
|
# @return [Time] expiration time
|
@@ -93,6 +102,20 @@ class Money
|
|
93
102
|
# @return [Boolean] Setted show alternative
|
94
103
|
attr_writer :show_alternative
|
95
104
|
|
105
|
+
# Set current rates timestamp
|
106
|
+
#
|
107
|
+
# @return [Time]
|
108
|
+
def rates_timestamp=(at)
|
109
|
+
@rates_timestamp = Time.at(at)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Current rates timestamp
|
113
|
+
#
|
114
|
+
# @return [Time]
|
115
|
+
def rates_timestamp
|
116
|
+
@rates_timestamp || Time.at(0)
|
117
|
+
end
|
118
|
+
|
96
119
|
# Set the seconds after than the current rates are automatically expired
|
97
120
|
# by default, they never expire.
|
98
121
|
#
|
@@ -144,17 +167,6 @@ class Money
|
|
144
167
|
end
|
145
168
|
end
|
146
169
|
|
147
|
-
# Save rates on cache
|
148
|
-
# Can raise InvalidCache
|
149
|
-
#
|
150
|
-
# @return [Proc,File]
|
151
|
-
def save_rates
|
152
|
-
return nil unless cache
|
153
|
-
store_in_cache(@json_response) if valid_rates?(@json_response)
|
154
|
-
rescue Errno::ENOENT
|
155
|
-
raise InvalidCache
|
156
|
-
end
|
157
|
-
|
158
170
|
# Alias super method
|
159
171
|
alias super_get_rate get_rate
|
160
172
|
|
@@ -171,13 +183,23 @@ class Money
|
|
171
183
|
rate || calc_pair_rate_using_base(from_currency, to_currency, opts)
|
172
184
|
end
|
173
185
|
|
186
|
+
# Fetch from url and save cache
|
187
|
+
#
|
188
|
+
# @return [Array] Array of exchange rates
|
189
|
+
def refresh_rates
|
190
|
+
read_from_url
|
191
|
+
end
|
192
|
+
|
193
|
+
# Alias refresh_rates method
|
194
|
+
alias save_rates refresh_rates
|
195
|
+
|
174
196
|
# Expire rates when expired
|
175
197
|
#
|
176
198
|
# @return [NilClass, Time] nil if not expired or new expiration time
|
177
199
|
def expire_rates
|
178
200
|
return unless ttl_in_seconds
|
179
201
|
return if rates_expiration > Time.now
|
180
|
-
|
202
|
+
refresh_rates if force_refresh_rate_on_expire
|
181
203
|
update_rates
|
182
204
|
refresh_rates_expiration
|
183
205
|
end
|
@@ -205,6 +227,16 @@ class Money
|
|
205
227
|
|
206
228
|
protected
|
207
229
|
|
230
|
+
# Save rates on cache
|
231
|
+
# Can raise InvalidCache
|
232
|
+
#
|
233
|
+
# @return [Proc,File]
|
234
|
+
def save_cache
|
235
|
+
store_in_cache(@json_response) if valid_rates?(@json_response)
|
236
|
+
rescue Errno::ENOENT
|
237
|
+
raise InvalidCache
|
238
|
+
end
|
239
|
+
|
208
240
|
# Latest url if no date given
|
209
241
|
#
|
210
242
|
# @return [String] URL
|
@@ -266,7 +298,7 @@ class Money
|
|
266
298
|
def read_from_url
|
267
299
|
raise NoAppId if app_id.nil? || app_id.empty?
|
268
300
|
@json_response = open(source_url).read
|
269
|
-
|
301
|
+
save_cache if cache
|
270
302
|
@json_response
|
271
303
|
end
|
272
304
|
|
@@ -280,7 +312,7 @@ class Money
|
|
280
312
|
def valid_rates?(text)
|
281
313
|
return false unless text
|
282
314
|
parsed = JSON.parse(text)
|
283
|
-
parsed && parsed.key?(
|
315
|
+
parsed && parsed.key?(RATES_KEY) && parsed.key?(TIMESTAMP_KEY)
|
284
316
|
rescue JSON::ParserError
|
285
317
|
false
|
286
318
|
end
|
@@ -290,14 +322,15 @@ class Money
|
|
290
322
|
# @return [Hash] key is country code (ISO 3166-1 alpha-3) value Float
|
291
323
|
def exchange_rates
|
292
324
|
doc = JSON.parse(read_from_cache || read_from_url)
|
293
|
-
|
325
|
+
self.rates_timestamp = doc[TIMESTAMP_KEY]
|
326
|
+
@oer_rates = doc[RATES_KEY]
|
294
327
|
end
|
295
328
|
|
296
329
|
# Refresh expiration from now
|
297
330
|
#
|
298
331
|
# @return [Time] new expiration time
|
299
332
|
def refresh_rates_expiration
|
300
|
-
@rates_expiration =
|
333
|
+
@rates_expiration = rates_timestamp + ttl_in_seconds
|
301
334
|
end
|
302
335
|
|
303
336
|
# Get rate or calculate it as inverse rate
|
@@ -23,7 +23,6 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
23
23
|
add_to_webmock(subject)
|
24
24
|
subject.cache = temp_cache_path
|
25
25
|
subject.update_rates
|
26
|
-
subject.save_rates
|
27
26
|
end
|
28
27
|
|
29
28
|
after do
|
@@ -148,13 +147,10 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
148
147
|
end
|
149
148
|
|
150
149
|
it 'should get from url' do
|
150
|
+
dont_allow(subject).save_cache
|
151
151
|
subject.update_rates
|
152
152
|
subject.oer_rates.wont_be_empty
|
153
153
|
end
|
154
|
-
|
155
|
-
it 'should return nil when cache is nil' do
|
156
|
-
subject.save_rates.must_equal nil
|
157
|
-
end
|
158
154
|
end
|
159
155
|
|
160
156
|
describe 'secure_connection' do
|
@@ -197,7 +193,7 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
197
193
|
add_to_webmock(subject)
|
198
194
|
end
|
199
195
|
|
200
|
-
it 'should raise an error if invalid path is given to
|
196
|
+
it 'should raise an error if invalid path is given to update_rates' do
|
201
197
|
proc { subject.update_rates }.must_raise Money::Bank::InvalidCache
|
202
198
|
end
|
203
199
|
end
|
@@ -221,7 +217,6 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
221
217
|
end
|
222
218
|
|
223
219
|
it 'should save from url and get from cache' do
|
224
|
-
subject.save_rates
|
225
220
|
@global_rates.wont_be_empty
|
226
221
|
dont_allow(subject).source_url
|
227
222
|
subject.update_rates
|
@@ -229,12 +224,11 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
229
224
|
end
|
230
225
|
end
|
231
226
|
|
232
|
-
describe '
|
227
|
+
describe '#refresh_rates' do
|
233
228
|
before do
|
234
229
|
add_to_webmock(subject)
|
235
230
|
subject.cache = temp_cache_path
|
236
231
|
subject.update_rates
|
237
|
-
subject.save_rates
|
238
232
|
end
|
239
233
|
|
240
234
|
after do
|
@@ -243,7 +237,7 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
243
237
|
|
244
238
|
it 'should allow update after save' do
|
245
239
|
begin
|
246
|
-
subject.
|
240
|
+
subject.refresh_rates
|
247
241
|
rescue
|
248
242
|
assert false, 'Should allow updating after saving'
|
249
243
|
end
|
@@ -252,31 +246,45 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
252
246
|
it 'should not break an existing file if save fails to read' do
|
253
247
|
initial_size = File.read(temp_cache_path).size
|
254
248
|
stub(subject).read_from_url { '' }
|
255
|
-
subject.
|
249
|
+
subject.refresh_rates
|
256
250
|
File.open(temp_cache_path).read.size.must_equal initial_size
|
257
251
|
end
|
258
252
|
|
259
253
|
it 'should not break an existing file if save returns json without rates' do
|
260
254
|
initial_size = File.read(temp_cache_path).size
|
261
255
|
stub(subject).read_from_url { '{"error": "An error"}' }
|
262
|
-
subject.
|
256
|
+
subject.refresh_rates
|
263
257
|
File.open(temp_cache_path).read.size.must_equal initial_size
|
264
258
|
end
|
265
259
|
|
266
260
|
it 'should not break an existing file if save returns a invalid json' do
|
267
261
|
initial_size = File.read(temp_cache_path).size
|
268
262
|
stub(subject).read_from_url { '{invalid_json: "An error"}' }
|
269
|
-
subject.
|
263
|
+
subject.refresh_rates
|
270
264
|
File.open(temp_cache_path).read.size.must_equal initial_size
|
271
265
|
end
|
272
266
|
end
|
273
267
|
|
268
|
+
describe '#rates_timestamp' do
|
269
|
+
before do
|
270
|
+
add_to_webmock(subject)
|
271
|
+
end
|
272
|
+
|
273
|
+
it 'should be set on update_rates' do
|
274
|
+
subject.update_rates
|
275
|
+
subject.rates_timestamp.must_equal Time.at(1_414_008_044)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
274
279
|
describe '#expire_rates' do
|
275
280
|
before do
|
276
281
|
add_to_webmock(subject)
|
277
|
-
|
282
|
+
# see test/data/latest.json +4
|
283
|
+
subject.rates_timestamp = 1_414_008_044
|
284
|
+
@ttl_in_seconds = 1000
|
285
|
+
subject.ttl_in_seconds = @ttl_in_seconds
|
278
286
|
@old_usd_eur_rate = 0.655
|
279
|
-
# see test/latest.json +52
|
287
|
+
# see test/data/latest.json +52
|
280
288
|
@new_usd_eur_rate = 0.79085
|
281
289
|
subject.add_rate('USD', 'EUR', @old_usd_eur_rate)
|
282
290
|
@global_rates = nil
|
@@ -291,47 +299,57 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
291
299
|
|
292
300
|
describe 'when the ttl has expired' do
|
293
301
|
it 'should update the rates' do
|
294
|
-
subject.
|
295
|
-
|
302
|
+
Timecop.freeze(subject.rates_timestamp) do
|
303
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
304
|
+
end
|
305
|
+
Timecop.freeze(subject.rates_timestamp + (@ttl_in_seconds + 1)) do
|
296
306
|
subject.get_rate('USD', 'EUR').wont_equal @old_usd_eur_rate
|
297
307
|
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
298
308
|
end
|
299
309
|
end
|
300
310
|
|
301
311
|
it 'should save rates' do
|
302
|
-
subject.
|
303
|
-
|
304
|
-
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
305
|
-
@global_rates.wont_be_empty
|
312
|
+
Timecop.freeze(subject.rates_timestamp) do
|
313
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
306
314
|
end
|
307
|
-
|
308
|
-
|
309
|
-
it 'should save rates and refresh it when cache is invalid' do
|
310
|
-
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
311
|
-
Timecop.freeze(Time.now + 1001) do
|
312
|
-
@global_rates = []
|
315
|
+
Timecop.freeze(subject.rates_timestamp + (@ttl_in_seconds + 1)) do
|
313
316
|
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
314
317
|
@global_rates.wont_be_empty
|
315
318
|
end
|
316
319
|
end
|
317
320
|
|
318
321
|
it 'updates the next expiration time' do
|
319
|
-
Timecop.freeze(
|
320
|
-
exp_time =
|
322
|
+
Timecop.freeze(subject.rates_timestamp + (@ttl_in_seconds + 1)) do
|
323
|
+
exp_time = subject.rates_timestamp + @ttl_in_seconds
|
321
324
|
subject.expire_rates
|
322
325
|
subject.rates_expiration.must_equal exp_time
|
323
326
|
end
|
324
327
|
end
|
328
|
+
|
329
|
+
describe '#force_refresh_rate_on_expire' do
|
330
|
+
it 'should save rates and force refresh' do
|
331
|
+
subject.force_refresh_rate_on_expire = true
|
332
|
+
Timecop.freeze(subject.rates_timestamp) do
|
333
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
334
|
+
end
|
335
|
+
Timecop.freeze(Time.now + 1001) do
|
336
|
+
@global_rates = []
|
337
|
+
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
338
|
+
@global_rates.wont_be_empty
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
325
342
|
end
|
326
343
|
|
327
344
|
describe 'when the ttl has not expired' do
|
328
|
-
it 'not
|
345
|
+
it 'should not update the rates' do
|
329
346
|
exp_time = subject.rates_expiration
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
347
|
+
Timecop.freeze(subject.rates_timestamp) do
|
348
|
+
dont_allow(subject).update_rates
|
349
|
+
dont_allow(subject).refresh_rates_expiration
|
350
|
+
subject.expire_rates
|
351
|
+
subject.rates_expiration.must_equal exp_time
|
352
|
+
end
|
335
353
|
end
|
336
354
|
end
|
337
355
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: money-open-exchange-rates
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Laurent Arnoud
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-03-
|
11
|
+
date: 2018-03-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: money
|