money-open-exchange-rates 0.7.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build](https://img.shields.io/travis-ci/spk/money-open-exchange-rates.svg)](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.
|