money-open-exchange-rates 0.7.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/History.md +13 -0
- data/LICENSE +1 -1
- data/README.md +54 -8
- data/lib/money/bank/open_exchange_rates_bank.rb +39 -26
- data/lib/open_exchange_rates_bank/version.rb +1 -1
- data/test/open_exchange_rates_bank_test.rb +67 -68
- data/test/open_exchange_rates_bank_test.rb.orig +393 -0
- data/test/test_helper.rb +1 -0
- metadata +15 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1cbadeb275d02260424c03168a97965ac705de08b518e2310a58b7dd649a8b53
|
4
|
+
data.tar.gz: 85467d626de3da3637f10a2e9113899ea3ca40df60c845f3fef2b251c618810b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2082ea6e2ec8df6b788bda461537b1dd70896d1f849b3b4e644b9fd30a2ee3c5fa1c8168999e4555dabd9b2cf4d2a2b1c714b6c0ec61ee730236c7cd6f7426e9
|
7
|
+
data.tar.gz: 5128bf8ee272026571487a92828550b7e3de82f9d7bd409d014048f1cb2413a26b21fd4261e07df9bfa3105cc0ef1700686427adcfae7adfa37125199a2b056f
|
data/History.md
CHANGED
@@ -1,4 +1,17 @@
|
|
1
1
|
|
2
|
+
v1.0.0 / 2018-03-25
|
3
|
+
===================
|
4
|
+
|
5
|
+
* Merge pull request #41 from @b-mandelbrot /add-show-alternative
|
6
|
+
* Add support for black market and digital currency rates
|
7
|
+
* Merge pull request #42 from spk/save-rates-when-ttl-expire
|
8
|
+
* Save rates to cache after first fetch and add example with Rails
|
9
|
+
* Improve documation about cache and rates ttl
|
10
|
+
* Save rates when ttl expire
|
11
|
+
* Merge pull request #40 from @Jetbuilt / deprecate-secure_connection
|
12
|
+
* Closes #39 - Make all requests over https and deprecate `secure_connection`
|
13
|
+
* Support Ruby >= 2.0
|
14
|
+
|
2
15
|
v0.7.0 / 2016-10-23
|
3
16
|
===================
|
4
17
|
|
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License
|
2
2
|
|
3
|
-
Copyright (c) 2011-
|
3
|
+
Copyright (c) 2011-2018 Laurent Arnoud <laurent@spkdev.net>
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining
|
6
6
|
a copy of this software and associated documentation files (the
|
data/README.md
CHANGED
@@ -11,8 +11,12 @@ Check [api documentation](https://docs.openexchangerates.org/)
|
|
11
11
|
[180 currencies](https://docs.openexchangerates.org/docs/supported-currencies).
|
12
12
|
* [Free plan](https://openexchangerates.org/signup) hourly updates, with USD
|
13
13
|
base and up to 1,000 requests/month.
|
14
|
-
*
|
14
|
+
* Automatically caches API results to file or Rails cache.
|
15
15
|
* Calculate pair rates.
|
16
|
+
* Automatically fetches new data from API if data becomes stale when
|
17
|
+
`ttl_in_seconds` option is provided.
|
18
|
+
* Support for black market and digital currency rates with `show_alternative`
|
19
|
+
option.
|
16
20
|
|
17
21
|
## Installation
|
18
22
|
|
@@ -48,11 +52,6 @@ oxr.update_rates
|
|
48
52
|
# by default, they never expire, in this example 1 day.
|
49
53
|
oxr.ttl_in_seconds = 86400
|
50
54
|
# (optional)
|
51
|
-
# use https to fetch rates from Open Exchange Rates
|
52
|
-
# disabled by default to support free-tier users
|
53
|
-
# see https://openexchangerates.org/documentation#https
|
54
|
-
oxr.secure_connection = true
|
55
|
-
# (optional)
|
56
55
|
# set historical date of the rate
|
57
56
|
# see https://openexchangerates.org/documentation#historical-data
|
58
57
|
oxr.date = '2015-01-01'
|
@@ -61,8 +60,17 @@ oxr.date = '2015-01-01'
|
|
61
60
|
# OpenExchangeRates only allows USD as base currency
|
62
61
|
# for the free plan users.
|
63
62
|
oxr.source = 'USD'
|
63
|
+
# (optional)
|
64
|
+
# Extend returned values with alternative, black market and digital currency
|
65
|
+
# rates. By default, false is used
|
66
|
+
# see: https://docs.openexchangerates.org/docs/alternative-currencies
|
67
|
+
oxr.show_alternative = true
|
64
68
|
|
65
69
|
# Store in cache
|
70
|
+
# Force rates storage in cache, this is done automaticly after TTL is expire.
|
71
|
+
# If you are using unicorn-worker-killer gem or on Heroku like platform,
|
72
|
+
# you should avoid to put this on the initializer of your Rails application,
|
73
|
+
# because will increase your OXR API usage.
|
66
74
|
oxr.save_rates
|
67
75
|
|
68
76
|
Money.default_bank = oxr
|
@@ -70,7 +78,7 @@ Money.default_bank = oxr
|
|
70
78
|
Money.default_bank.get_rate('USD', 'CAD')
|
71
79
|
~~~
|
72
80
|
|
73
|
-
You can also provide a Proc as a cache to provide your own caching mechanism
|
81
|
+
You can also provide a `Proc` as a cache to provide your own caching mechanism
|
74
82
|
perhaps with Redis or just a thread safe `Hash` (global). For example:
|
75
83
|
|
76
84
|
~~~ ruby
|
@@ -84,9 +92,47 @@ oxr.cache = Proc.new do |v|
|
|
84
92
|
end
|
85
93
|
~~~
|
86
94
|
|
95
|
+
With `Rails` cache example:
|
96
|
+
|
97
|
+
~~~ ruby
|
98
|
+
OXR_CACHE_KEY = 'money:exchange_rates'.freeze
|
99
|
+
OXR_CACHE_TTL = 10
|
100
|
+
# using same ttl with refreshing current rates and cache
|
101
|
+
oxr.ttl_in_seconds = OXR_CACHE_TTL
|
102
|
+
oxr.cache = Proc.new do |text|
|
103
|
+
if text && !Rails.cache.exist?(OXR_CACHE_KEY)
|
104
|
+
Rails.cache.write(OXR_CACHE_KEY, text, expires_in: OXR_CACHE_TTL)
|
105
|
+
else
|
106
|
+
Rails.cache.read(OXR_CACHE_KEY)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
~~~
|
110
|
+
|
87
111
|
Unknown pair rates are transparently calculated: using inverse rate (if known),
|
88
112
|
or using base currency rate to both currencies forming the pair.
|
89
113
|
|
114
|
+
## Full example configuration initializer with Rails and cache
|
115
|
+
|
116
|
+
~~~
|
117
|
+
require 'money/bank/open_exchange_rates_bank'
|
118
|
+
|
119
|
+
OXR_CACHE_KEY = 'money:exchange_rates'.freeze
|
120
|
+
OXR_CACHE_TTL = 10
|
121
|
+
oxr = Money::Bank::OpenExchangeRatesBank.new
|
122
|
+
oxr.ttl_in_seconds = OXR_CACHE_TTL
|
123
|
+
oxr.cache = Proc.new do |text|
|
124
|
+
if text && !Rails.cache.exist?(OXR_CACHE_KEY)
|
125
|
+
Rails.cache.write(OXR_CACHE_KEY, text, expires_in: OXR_CACHE_TTL)
|
126
|
+
else
|
127
|
+
Rails.cache.read(OXR_CACHE_KEY)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
oxr.app_id = ENV['OXR_API_KEY']
|
131
|
+
oxr.update_rates
|
132
|
+
|
133
|
+
Money.default_bank = oxr
|
134
|
+
~~~
|
135
|
+
|
90
136
|
## Tests
|
91
137
|
|
92
138
|
~~~
|
@@ -108,7 +154,7 @@ See [GitHub](https://github.com/spk/money-open-exchange-rates/graphs/contributor
|
|
108
154
|
|
109
155
|
The MIT License
|
110
156
|
|
111
|
-
Copyright © 2011-
|
157
|
+
Copyright © 2011-2018 Laurent Arnoud <laurent@spkdev.net>
|
112
158
|
|
113
159
|
---
|
114
160
|
[](https://travis-ci.org/spk/money-open-exchange-rates)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'uri'
|
3
4
|
require 'open-uri'
|
4
5
|
require 'money'
|
@@ -19,28 +20,19 @@ class Money
|
|
19
20
|
# OpenExchangeRatesBank base class
|
20
21
|
class OpenExchangeRatesBank < Money::Bank::VariableExchange
|
21
22
|
VERSION = ::OpenExchangeRatesBank::VERSION
|
22
|
-
BASE_URL = '
|
23
|
+
BASE_URL = 'https://openexchangerates.org/api/'.freeze
|
24
|
+
|
23
25
|
# OpenExchangeRates urls
|
24
26
|
OER_URL = URI.join(BASE_URL, 'latest.json')
|
25
27
|
OER_HISTORICAL_URL = URI.join(BASE_URL, 'historical/')
|
26
|
-
# OpenExchangeRates secure url
|
27
|
-
SECURE_OER_URL = OER_URL.clone
|
28
|
-
SECURE_OER_URL.scheme = 'https'
|
29
|
-
SECURE_OER_HISTORICAL_URL = OER_HISTORICAL_URL.clone
|
30
|
-
SECURE_OER_HISTORICAL_URL.scheme = 'https'
|
31
28
|
|
32
29
|
# Default base currency "base": "USD"
|
33
30
|
OE_SOURCE = 'USD'.freeze
|
34
31
|
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# oxr.secure_connection = true
|
40
|
-
#
|
41
|
-
# @param [Boolean] true for https, false for http
|
42
|
-
# @return [Boolean] true for https, false for http
|
43
|
-
attr_accessor :secure_connection
|
32
|
+
# @deprecated secure_connection is deprecated and has no effect
|
33
|
+
def secure_connection=(*)
|
34
|
+
'secure_connection is deprecated and has no effect'
|
35
|
+
end
|
44
36
|
|
45
37
|
# As of the end of August 2012 all requests to the Open Exchange Rates
|
46
38
|
# API must have a valid app_id
|
@@ -82,11 +74,25 @@ class Money
|
|
82
74
|
# @return [Hash] All rates as Hash
|
83
75
|
attr_reader :oer_rates
|
84
76
|
|
77
|
+
# Unparsed OpenExchangeRates response as String
|
78
|
+
#
|
79
|
+
# @return [String] OpenExchangeRates json response
|
80
|
+
attr_reader :json_response
|
81
|
+
|
85
82
|
# Seconds after than the current rates are automatically expired
|
86
83
|
#
|
87
84
|
# @return [Integer] Setted time to live in seconds
|
88
85
|
attr_reader :ttl_in_seconds
|
89
86
|
|
87
|
+
# Set support for the black market and alternative digital currencies
|
88
|
+
# see https://docs.openexchangerates.org/docs/alternative-currencies
|
89
|
+
# @example
|
90
|
+
# oxr.show_alternative = true
|
91
|
+
#
|
92
|
+
# @param [Boolean] if true show alternative
|
93
|
+
# @return [Boolean] Setted show alternative
|
94
|
+
attr_writer :show_alternative
|
95
|
+
|
90
96
|
# Set the seconds after than the current rates are automatically expired
|
91
97
|
# by default, they never expire.
|
92
98
|
#
|
@@ -143,9 +149,8 @@ class Money
|
|
143
149
|
#
|
144
150
|
# @return [Proc,File]
|
145
151
|
def save_rates
|
146
|
-
|
147
|
-
|
148
|
-
store_in_cache(text) if valid_rates?(text)
|
152
|
+
return nil unless cache
|
153
|
+
store_in_cache(@json_response) if valid_rates?(@json_response)
|
149
154
|
rescue Errno::ENOENT
|
150
155
|
raise InvalidCache
|
151
156
|
end
|
@@ -176,15 +181,24 @@ class Money
|
|
176
181
|
refresh_rates_expiration
|
177
182
|
end
|
178
183
|
|
184
|
+
# Get show alternative
|
185
|
+
#
|
186
|
+
# @return [Boolean] if true show alternative
|
187
|
+
def show_alternative
|
188
|
+
@show_alternative ||= false
|
189
|
+
end
|
190
|
+
|
179
191
|
# Source url of openexchangerates
|
180
|
-
# defined with app_id
|
192
|
+
# defined with app_id
|
181
193
|
#
|
182
194
|
# @return [String] URL
|
183
195
|
def source_url
|
184
196
|
if source == OE_SOURCE
|
185
|
-
"#{oer_url}?app_id=#{app_id}"
|
197
|
+
"#{oer_url}?app_id=#{app_id}" \
|
198
|
+
"&show_alternative=#{show_alternative}"
|
186
199
|
else
|
187
|
-
"#{oer_url}?app_id=#{app_id}&base=#{source}"
|
200
|
+
"#{oer_url}?app_id=#{app_id}&base=#{source}" \
|
201
|
+
"&show_alternative=#{show_alternative}"
|
188
202
|
end
|
189
203
|
end
|
190
204
|
|
@@ -205,16 +219,13 @@ class Money
|
|
205
219
|
#
|
206
220
|
# @return [String] URL
|
207
221
|
def historical_url
|
208
|
-
|
209
|
-
url = SECURE_OER_HISTORICAL_URL if secure_connection
|
210
|
-
URI.join(url, "#{date}.json")
|
222
|
+
URI.join(OER_HISTORICAL_URL, "#{date}.json")
|
211
223
|
end
|
212
224
|
|
213
225
|
# Latest url
|
214
226
|
#
|
215
227
|
# @return [String] URL
|
216
228
|
def latest_url
|
217
|
-
return SECURE_OER_URL if secure_connection
|
218
229
|
OER_URL
|
219
230
|
end
|
220
231
|
|
@@ -252,7 +263,9 @@ class Money
|
|
252
263
|
# @return [String] JSON content
|
253
264
|
def read_from_url
|
254
265
|
raise NoAppId if app_id.nil? || app_id.empty?
|
255
|
-
open(source_url).read
|
266
|
+
@json_response = open(source_url).read
|
267
|
+
save_rates
|
268
|
+
@json_response
|
256
269
|
end
|
257
270
|
|
258
271
|
# Check validity of rates response only for store in cache
|
@@ -1,15 +1,12 @@
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
2
2
|
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
3
4
|
describe Money::Bank::OpenExchangeRatesBank do
|
4
5
|
subject { Money::Bank::OpenExchangeRatesBank.new }
|
5
6
|
let(:oer_url) { Money::Bank::OpenExchangeRatesBank::OER_URL }
|
6
7
|
let(:oer_historical_url) do
|
7
8
|
Money::Bank::OpenExchangeRatesBank::OER_HISTORICAL_URL
|
8
9
|
end
|
9
|
-
let(:oer_secure_url) { Money::Bank::OpenExchangeRatesBank::SECURE_OER_URL }
|
10
|
-
let(:oer_historical_secure_url) do
|
11
|
-
Money::Bank::OpenExchangeRatesBank::SECURE_OER_HISTORICAL_URL
|
12
|
-
end
|
13
10
|
|
14
11
|
let(:temp_cache_path) do
|
15
12
|
data_file('tmp.json')
|
@@ -25,6 +22,7 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
25
22
|
before do
|
26
23
|
add_to_webmock(subject)
|
27
24
|
subject.cache = temp_cache_path
|
25
|
+
subject.update_rates
|
28
26
|
subject.save_rates
|
29
27
|
end
|
30
28
|
|
@@ -130,7 +128,7 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
130
128
|
end
|
131
129
|
|
132
130
|
it 'should raise an error if no App ID is set' do
|
133
|
-
proc { subject.
|
131
|
+
proc { subject.update_rates }.must_raise Money::Bank::NoAppId
|
134
132
|
end
|
135
133
|
|
136
134
|
# TODO: As App IDs are compulsory soon, need to add more tests handle
|
@@ -149,8 +147,8 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
149
147
|
subject.oer_rates.wont_be_empty
|
150
148
|
end
|
151
149
|
|
152
|
-
it 'should
|
153
|
-
|
150
|
+
it 'should return nil when cache is nil' do
|
151
|
+
subject.save_rates.must_equal nil
|
154
152
|
end
|
155
153
|
end
|
156
154
|
|
@@ -158,68 +156,30 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
158
156
|
before do
|
159
157
|
subject.app_id = TEST_APP_ID
|
160
158
|
end
|
159
|
+
let(:source_url) do
|
160
|
+
"#{oer_url}#{subject.date}?app_id=#{TEST_APP_ID}&show_alternative=false"
|
161
|
+
end
|
161
162
|
|
162
163
|
describe 'historical' do
|
163
164
|
before do
|
164
165
|
subject.date = '2015-01-01'
|
165
166
|
end
|
166
167
|
|
167
|
-
|
168
|
-
"#{oer_historical_url}#{subject.date}.json?app_id=#{TEST_APP_ID}"
|
169
|
-
|
170
|
-
|
171
|
-
def historical_secure_url
|
172
|
-
"#{oer_historical_secure_url}#{subject.date}.json?app_id=#{TEST_APP_ID}"
|
173
|
-
end
|
174
|
-
|
175
|
-
it 'should use the non-secure http url if secure_connection is nil' do
|
176
|
-
subject.secure_connection = nil
|
177
|
-
subject.source_url.must_equal historical_url
|
178
|
-
subject.source_url.must_include 'http://'
|
179
|
-
subject.source_url.must_include "/api/historical/#{subject.date}.json"
|
168
|
+
let(:historical_url) do
|
169
|
+
"#{oer_historical_url}#{subject.date}.json?app_id=#{TEST_APP_ID}" \
|
170
|
+
'&show_alternative=false'
|
180
171
|
end
|
181
172
|
|
182
|
-
it 'should use the
|
183
|
-
subject.secure_connection = false
|
173
|
+
it 'should use the secure https url' do
|
184
174
|
subject.source_url.must_equal historical_url
|
185
|
-
subject.source_url.must_include 'http://'
|
186
|
-
subject.source_url.must_include "/api/historical/#{subject.date}.json"
|
187
|
-
end
|
188
|
-
|
189
|
-
it 'should use the secure https url if secure_connection is true' do
|
190
|
-
subject.secure_connection = true
|
191
|
-
subject.source_url.must_equal historical_secure_url
|
192
175
|
subject.source_url.must_include 'https://'
|
193
176
|
subject.source_url.must_include "/api/historical/#{subject.date}.json"
|
194
177
|
end
|
195
178
|
end
|
196
179
|
|
197
180
|
describe 'latest' do
|
198
|
-
|
199
|
-
"#{oer_url}?app_id=#{TEST_APP_ID}"
|
200
|
-
end
|
201
|
-
|
202
|
-
def source_secure_url
|
203
|
-
"#{oer_secure_url}?app_id=#{TEST_APP_ID}"
|
204
|
-
end
|
205
|
-
|
206
|
-
it 'should use the non-secure http url if secure_connection is nil' do
|
207
|
-
subject.secure_connection = nil
|
181
|
+
it 'should use the secure https url' do
|
208
182
|
subject.source_url.must_equal source_url
|
209
|
-
subject.source_url.must_include 'http://'
|
210
|
-
subject.source_url.must_include '/api/latest.json'
|
211
|
-
end
|
212
|
-
|
213
|
-
it 'should use the non-secure http url if secure_connection is false' do
|
214
|
-
subject.secure_connection = false
|
215
|
-
subject.source_url.must_equal source_url
|
216
|
-
subject.source_url.must_include 'http://'
|
217
|
-
subject.source_url.must_include '/api/latest.json'
|
218
|
-
end
|
219
|
-
|
220
|
-
it 'should use the secure https url if secure_connection is true' do
|
221
|
-
subject.secure_connection = true
|
222
|
-
subject.source_url.must_equal source_secure_url
|
223
183
|
subject.source_url.must_include 'https://'
|
224
184
|
subject.source_url.must_include '/api/latest.json'
|
225
185
|
end
|
@@ -232,31 +192,26 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
232
192
|
add_to_webmock(subject)
|
233
193
|
end
|
234
194
|
|
235
|
-
it 'should get from url' do
|
236
|
-
subject.update_rates
|
237
|
-
subject.oer_rates.wont_be_empty
|
238
|
-
end
|
239
|
-
|
240
195
|
it 'should raise an error if invalid path is given to save_rates' do
|
241
|
-
proc { subject.
|
196
|
+
proc { subject.update_rates }.must_raise Money::Bank::InvalidCache
|
242
197
|
end
|
243
198
|
end
|
244
199
|
|
245
200
|
describe 'using proc for cache' do
|
246
201
|
before do
|
247
202
|
@global_rates = nil
|
248
|
-
subject.cache = proc
|
203
|
+
subject.cache = proc do |v|
|
249
204
|
if v
|
250
205
|
@global_rates = v
|
251
206
|
else
|
252
207
|
@global_rates
|
253
208
|
end
|
254
|
-
|
209
|
+
end
|
255
210
|
add_to_webmock(subject)
|
211
|
+
subject.update_rates
|
256
212
|
end
|
257
213
|
|
258
214
|
it 'should get from url normally' do
|
259
|
-
subject.update_rates
|
260
215
|
subject.oer_rates.wont_be_empty
|
261
216
|
end
|
262
217
|
|
@@ -273,6 +228,7 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
273
228
|
before do
|
274
229
|
add_to_webmock(subject)
|
275
230
|
subject.cache = temp_cache_path
|
231
|
+
subject.update_rates
|
276
232
|
subject.save_rates
|
277
233
|
end
|
278
234
|
|
@@ -318,12 +274,14 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
318
274
|
# see test/latest.json +52
|
319
275
|
@new_usd_eur_rate = 0.79085
|
320
276
|
subject.add_rate('USD', 'EUR', @old_usd_eur_rate)
|
321
|
-
|
322
|
-
subject.
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
277
|
+
@global_rates = nil
|
278
|
+
subject.cache = proc do |v|
|
279
|
+
if v
|
280
|
+
@global_rates = v
|
281
|
+
else
|
282
|
+
@global_rates
|
283
|
+
end
|
284
|
+
end
|
327
285
|
end
|
328
286
|
|
329
287
|
describe 'when the ttl has expired' do
|
@@ -335,6 +293,14 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
335
293
|
end
|
336
294
|
end
|
337
295
|
|
296
|
+
it 'should save rates' do
|
297
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
298
|
+
Timecop.freeze(Time.now + 1001) do
|
299
|
+
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
300
|
+
@global_rates.wont_be_empty
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
338
304
|
it 'updates the next expiration time' do
|
339
305
|
Timecop.freeze(Time.now + 1001) do
|
340
306
|
exp_time = Time.now + 1000
|
@@ -347,6 +313,9 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
347
313
|
describe 'when the ttl has not expired' do
|
348
314
|
it 'not should update the rates' do
|
349
315
|
exp_time = subject.rates_expiration
|
316
|
+
dont_allow(subject).update_rates
|
317
|
+
dont_allow(subject).save_rates
|
318
|
+
dont_allow(subject).refresh_rates_expiration
|
350
319
|
subject.expire_rates
|
351
320
|
subject.rates_expiration.must_equal exp_time
|
352
321
|
end
|
@@ -383,4 +352,34 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
383
352
|
subject.source.must_equal 'USD'
|
384
353
|
end
|
385
354
|
end
|
355
|
+
|
356
|
+
describe 'show alternative' do
|
357
|
+
describe 'when no value given' do
|
358
|
+
before do
|
359
|
+
subject.show_alternative = nil
|
360
|
+
end
|
361
|
+
|
362
|
+
it 'should return the default value' do
|
363
|
+
subject.show_alternative.must_equal false
|
364
|
+
end
|
365
|
+
|
366
|
+
it 'should include show_alternative param as false' do
|
367
|
+
subject.source_url.must_include 'show_alternative=false'
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
describe 'when value is given' do
|
372
|
+
before do
|
373
|
+
subject.show_alternative = true
|
374
|
+
end
|
375
|
+
|
376
|
+
it 'should return the value' do
|
377
|
+
subject.show_alternative.must_equal true
|
378
|
+
end
|
379
|
+
|
380
|
+
it 'should include show_alternative param as true' do
|
381
|
+
subject.source_url.must_include 'show_alternative=true'
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
386
385
|
end
|
@@ -0,0 +1,393 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
describe Money::Bank::OpenExchangeRatesBank do
|
5
|
+
subject { Money::Bank::OpenExchangeRatesBank.new }
|
6
|
+
let(:oer_url) { Money::Bank::OpenExchangeRatesBank::OER_URL }
|
7
|
+
let(:oer_historical_url) do
|
8
|
+
Money::Bank::OpenExchangeRatesBank::OER_HISTORICAL_URL
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:temp_cache_path) do
|
12
|
+
data_file('tmp.json')
|
13
|
+
end
|
14
|
+
let(:oer_latest_path) do
|
15
|
+
data_file('latest.json')
|
16
|
+
end
|
17
|
+
let(:oer_historical_path) do
|
18
|
+
data_file('2015-01-01.json')
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'exchange' do
|
22
|
+
before do
|
23
|
+
add_to_webmock(subject)
|
24
|
+
subject.cache = temp_cache_path
|
25
|
+
subject.save_rates
|
26
|
+
end
|
27
|
+
|
28
|
+
after do
|
29
|
+
File.unlink(temp_cache_path)
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'without rates' do
|
33
|
+
it 'able to exchange a money to its own currency even without rates' do
|
34
|
+
money = Money.new(0, 'USD')
|
35
|
+
subject.exchange_with(money, 'USD').must_equal money
|
36
|
+
end
|
37
|
+
|
38
|
+
it "raise if it can't find an exchange rate" do
|
39
|
+
money = Money.new(0, 'USD')
|
40
|
+
proc { subject.exchange_with(money, 'SSP') }
|
41
|
+
.must_raise Money::Bank::UnknownRate
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'with rates' do
|
46
|
+
before do
|
47
|
+
subject.update_rates
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should be able to exchange money from USD to a known exchange rate' do
|
51
|
+
money = Money.new(100, 'USD')
|
52
|
+
subject.exchange_with(money, 'BBD').must_equal Money.new(200, 'BBD')
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should be able to exchange money from a known exchange rate to USD' do
|
56
|
+
money = Money.new(200, 'BBD')
|
57
|
+
subject.exchange_with(money, 'USD').must_equal Money.new(100, 'USD')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should be able to exchange money when direct rate is unknown' do
|
61
|
+
money = Money.new(100, 'BBD')
|
62
|
+
subject.exchange_with(money, 'BMD').must_equal Money.new(50, 'BMD')
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should raise if it can't find an exchange rate" do
|
66
|
+
money = Money.new(0, 'USD')
|
67
|
+
proc { subject.exchange_with(money, 'SSP') }
|
68
|
+
.must_raise Money::Bank::UnknownRate
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'update_rates' do
|
74
|
+
before do
|
75
|
+
subject.app_id = TEST_APP_ID
|
76
|
+
subject.cache = oer_latest_path
|
77
|
+
subject.update_rates
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should update itself with exchange rates from OpenExchangeRates' do
|
81
|
+
subject.oer_rates.keys.each do |currency|
|
82
|
+
next unless Money::Currency.find(currency)
|
83
|
+
subject.get_rate('USD', currency).must_be :>, 0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should not return 0 with integer rate' do
|
88
|
+
wtf = {
|
89
|
+
priority: 1,
|
90
|
+
iso_code: 'WTF',
|
91
|
+
name: 'WTF',
|
92
|
+
symbol: 'WTF',
|
93
|
+
subunit: 'Cent',
|
94
|
+
subunit_to_unit: 1000,
|
95
|
+
separator: '.',
|
96
|
+
delimiter: ','
|
97
|
+
}
|
98
|
+
Money::Currency.register(wtf)
|
99
|
+
subject.add_rate('USD', 'WTF', 2)
|
100
|
+
subject.add_rate('WTF', 'USD', 2)
|
101
|
+
subject.exchange_with(5000.to_money('WTF'), 'USD').cents.wont_equal 0
|
102
|
+
end
|
103
|
+
|
104
|
+
# in response to #4
|
105
|
+
it 'should exchange btc' do
|
106
|
+
btc = {
|
107
|
+
priority: 1,
|
108
|
+
iso_code: 'BTC',
|
109
|
+
name: 'Bitcoin',
|
110
|
+
symbol: 'BTC',
|
111
|
+
subunit: 'Cent',
|
112
|
+
subunit_to_unit: 1000,
|
113
|
+
separator: '.',
|
114
|
+
delimiter: ','
|
115
|
+
}
|
116
|
+
Money::Currency.register(btc)
|
117
|
+
rate = 13.7603
|
118
|
+
subject.add_rate('USD', 'BTC', 1 / 13.7603)
|
119
|
+
subject.add_rate('BTC', 'USD', rate)
|
120
|
+
subject.exchange_with(100.to_money('BTC'), 'USD').cents.must_equal 137_603
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe 'App ID' do
|
125
|
+
before do
|
126
|
+
subject.cache = temp_cache_path
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should raise an error if no App ID is set' do
|
130
|
+
proc { subject.save_rates }.must_raise Money::Bank::NoAppId
|
131
|
+
end
|
132
|
+
|
133
|
+
# TODO: As App IDs are compulsory soon, need to add more tests handle
|
134
|
+
# app_id-specific errors from
|
135
|
+
# https://openexchangerates.org/documentation#errors
|
136
|
+
end
|
137
|
+
|
138
|
+
describe 'no cache' do
|
139
|
+
before do
|
140
|
+
subject.cache = nil
|
141
|
+
add_to_webmock(subject)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should get from url' do
|
145
|
+
subject.update_rates
|
146
|
+
subject.oer_rates.wont_be_empty
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should raise an error if invalid path is given to save_rates' do
|
150
|
+
proc { subject.save_rates }.must_raise Money::Bank::InvalidCache
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe 'secure_connection' do
|
155
|
+
before do
|
156
|
+
subject.app_id = TEST_APP_ID
|
157
|
+
end
|
158
|
+
|
159
|
+
describe 'historical' do
|
160
|
+
before do
|
161
|
+
subject.date = '2015-01-01'
|
162
|
+
end
|
163
|
+
|
164
|
+
<<<<<<< HEAD
|
165
|
+
let(:historical_url) do
|
166
|
+
"#{oer_historical_url}#{subject.date}.json?app_id=#{TEST_APP_ID}&show_alternative=false"
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should use the secure https url' do
|
170
|
+
=======
|
171
|
+
def historical_url
|
172
|
+
"#{oer_historical_url}#{subject.date}.json?" \
|
173
|
+
"app_id=#{TEST_APP_ID}&show_alternative=false"
|
174
|
+
end
|
175
|
+
|
176
|
+
def historical_secure_url
|
177
|
+
"#{oer_historical_secure_url}#{subject.date}.json?" \
|
178
|
+
"app_id=#{TEST_APP_ID}&show_alternative=false"
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should use the non-secure http url if secure_connection is nil' do
|
182
|
+
subject.secure_connection = nil
|
183
|
+
subject.source_url.must_equal historical_url
|
184
|
+
subject.source_url.must_include 'http://'
|
185
|
+
subject.source_url.must_include "/api/historical/#{subject.date}.json"
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'should use the non-secure http url if secure_connection is false' do
|
189
|
+
subject.secure_connection = false
|
190
|
+
>>>>>>> Fix offenses
|
191
|
+
subject.source_url.must_equal historical_url
|
192
|
+
subject.source_url.must_include 'https://'
|
193
|
+
subject.source_url.must_include "/api/historical/#{subject.date}.json"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe 'latest' do
|
198
|
+
it 'should use the secure https url' do
|
199
|
+
subject.source_url.must_equal source_secure_url
|
200
|
+
subject.source_url.must_include 'https://'
|
201
|
+
subject.source_url.must_include '/api/latest.json'
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe 'no valid file for cache' do
|
207
|
+
before do
|
208
|
+
subject.cache = "space_dir#{rand(999_999_999)}/out_space_file.json"
|
209
|
+
add_to_webmock(subject)
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'should get from url' do
|
213
|
+
subject.update_rates
|
214
|
+
subject.oer_rates.wont_be_empty
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should raise an error if invalid path is given to save_rates' do
|
218
|
+
proc { subject.save_rates }.must_raise Money::Bank::InvalidCache
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe 'using proc for cache' do
|
223
|
+
before do
|
224
|
+
@global_rates = nil
|
225
|
+
subject.cache = proc do |v|
|
226
|
+
if v
|
227
|
+
@global_rates = v
|
228
|
+
else
|
229
|
+
@global_rates
|
230
|
+
end
|
231
|
+
end
|
232
|
+
add_to_webmock(subject)
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'should get from url normally' do
|
236
|
+
subject.update_rates
|
237
|
+
subject.oer_rates.wont_be_empty
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'should save from url and get from cache' do
|
241
|
+
subject.save_rates
|
242
|
+
@global_rates.wont_be_empty
|
243
|
+
dont_allow(subject).source_url
|
244
|
+
subject.update_rates
|
245
|
+
subject.oer_rates.wont_be_empty
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe 'save rates' do
|
250
|
+
before do
|
251
|
+
add_to_webmock(subject)
|
252
|
+
subject.cache = temp_cache_path
|
253
|
+
subject.save_rates
|
254
|
+
end
|
255
|
+
|
256
|
+
after do
|
257
|
+
File.unlink(temp_cache_path)
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'should allow update after save' do
|
261
|
+
begin
|
262
|
+
subject.update_rates
|
263
|
+
rescue
|
264
|
+
assert false, 'Should allow updating after saving'
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'should not break an existing file if save fails to read' do
|
269
|
+
initial_size = File.read(temp_cache_path).size
|
270
|
+
stub(subject).read_from_url { '' }
|
271
|
+
subject.save_rates
|
272
|
+
File.open(temp_cache_path).read.size.must_equal initial_size
|
273
|
+
end
|
274
|
+
|
275
|
+
it 'should not break an existing file if save returns json without rates' do
|
276
|
+
initial_size = File.read(temp_cache_path).size
|
277
|
+
stub(subject).read_from_url { '{"error": "An error"}' }
|
278
|
+
subject.save_rates
|
279
|
+
File.open(temp_cache_path).read.size.must_equal initial_size
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'should not break an existing file if save returns a invalid json' do
|
283
|
+
initial_size = File.read(temp_cache_path).size
|
284
|
+
stub(subject).read_from_url { '{invalid_json: "An error"}' }
|
285
|
+
subject.save_rates
|
286
|
+
File.open(temp_cache_path).read.size.must_equal initial_size
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe '#expire_rates' do
|
291
|
+
before do
|
292
|
+
add_to_webmock(subject)
|
293
|
+
subject.ttl_in_seconds = 1000
|
294
|
+
@old_usd_eur_rate = 0.655
|
295
|
+
# see test/latest.json +52
|
296
|
+
@new_usd_eur_rate = 0.79085
|
297
|
+
subject.add_rate('USD', 'EUR', @old_usd_eur_rate)
|
298
|
+
subject.cache = temp_cache_path
|
299
|
+
subject.save_rates
|
300
|
+
end
|
301
|
+
|
302
|
+
after do
|
303
|
+
File.unlink(temp_cache_path)
|
304
|
+
end
|
305
|
+
|
306
|
+
describe 'when the ttl has expired' do
|
307
|
+
it 'should update the rates' do
|
308
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
309
|
+
Timecop.freeze(Time.now + 1001) do
|
310
|
+
subject.get_rate('USD', 'EUR').wont_equal @old_usd_eur_rate
|
311
|
+
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
it 'updates the next expiration time' do
|
316
|
+
Timecop.freeze(Time.now + 1001) do
|
317
|
+
exp_time = Time.now + 1000
|
318
|
+
subject.expire_rates
|
319
|
+
subject.rates_expiration.must_equal exp_time
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe 'when the ttl has not expired' do
|
325
|
+
it 'not should update the rates' do
|
326
|
+
exp_time = subject.rates_expiration
|
327
|
+
subject.expire_rates
|
328
|
+
subject.rates_expiration.must_equal exp_time
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
describe 'historical' do
|
334
|
+
before do
|
335
|
+
add_to_webmock(subject)
|
336
|
+
# see test/latest.json +52
|
337
|
+
@latest_usd_eur_rate = 0.79085
|
338
|
+
# see test/2015-01-01.json +52
|
339
|
+
@old_usd_eur_rate = 0.830151
|
340
|
+
subject.update_rates
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'should be different than the latest' do
|
344
|
+
subject.get_rate('USD', 'EUR').must_equal @latest_usd_eur_rate
|
345
|
+
subject.date = '2015-01-01'
|
346
|
+
add_to_webmock(subject, oer_historical_path)
|
347
|
+
subject.update_rates
|
348
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
describe 'source currency' do
|
353
|
+
it 'should be changed when a known currency is given' do
|
354
|
+
subject.source = 'EUR'
|
355
|
+
subject.source.must_equal 'EUR'
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'should use USD when given unknown currency' do
|
359
|
+
subject.source = 'invalid'
|
360
|
+
subject.source.must_equal 'USD'
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
describe 'show alternative' do
|
365
|
+
describe 'when no value given' do
|
366
|
+
before do
|
367
|
+
subject.show_alternative = nil
|
368
|
+
end
|
369
|
+
|
370
|
+
it 'should return the default value' do
|
371
|
+
subject.show_alternative.must_equal false
|
372
|
+
end
|
373
|
+
|
374
|
+
it 'should include show_alternative param as false' do
|
375
|
+
subject.source_url.must_include 'show_alternative=false'
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
describe 'when value is given' do
|
380
|
+
before do
|
381
|
+
subject.show_alternative = true
|
382
|
+
end
|
383
|
+
|
384
|
+
it 'should return the value' do
|
385
|
+
subject.show_alternative.must_equal true
|
386
|
+
end
|
387
|
+
|
388
|
+
it 'should include show_alternative param as true' do
|
389
|
+
subject.source_url.must_include 'show_alternative=true'
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
data/test/test_helper.rb
CHANGED
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: 0.
|
4
|
+
version: 1.0.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:
|
11
|
+
date: 2018-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: money
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '6.
|
19
|
+
version: '6.8'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '6.
|
26
|
+
version: '6.8'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: monetize
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '1.
|
33
|
+
version: '1.5'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '1.
|
40
|
+
version: '1.5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '12'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '12'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: minitest
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,28 +100,28 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
103
|
+
version: '2.3'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
110
|
+
version: '2.3'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: rubocop
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 0.
|
117
|
+
version: 0.49.0
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: 0.
|
124
|
+
version: 0.49.0
|
125
125
|
description: A gem that calculates the exchange rate using published rates from open-exchange-rates.
|
126
126
|
Compatible with the money gem.
|
127
127
|
email: laurent@spkdev.net
|
@@ -142,6 +142,7 @@ files:
|
|
142
142
|
- test/integration/Gemfile.lock
|
143
143
|
- test/integration/api.rb
|
144
144
|
- test/open_exchange_rates_bank_test.rb
|
145
|
+
- test/open_exchange_rates_bank_test.rb.orig
|
145
146
|
- test/test_helper.rb
|
146
147
|
homepage: http://github.com/spk/money-open-exchange-rates
|
147
148
|
licenses:
|
@@ -155,7 +156,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
155
156
|
requirements:
|
156
157
|
- - ">="
|
157
158
|
- !ruby/object:Gem::Version
|
158
|
-
version:
|
159
|
+
version: '2.0'
|
159
160
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
161
|
requirements:
|
161
162
|
- - ">="
|
@@ -163,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
164
|
version: '0'
|
164
165
|
requirements: []
|
165
166
|
rubyforge_project:
|
166
|
-
rubygems_version: 2.
|
167
|
+
rubygems_version: 2.7.6
|
167
168
|
signing_key:
|
168
169
|
specification_version: 4
|
169
170
|
summary: A gem that calculates the exchange rate using published rates from open-exchange-rates.
|