money-currencylayer-bank 0.4.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 +7 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +108 -0
- data/lib/money/bank/currencylayer_bank.rb +192 -0
- data/test/currencylayer_bank_test.rb +295 -0
- data/test/live.json +177 -0
- data/test/test_helper.rb +15 -0
- metadata +208 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 37108e2aeabffc38f2c89a1db5161ca94d52e00e
|
4
|
+
data.tar.gz: 345f18d20d8cac0d4aa1a9b401021ec93af72301
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b10c6233f280ced125b6646341eac9b2e9859581154f8643d1ac984f44ad2018f1a2f53869d0e28d8b8be0fd6758bdc7680fda8880f683c51a35779c8a23e1d8
|
7
|
+
data.tar.gz: 334f01cd4f112a59adad4596b5f105dcaf2bf068c3077abd4f8d16936e6f9c25beed962cbff4f44a3ca654166fe4f43466e3786de95602763c432cc07659ea44
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Phlegx Systems OG
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# Money Currencylayer Bank
|
2
|
+
|
3
|
+
A gem that calculates the exchange rate using published rates from
|
4
|
+
[currencylayer.com](https://currencylayer.com/)
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
~~~ ruby
|
9
|
+
# Minimal requirements
|
10
|
+
require 'money/bank/currencylayer_bank'
|
11
|
+
mclb = Money::Bank::MoneyCurrencylayerBank.new
|
12
|
+
mclb.access_key = 'your access_key from https://currencylayer.com/product'
|
13
|
+
|
14
|
+
# Update rates
|
15
|
+
mclb.update_rates
|
16
|
+
|
17
|
+
# Store in cache
|
18
|
+
mclb.save_rates
|
19
|
+
|
20
|
+
# (optional)
|
21
|
+
# Set the base currency for all rates. By default, USD is used.
|
22
|
+
# CurrencylayerBank only allows USD as base currency for the free plan users.
|
23
|
+
mclb.source = 'EUR'
|
24
|
+
|
25
|
+
# (optional)
|
26
|
+
# Set the seconds after than the current rates are automatically expired
|
27
|
+
# by default, they never expire, in this example 1 day.
|
28
|
+
mclb.ttl_in_seconds = 86400
|
29
|
+
|
30
|
+
# (optional)
|
31
|
+
# Use https to fetch rates from CurrencylayerBank
|
32
|
+
# CurrencylayerBank only allows http as connection for the free plan users.
|
33
|
+
mclb.secure_connection = true
|
34
|
+
|
35
|
+
# Define cache
|
36
|
+
mclb.cache = 'path/to/file/cache'
|
37
|
+
|
38
|
+
# Set money default bank to currencylayer bank
|
39
|
+
Money.default_bank = mclb
|
40
|
+
~~~
|
41
|
+
|
42
|
+
### Using gem money-rails
|
43
|
+
|
44
|
+
You can also use it in Rails with the gem [money-rails](https://github.com/RubyMoney/money-rails).
|
45
|
+
|
46
|
+
~~~ ruby
|
47
|
+
require 'money/bank/currencylayer_bank'
|
48
|
+
|
49
|
+
MoneyRails.configure do |config|
|
50
|
+
mclb = Money::Bank::MoneyCurrencylayerBank.new
|
51
|
+
mclb.access_key = 'your access_key from https://currencylayer.com/product'
|
52
|
+
mclb.update_rates
|
53
|
+
|
54
|
+
config.default_bank = mclb
|
55
|
+
end
|
56
|
+
~~~
|
57
|
+
|
58
|
+
### Cache
|
59
|
+
|
60
|
+
You can also provide a Proc as a cache to provide your own caching mechanism
|
61
|
+
perhaps with Redis or just a thread safe `Hash` (global). For example:
|
62
|
+
|
63
|
+
~~~ ruby
|
64
|
+
mclb.cache = Proc.new do |v|
|
65
|
+
key = 'money:currencylayer_bank'
|
66
|
+
if v
|
67
|
+
Thread.current[key] = v
|
68
|
+
else
|
69
|
+
Thread.current[key]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
~~~
|
73
|
+
|
74
|
+
## Process
|
75
|
+
|
76
|
+
The gem fetches all rates in a cache with USD as base currency. It's possible to compute the rate between any of the currencies by calculating a pair rate using base USD rate.
|
77
|
+
|
78
|
+
## Tests
|
79
|
+
|
80
|
+
You can place your own key on a file or environment
|
81
|
+
variable named TEST_ACCESS_KEY and then run:
|
82
|
+
|
83
|
+
~~~
|
84
|
+
bundle exec rake
|
85
|
+
~~~
|
86
|
+
|
87
|
+
## Refs
|
88
|
+
|
89
|
+
* <https://github.com/RubyMoney/money>
|
90
|
+
* <https://github.com/currencybot/open-exchange-rates>
|
91
|
+
|
92
|
+
## Contributors
|
93
|
+
|
94
|
+
* See [github.com/phlegx/money-currencylayer-bank](https://github.com/phlegx/money-currencylayer-bank/graphs/contributors).
|
95
|
+
* Inspired by [github.com/spk/money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates/graphs/contributors).
|
96
|
+
|
97
|
+
## License
|
98
|
+
|
99
|
+
The MIT License
|
100
|
+
|
101
|
+
Copyright (c) 2015 Phlegx Systems OG
|
102
|
+
|
103
|
+
---
|
104
|
+
[](https://rubygems.org/gems/money-currencylayer-bank)
|
105
|
+
[](https://travis-ci.org/phlegx/money-currencylayer-bank)
|
106
|
+
[](https://codeclimate.com/github/phlegx/money-currencylayer-bank)
|
107
|
+
[](http://inch-ci.org/github/phlegx/money-currencylayer-bank)
|
108
|
+
[](http://opensource.org/licenses/MIT)
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'open-uri'
|
3
|
+
require 'money'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# Money gem class
|
7
|
+
class Money
|
8
|
+
# https://github.com/RubyMoney/money#exchange-rate-stores
|
9
|
+
module Bank
|
10
|
+
# Invalid cache, file not found or cache empty
|
11
|
+
class InvalidCache < StandardError; end
|
12
|
+
|
13
|
+
# App id not set error
|
14
|
+
class NoAccessKey < StandardError; end
|
15
|
+
|
16
|
+
# CurrencylayerBank base class
|
17
|
+
class CurrencylayerBank < Money::Bank::VariableExchange
|
18
|
+
# CurrencylayerBank url
|
19
|
+
CL_URL = 'http://apilayer.net/api/live'
|
20
|
+
# CurrencylayerBank secure url
|
21
|
+
SECURE_CL_URL = CL_URL.gsub('http:', 'https:')
|
22
|
+
# Default base currency
|
23
|
+
CL_SOURCE = 'USD'
|
24
|
+
|
25
|
+
# Use https to fetch rates from CurrencylayerBank
|
26
|
+
# CurrencylayerBank only allows http as connection for the free plan users.
|
27
|
+
attr_accessor :secure_connection
|
28
|
+
|
29
|
+
# API must have a valid access_key
|
30
|
+
attr_accessor :access_key
|
31
|
+
|
32
|
+
# Cache accessor, can be a String or a Proc
|
33
|
+
attr_accessor :cache
|
34
|
+
|
35
|
+
# Rates expiration Time
|
36
|
+
attr_reader :rates_expiration
|
37
|
+
|
38
|
+
# Parsed CurrencylayerBank result as Hash
|
39
|
+
attr_reader :cl_rates
|
40
|
+
|
41
|
+
# Seconds after than the current rates are automatically expired
|
42
|
+
attr_reader :ttl_in_seconds
|
43
|
+
|
44
|
+
# Set the base currency for all rates. By default, USD is used.
|
45
|
+
# CurrencylayerBank only allows USD as base currency for the free plan users.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# source = 'USD'
|
49
|
+
#
|
50
|
+
# @param value [String] Currency code, ISO 3166-1 alpha-3
|
51
|
+
#
|
52
|
+
# @return [String] Setted base currency
|
53
|
+
def source=(value)
|
54
|
+
@source = Money::Currency.find(value.to_s).try(:iso_code) || CL_SOURCE
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get the base currency for all rates. By default, USD is used.
|
58
|
+
# @return [String] Base currency
|
59
|
+
def source
|
60
|
+
@source ||= CL_SOURCE
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set the seconds after than the current rates are automatically expired
|
64
|
+
# by default, they never expire.
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# ttl_in_seconds = 86400 # will expire the rates in one day
|
68
|
+
#
|
69
|
+
# @param value [Integer] Time to live in seconds
|
70
|
+
#
|
71
|
+
# @return [Integer] Setted time to live in seconds
|
72
|
+
def ttl_in_seconds=(value)
|
73
|
+
@ttl_in_seconds = value
|
74
|
+
refresh_rates_expiration if ttl_in_seconds
|
75
|
+
@ttl_in_seconds
|
76
|
+
end
|
77
|
+
|
78
|
+
# Update all rates from CurrencylayerBank JSON
|
79
|
+
# @return [Array] Array of exchange rates
|
80
|
+
def update_rates
|
81
|
+
exchange_rates.each do |exchange_rate|
|
82
|
+
currency = exchange_rate.first[3..-1]
|
83
|
+
rate = exchange_rate.last
|
84
|
+
next unless Money::Currency.find(currency)
|
85
|
+
set_rate(source, currency, rate)
|
86
|
+
set_rate(currency, source, 1.0 / rate)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Save rates on cache
|
91
|
+
# Can raise InvalidCache
|
92
|
+
#
|
93
|
+
# @return [Proc,File]
|
94
|
+
def save_rates
|
95
|
+
fail InvalidCache unless cache
|
96
|
+
text = read_from_url
|
97
|
+
store_in_cache(text) if valid_rates?(text)
|
98
|
+
rescue Errno::ENOENT
|
99
|
+
raise InvalidCache
|
100
|
+
end
|
101
|
+
|
102
|
+
# Override Money `get_rate` method for caching
|
103
|
+
# @param [String] from_currency Currency ISO code. ex. 'USD'
|
104
|
+
# @param [String] to_currency Currency ISO code. ex. 'CAD'
|
105
|
+
#
|
106
|
+
# @return [Numeric] rate.
|
107
|
+
def get_rate(from_currency, to_currency, opts = {})
|
108
|
+
expire_rates
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
# Expire rates when expired
|
113
|
+
def expire_rates
|
114
|
+
return unless ttl_in_seconds
|
115
|
+
return if rates_expiration > Time.now
|
116
|
+
update_rates
|
117
|
+
refresh_rates_expiration
|
118
|
+
end
|
119
|
+
|
120
|
+
# Source url of CurrencylayerBank
|
121
|
+
# defined with access_key and secure_connection
|
122
|
+
def source_url
|
123
|
+
fail NoAccessKey if access_key.nil? || access_key.empty?
|
124
|
+
cl_url = CL_URL
|
125
|
+
cl_url = SECURE_CL_URL if secure_connection
|
126
|
+
"#{cl_url}?source=#{source}&access_key=#{access_key}"
|
127
|
+
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
# Store the provided text data by calling the proc method provided
|
132
|
+
# for the cache, or write to the cache file.
|
133
|
+
#
|
134
|
+
# @example
|
135
|
+
# store_in_cache("{\"quotes\": {\"USDAED\": 3.67304}}")
|
136
|
+
#
|
137
|
+
# @param text [String] String to cache
|
138
|
+
# @return [String,Integer]
|
139
|
+
def store_in_cache(text)
|
140
|
+
if cache.is_a?(Proc)
|
141
|
+
cache.call(text)
|
142
|
+
elsif cache.is_a?(String)
|
143
|
+
open(cache, 'w') do |f|
|
144
|
+
f.write(text)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Read from cache when exist
|
150
|
+
def read_from_cache
|
151
|
+
if cache.is_a?(Proc)
|
152
|
+
cache.call(nil)
|
153
|
+
elsif cache.is_a?(String) && File.exist?(cache)
|
154
|
+
open(cache).read
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Read from url
|
159
|
+
# @return [String] JSON content
|
160
|
+
def read_from_url
|
161
|
+
open(source_url).read
|
162
|
+
end
|
163
|
+
|
164
|
+
# Check validity of rates response only for store in cache
|
165
|
+
#
|
166
|
+
# @example
|
167
|
+
# valid_rates?("{\"quotes\": {\"USDAED\": 3.67304}}")
|
168
|
+
#
|
169
|
+
# @param [String] text is JSON content
|
170
|
+
# @return [Boolean] valid or not
|
171
|
+
def valid_rates?(text)
|
172
|
+
parsed = JSON.parse(text)
|
173
|
+
parsed && parsed.key?('quotes')
|
174
|
+
rescue JSON::ParserError
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
# Get expire rates, first from cache and then from url
|
179
|
+
# @return [Hash] key is country code (ISO 3166-1 alpha-3) value Float
|
180
|
+
def exchange_rates
|
181
|
+
doc = JSON.parse(read_from_cache) rescue doc = JSON.parse(read_from_url) rescue doc = { 'quotes' => {}}
|
182
|
+
@cl_rates = doc['quotes']
|
183
|
+
end
|
184
|
+
|
185
|
+
# Refresh expiration from now
|
186
|
+
# return [Time] new expiration time
|
187
|
+
def refresh_rates_expiration
|
188
|
+
@rates_expiration = Time.now + ttl_in_seconds
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
3
|
+
|
4
|
+
describe Money::Bank::CurrencylayerBank do
|
5
|
+
subject { Money::Bank::CurrencylayerBank.new }
|
6
|
+
let(:url) { Money::Bank::CurrencylayerBank::CL_URL }
|
7
|
+
let(:secure_url) { Money::Bank::CurrencylayerBank::SECURE_CL_URL }
|
8
|
+
let(:source) { Money::Bank::CurrencylayerBank::CL_SOURCE }
|
9
|
+
let(:temp_cache_path) do
|
10
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'temp.json'))
|
11
|
+
end
|
12
|
+
let(:cache_path) do
|
13
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'live.json'))
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'exchange' do
|
17
|
+
before do
|
18
|
+
subject.access_key = TEST_ACCESS_KEY
|
19
|
+
subject.cache = temp_cache_path
|
20
|
+
stub(subject).read_from_url { File.read cache_path }
|
21
|
+
subject.save_rates
|
22
|
+
end
|
23
|
+
|
24
|
+
after do
|
25
|
+
File.unlink(temp_cache_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'without rates' do
|
29
|
+
it 'able to exchange a money to its own currency even without rates' do
|
30
|
+
money = Money.new(0, 'USD')
|
31
|
+
subject.exchange_with(money, 'USD').must_equal money
|
32
|
+
end
|
33
|
+
|
34
|
+
it "raise if it can't find an exchange rate" do
|
35
|
+
money = Money.new(0, 'USD')
|
36
|
+
proc { subject.exchange_with(money, 'SSP') }
|
37
|
+
.must_raise Money::Bank::UnknownRate
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'with rates' do
|
42
|
+
before do
|
43
|
+
subject.update_rates
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should be able to exchange money from USD to a known exchange rate' do
|
47
|
+
money = Money.new(100, 'USD')
|
48
|
+
subject.exchange_with(money, 'BBD').must_equal Money.new(200, 'BBD')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should be able to exchange money from a known exchange rate to USD' do
|
52
|
+
money = Money.new(200, 'BBD')
|
53
|
+
subject.exchange_with(money, 'USD').must_equal Money.new(100, 'USD')
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should raise if it can't find an exchange rate" do
|
57
|
+
money = Money.new(0, 'USD')
|
58
|
+
proc { subject.exchange_with(money, 'SSP') }
|
59
|
+
.must_raise Money::Bank::UnknownRate
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'no cache' do
|
65
|
+
before do
|
66
|
+
subject.cache = nil
|
67
|
+
subject.access_key = TEST_ACCESS_KEY
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should get from url' do
|
71
|
+
stub(OpenURI::OpenRead).open(url) { File.read cache_path }
|
72
|
+
subject.update_rates
|
73
|
+
subject.cl_rates.wont_be_empty
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should raise an error if invalid path is given to save_rates' do
|
77
|
+
proc { subject.save_rates }.must_raise Money::Bank::InvalidCache
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe '#secure_connection' do
|
82
|
+
it "should use the non-secure http url if secure_connection isn't set" do
|
83
|
+
subject.secure_connection = nil
|
84
|
+
subject.access_key = TEST_ACCESS_KEY
|
85
|
+
subject.source_url.must_equal "#{url}?source=#{source}&access_key=#{TEST_ACCESS_KEY}"
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should use the non-secure http url if secure_connection is false' do
|
89
|
+
subject.secure_connection = false
|
90
|
+
subject.access_key = TEST_ACCESS_KEY
|
91
|
+
subject.source_url.must_equal "#{url}?source=#{source}&access_key=#{TEST_ACCESS_KEY}"
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should use the secure https url if secure_connection is set to true' do
|
95
|
+
subject.secure_connection = true
|
96
|
+
subject.access_key = TEST_ACCESS_KEY
|
97
|
+
subject.source_url.must_equal "#{secure_url}?source=#{source}&access_key=#{TEST_ACCESS_KEY}"
|
98
|
+
subject.source_url.must_include 'https://'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe 'no valid file for cache' do
|
103
|
+
before do
|
104
|
+
subject.cache = "space_dir#{rand(999_999_999)}/out_space_file.json"
|
105
|
+
subject.access_key = TEST_ACCESS_KEY
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should get from url' do
|
109
|
+
stub(OpenURI::OpenRead).open(url) { File.read cache_path }
|
110
|
+
subject.update_rates
|
111
|
+
subject.cl_rates.wont_be_empty
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should raise an error if invalid path is given to save_rates' do
|
115
|
+
proc { subject.save_rates }.must_raise Money::Bank::InvalidCache
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe 'using proc for cache' do
|
120
|
+
before :each do
|
121
|
+
@global_rates = nil
|
122
|
+
subject.cache = proc { |v|
|
123
|
+
if v
|
124
|
+
@global_rates = v
|
125
|
+
else
|
126
|
+
@global_rates
|
127
|
+
end
|
128
|
+
}
|
129
|
+
subject.access_key = TEST_ACCESS_KEY
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should get from url normally' do
|
133
|
+
stub(subject).source_url { cache_path }
|
134
|
+
subject.update_rates
|
135
|
+
subject.cl_rates.wont_be_empty
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should save from url and get from cache' do
|
139
|
+
stub(subject).source_url { cache_path }
|
140
|
+
subject.save_rates
|
141
|
+
@global_rates.wont_be_empty
|
142
|
+
dont_allow(subject).source_url
|
143
|
+
subject.update_rates
|
144
|
+
subject.cl_rates.wont_be_empty
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#update_rates' do
|
149
|
+
before do
|
150
|
+
subject.access_key = TEST_ACCESS_KEY
|
151
|
+
subject.cache = cache_path
|
152
|
+
subject.update_rates
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should update itself with exchange rates from CurrencylayerBank' do
|
156
|
+
subject.cl_rates.keys.each do |currency|
|
157
|
+
next unless Money::Currency.find(currency)
|
158
|
+
subject.get_rate('USD', currency).must_be :>, 0
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'should not return 0 with integer rate' do
|
163
|
+
wtf = {
|
164
|
+
priority: 1,
|
165
|
+
iso_code: 'WTF',
|
166
|
+
name: 'WTF',
|
167
|
+
symbol: 'WTF',
|
168
|
+
subunit: 'Cent',
|
169
|
+
subunit_to_unit: 1000,
|
170
|
+
separator: '.',
|
171
|
+
delimiter: ','
|
172
|
+
}
|
173
|
+
Money::Currency.register(wtf)
|
174
|
+
subject.add_rate('USD', 'WTF', 2)
|
175
|
+
subject.add_rate('WTF', 'USD', 2)
|
176
|
+
subject.exchange_with(5000.to_money('WTF'), 'USD').cents.wont_equal 0
|
177
|
+
end
|
178
|
+
|
179
|
+
# in response to #4
|
180
|
+
it 'should exchange btc' do
|
181
|
+
btc = {
|
182
|
+
priority: 1,
|
183
|
+
iso_code: 'BTC',
|
184
|
+
name: 'Bitcoin',
|
185
|
+
symbol: 'BTC',
|
186
|
+
subunit: 'Cent',
|
187
|
+
subunit_to_unit: 1000,
|
188
|
+
separator: '.',
|
189
|
+
delimiter: ','
|
190
|
+
}
|
191
|
+
Money::Currency.register(btc)
|
192
|
+
rate = 13.7603
|
193
|
+
subject.add_rate('USD', 'BTC', 1 / 13.7603)
|
194
|
+
subject.add_rate('BTC', 'USD', rate)
|
195
|
+
subject.exchange_with(100.to_money('BTC'), 'USD').cents.must_equal 137_603
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe '#access_key' do
|
200
|
+
before do
|
201
|
+
subject.cache = temp_cache_path
|
202
|
+
stub(OpenURI::OpenRead).open(url) { File.read cache_path }
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should raise an error if no access key is set' do
|
206
|
+
proc { subject.save_rates }.must_raise Money::Bank::NoAccessKey
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe '#save_rates' do
|
211
|
+
before do
|
212
|
+
subject.access_key = TEST_ACCESS_KEY
|
213
|
+
subject.cache = temp_cache_path
|
214
|
+
stub(OpenURI::OpenRead).open(url) { File.read cache_path }
|
215
|
+
subject.save_rates
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'should allow update after save' do
|
219
|
+
begin
|
220
|
+
subject.update_rates
|
221
|
+
rescue
|
222
|
+
assert false, 'Should allow updating after saving'
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'should not break an existing file if save fails to read' do
|
227
|
+
initial_size = File.read(temp_cache_path).size
|
228
|
+
stub(subject).read_from_url { '' }
|
229
|
+
subject.save_rates
|
230
|
+
File.open(temp_cache_path).read.size.must_equal initial_size
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should not break an existing file if save returns json without rates' do
|
234
|
+
initial_size = File.read(temp_cache_path).size
|
235
|
+
stub(subject).read_from_url { '{ "error": "An error" }' }
|
236
|
+
subject.save_rates
|
237
|
+
File.open(temp_cache_path).read.size.must_equal initial_size
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'should not break an existing file if save returns a invalid json' do
|
241
|
+
initial_size = File.read(temp_cache_path).size
|
242
|
+
stub(subject).read_from_url { '{ invalid_json: "An error" }' }
|
243
|
+
subject.save_rates
|
244
|
+
File.open(temp_cache_path).read.size.must_equal initial_size
|
245
|
+
end
|
246
|
+
|
247
|
+
after do
|
248
|
+
File.delete temp_cache_path
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe '#expire_rates' do
|
253
|
+
before do
|
254
|
+
subject.access_key = TEST_ACCESS_KEY
|
255
|
+
subject.ttl_in_seconds = 1000
|
256
|
+
@old_usd_eur_rate = 0.655
|
257
|
+
# see test/latest.json +54
|
258
|
+
@new_usd_eur_rate = 0.886584
|
259
|
+
subject.add_rate('USD', 'EUR', @old_usd_eur_rate)
|
260
|
+
subject.cache = temp_cache_path
|
261
|
+
stub(subject).read_from_url { File.read cache_path }
|
262
|
+
subject.save_rates
|
263
|
+
end
|
264
|
+
|
265
|
+
after do
|
266
|
+
File.delete temp_cache_path
|
267
|
+
end
|
268
|
+
|
269
|
+
describe 'when the ttl has expired' do
|
270
|
+
it 'should update the rates' do
|
271
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
272
|
+
Timecop.freeze(Time.now + 1001) do
|
273
|
+
subject.get_rate('USD', 'EUR').wont_equal @old_usd_eur_rate
|
274
|
+
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'updates the next expiration time' do
|
279
|
+
Timecop.freeze(Time.now + 1001) do
|
280
|
+
exp_time = Time.now + 1000
|
281
|
+
subject.expire_rates
|
282
|
+
subject.rates_expiration.must_equal exp_time
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
describe 'when the ttl has not expired' do
|
288
|
+
it 'not should update the rates' do
|
289
|
+
exp_time = subject.rates_expiration
|
290
|
+
subject.expire_rates
|
291
|
+
subject.rates_expiration.must_equal exp_time
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
data/test/live.json
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
{
|
2
|
+
"success":true,
|
3
|
+
"terms":"https:\/\/currencylayer.com\/terms",
|
4
|
+
"privacy":"https:\/\/currencylayer.com\/privacy",
|
5
|
+
"timestamp":1440755169,
|
6
|
+
"source":"USD",
|
7
|
+
"quotes":{
|
8
|
+
"USDAED":3.67315,
|
9
|
+
"USDAFN":65.059998,
|
10
|
+
"USDALL":124.204498,
|
11
|
+
"USDAMD":484.700012,
|
12
|
+
"USDANG":1.789762,
|
13
|
+
"USDAOA":125.800003,
|
14
|
+
"USDARS":9.289801,
|
15
|
+
"USDAUD":1.396775,
|
16
|
+
"USDAWG":1.79,
|
17
|
+
"USDAZN":1.05025,
|
18
|
+
"USDBAM":1.73215,
|
19
|
+
"USDBBD":2,
|
20
|
+
"USDBDT":78.16745,
|
21
|
+
"USDBGN":1.733895,
|
22
|
+
"USDBHD":0.37743,
|
23
|
+
"USDBIF":1558.050049,
|
24
|
+
"USDBMD":1,
|
25
|
+
"USDBND":1.403802,
|
26
|
+
"USDBOB":6.901852,
|
27
|
+
"USDBRL":3.55925,
|
28
|
+
"USDBSD":1,
|
29
|
+
"USDBTC":0.004416,
|
30
|
+
"USDBTN":66.165001,
|
31
|
+
"USDBWP":10.26445,
|
32
|
+
"USDBYR":17500,
|
33
|
+
"USDBZD":1.994978,
|
34
|
+
"USDCAD":1.321355,
|
35
|
+
"USDCDF":927.999825,
|
36
|
+
"USDCHF":0.962099,
|
37
|
+
"USDCLF":0.0246,
|
38
|
+
"USDCLP":694.494995,
|
39
|
+
"USDCNY":6.388203,
|
40
|
+
"USDCOP":3171.030029,
|
41
|
+
"USDCRC":532.380005,
|
42
|
+
"USDCUC":1,
|
43
|
+
"USDCUP":0.999558,
|
44
|
+
"USDCVE":98.070999,
|
45
|
+
"USDCZK":23.979502,
|
46
|
+
"USDDJF":177.669998,
|
47
|
+
"USDDKK":6.616496,
|
48
|
+
"USDDOP":44.889999,
|
49
|
+
"USDDZD":105.955002,
|
50
|
+
"USDEEK":13.850996,
|
51
|
+
"USDEGP":7.827797,
|
52
|
+
"USDERN":15.280041,
|
53
|
+
"USDETB":20.8335,
|
54
|
+
"USDEUR":0.886584,
|
55
|
+
"USDFJD":2.15655,
|
56
|
+
"USDFKP":0.648498,
|
57
|
+
"USDGBP":0.649287,
|
58
|
+
"USDGEL":2.344962,
|
59
|
+
"USDGGP":0.64902,
|
60
|
+
"USDGHS":4.117502,
|
61
|
+
"USDGIP":0.6485,
|
62
|
+
"USDGMD":39.630001,
|
63
|
+
"USDGNF":7277.700195,
|
64
|
+
"USDGTQ":7.672497,
|
65
|
+
"USDGYD":207.210007,
|
66
|
+
"USDHKD":7.75022,
|
67
|
+
"USDHNL":21.988899,
|
68
|
+
"USDHRK":6.7026,
|
69
|
+
"USDHTG":51.446201,
|
70
|
+
"USDHUF":279.019989,
|
71
|
+
"USDIDR":13995.5,
|
72
|
+
"USDILS":3.92785,
|
73
|
+
"USDIMP":0.64909,
|
74
|
+
"USDINR":66.160454,
|
75
|
+
"USDIQD":1149.349976,
|
76
|
+
"USDIRR":29929.999905,
|
77
|
+
"USDISK":129.264999,
|
78
|
+
"USDJEP":0.64908,
|
79
|
+
"USDJMD":117.410004,
|
80
|
+
"USDJOD":0.70905,
|
81
|
+
"USDJPY":120.879501,
|
82
|
+
"USDKES":103.844498,
|
83
|
+
"USDKGS":65.995598,
|
84
|
+
"USDKHR":4116.250384,
|
85
|
+
"USDKMF":435.697449,
|
86
|
+
"USDKPW":900.000329,
|
87
|
+
"USDKRW":1176.954956,
|
88
|
+
"USDKWD":0.30221,
|
89
|
+
"USDKYD":0.819677,
|
90
|
+
"USDKZT":241.599945,
|
91
|
+
"USDLAK":8196.599609,
|
92
|
+
"USDLBP":1506.501675,
|
93
|
+
"USDLKR":134.779999,
|
94
|
+
"USDLRD":84.660004,
|
95
|
+
"USDLSL":13.188027,
|
96
|
+
"USDLTL":3.048701,
|
97
|
+
"USDLVL":0.62055,
|
98
|
+
"USDLYD":1.37265,
|
99
|
+
"USDMAD":9.67355,
|
100
|
+
"USDMDL":19.049999,
|
101
|
+
"USDMGA":3322.699951,
|
102
|
+
"USDMKD":54.830002,
|
103
|
+
"USDMMK":1281.750421,
|
104
|
+
"USDMNT":1991.000101,
|
105
|
+
"USDMOP":7.98255,
|
106
|
+
"USDMRO":312.000071,
|
107
|
+
"USDMUR":35.150002,
|
108
|
+
"USDMVR":15.359568,
|
109
|
+
"USDMWK":555.734985,
|
110
|
+
"USDMXN":16.911249,
|
111
|
+
"USDMYR":4.187203,
|
112
|
+
"USDMZN":41.255001,
|
113
|
+
"USDNAD":13.18801,
|
114
|
+
"USDNGN":199.050003,
|
115
|
+
"USDNIO":27.4655,
|
116
|
+
"USDNOK":8.284018,
|
117
|
+
"USDNPR":105.863998,
|
118
|
+
"USDNZD":1.545237,
|
119
|
+
"USDOMR":0.38515,
|
120
|
+
"USDPAB":1,
|
121
|
+
"USDPEN":3.281097,
|
122
|
+
"USDPGK":2.8063,
|
123
|
+
"USDPHP":46.721501,
|
124
|
+
"USDPKR":103.904999,
|
125
|
+
"USDPLN":3.75175,
|
126
|
+
"USDPYG":5393.502337,
|
127
|
+
"USDQAR":3.64245,
|
128
|
+
"USDRON":3.92755,
|
129
|
+
"USDRSD":106.370003,
|
130
|
+
"USDRUB":66.537804,
|
131
|
+
"USDRWF":729.349976,
|
132
|
+
"USDSAR":3.75055,
|
133
|
+
"USDSBD":7.968634,
|
134
|
+
"USDSCR":13.03295,
|
135
|
+
"USDSDG":6.076982,
|
136
|
+
"USDSEK":8.44225,
|
137
|
+
"USDSGD":1.404397,
|
138
|
+
"USDSHP":0.6485,
|
139
|
+
"USDSLL":4638.000576,
|
140
|
+
"USDSOS":661.000153,
|
141
|
+
"USDSRD":3.296279,
|
142
|
+
"USDSTD":21789.5,
|
143
|
+
"USDSVC":8.734966,
|
144
|
+
"USDSYP":188.822006,
|
145
|
+
"USDSZL":13.187971,
|
146
|
+
"USDTHB":35.869999,
|
147
|
+
"USDTJS":6.319802,
|
148
|
+
"USDTMT":3.5,
|
149
|
+
"USDTND":1.95735,
|
150
|
+
"USDTOP":2.127081,
|
151
|
+
"USDTRY":2.91595,
|
152
|
+
"USDTTD":6.348697,
|
153
|
+
"USDTWD":32.221001,
|
154
|
+
"USDTZS":2153.300049,
|
155
|
+
"USDUAH":21.149933,
|
156
|
+
"USDUGX":3630.00047,
|
157
|
+
"USDUSD":1,
|
158
|
+
"USDUYU":28.52497,
|
159
|
+
"USDUZS":2595.520019,
|
160
|
+
"USDVEF":6.349652,
|
161
|
+
"USDVND":22462.5,
|
162
|
+
"USDVUV":111.910004,
|
163
|
+
"USDWST":2.620836,
|
164
|
+
"USDXAF":580.929871,
|
165
|
+
"USDXAG":0.069132,
|
166
|
+
"USDXAU":0.000887,
|
167
|
+
"USDXCD":2.700532,
|
168
|
+
"USDXDR":0.70995,
|
169
|
+
"USDXOF":580.929871,
|
170
|
+
"USDXPF":105.682747,
|
171
|
+
"USDYER":214.889999,
|
172
|
+
"USDZAR":13.17765,
|
173
|
+
"USDZMK":5156.098401,
|
174
|
+
"USDZMW":8.630369,
|
175
|
+
"USDZWL":322.355011
|
176
|
+
}
|
177
|
+
}
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'rr'
|
4
|
+
require 'money/bank/currencylayer_bank'
|
5
|
+
require 'monetize'
|
6
|
+
require 'timecop'
|
7
|
+
require 'pry'
|
8
|
+
|
9
|
+
TEST_ACCESS_KEY_PATH = File.join(File.dirname(__FILE__), '..', 'TEST_ACCESS_KEY')
|
10
|
+
TEST_ACCESS_KEY = ENV['TEST_ACCESS_KEY'] || File.read(TEST_ACCESS_KEY_PATH)
|
11
|
+
|
12
|
+
if TEST_ACCESS_KEY.nil? || TEST_ACCESS_KEY.empty?
|
13
|
+
fail "Please add a valid access key to file #{TEST_ACCESS_KEY_PATH} or to " \
|
14
|
+
' TEST_TEST_ACCESS_KEY environment'
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: money-currencylayer-bank
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Egon Zemmer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: money
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: monetize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.8'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.8'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-line
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.6'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rr
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.1'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: timecop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: inch
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: A gem that calculates the exchange rate using published rates from currencylayer.com.
|
168
|
+
Compatible with the money gem.
|
169
|
+
email: office@phlegx.com
|
170
|
+
executables: []
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files:
|
173
|
+
- README.md
|
174
|
+
files:
|
175
|
+
- Gemfile
|
176
|
+
- LICENSE
|
177
|
+
- README.md
|
178
|
+
- lib/money/bank/currencylayer_bank.rb
|
179
|
+
- test/currencylayer_bank_test.rb
|
180
|
+
- test/live.json
|
181
|
+
- test/test_helper.rb
|
182
|
+
homepage: http://github.com/phlegx/money-currencylayer-bank
|
183
|
+
licenses:
|
184
|
+
- MIT
|
185
|
+
metadata: {}
|
186
|
+
post_install_message:
|
187
|
+
rdoc_options: []
|
188
|
+
require_paths:
|
189
|
+
- lib
|
190
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: 1.9.3
|
195
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ">="
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: '0'
|
200
|
+
requirements: []
|
201
|
+
rubyforge_project:
|
202
|
+
rubygems_version: 2.4.8
|
203
|
+
signing_key:
|
204
|
+
specification_version: 4
|
205
|
+
summary: A gem that calculates the exchange rate using published rates from currencylayer.com.
|
206
|
+
test_files:
|
207
|
+
- test/currencylayer_bank_test.rb
|
208
|
+
has_rdoc:
|