currencylayer-historical-bank 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/lib/money/bank/currencylayer_historical_bank.rb +301 -0
- data/lib/money/version.rb +0 -0
- data/test/TEST_ACCESS_KEY +1 -0
- data/test/currencylayer_bank_test.rb +296 -0
- data/test/live.json +177 -0
- data/test/test_helper.rb +15 -0
- metadata +239 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f13d512a100631703bce783678d23f2ed8f49bde
|
4
|
+
data.tar.gz: bad37b4cad23a26ade8b03554b4578bffa081ca4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b5b419e7153236ff6d600b8f5061414616245ffd568ecb4bb4c6a5a093135938ab015600af956e856073dbd63ae292727c52ef3d664d0cb28b4a64fc5db7b748
|
7
|
+
data.tar.gz: 314d2525d4a57fd6c377af83703abbadf44245c6b81e5566f5238e8fb1bb95464891fc035c3601c4cbf419a3caac6c403649dbef9890d58c9efe6a3526d9d250
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017
|
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,185 @@
|
|
1
|
+
A gem that calculates the exchange rate using published rates from
|
2
|
+
[currencylayer.com](https://currencylayer.com/)
|
3
|
+
|
4
|
+
## Currencylayer API
|
5
|
+
|
6
|
+
~~~ json
|
7
|
+
{
|
8
|
+
"timestamp": 1441101909,
|
9
|
+
"source": "USD",
|
10
|
+
"quotes": {
|
11
|
+
/* 168 currencies */
|
12
|
+
"USDAUD": 1.413637,
|
13
|
+
"USDCAD": 1.316495,
|
14
|
+
"USDCHF": 0.96355,
|
15
|
+
"USDEUR": 0.888466,
|
16
|
+
"USDBTC": 0.004322, /* Includes Bitcoin currency! */
|
17
|
+
...
|
18
|
+
}
|
19
|
+
}
|
20
|
+
~~~
|
21
|
+
|
22
|
+
See more about Currencylayer product plans on https://currencylayer.com/product.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'currencylayer-historical-bank'
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install currencylayer-historical-bank
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
~~~ ruby
|
43
|
+
# Minimal requirements
|
44
|
+
require 'money/bank/currencylayer_historical_bank'
|
45
|
+
mclb = Money::Bank::CurrencylayerHistoricalBank.new
|
46
|
+
mclb.access_key = 'your access_key from https://currencylayer.com/product'
|
47
|
+
|
48
|
+
# Update rates (get new rates from remote if expired or access rates from cache)
|
49
|
+
mclb.update_rates
|
50
|
+
|
51
|
+
# Force update rates from remote and store in cache
|
52
|
+
# mclb.update_rates(true)
|
53
|
+
|
54
|
+
# (optional)
|
55
|
+
# Set the base currency for all rates. By default, USD is used.
|
56
|
+
# CurrencylayerHistoricalBank only allows USD as base currency for the free plan users.
|
57
|
+
mclb.source = 'EUR'
|
58
|
+
|
59
|
+
# (optional)
|
60
|
+
# Set the seconds after than the current rates are automatically expired
|
61
|
+
# by default, they never expire, in this example 1 day.
|
62
|
+
mclb.ttl_in_seconds = 86400
|
63
|
+
|
64
|
+
# (optional)
|
65
|
+
# Use https to fetch rates from CurrencylayerHistoricalBank
|
66
|
+
# CurrencylayerHistoricalBank only allows http as connection for the free plan users.
|
67
|
+
mclb.secure_connection = true
|
68
|
+
|
69
|
+
# Define cache (string or pathname)
|
70
|
+
mclb.cache = 'path/to/file/cache'
|
71
|
+
|
72
|
+
# Set money default bank to currencylayer bank
|
73
|
+
Money.default_bank = mclb
|
74
|
+
~~~
|
75
|
+
|
76
|
+
### More methods
|
77
|
+
|
78
|
+
~~~ ruby
|
79
|
+
mclb = Money::Bank::CurrencylayerHistoricalBank.new
|
80
|
+
|
81
|
+
# Returns the base currency set for all rates.
|
82
|
+
mclb.source
|
83
|
+
|
84
|
+
# Expires rates if the expiration time is reached.
|
85
|
+
mclb.expire_rates!
|
86
|
+
|
87
|
+
# Return true if the expiration time is reached.
|
88
|
+
mclb.expired?
|
89
|
+
|
90
|
+
# Get the API source url.
|
91
|
+
mclb.source_url
|
92
|
+
|
93
|
+
# Get the rates timestamp of the last API request.
|
94
|
+
mclb.rates_timestamp
|
95
|
+
~~~
|
96
|
+
|
97
|
+
### How to exchange
|
98
|
+
|
99
|
+
~~~ ruby
|
100
|
+
# Exchange 1000 cents (10.0 USD) to EUR
|
101
|
+
Money.new(1000, 'USD').exchange_to('EUR') # => #<Money fractional:89 currency:EUR>
|
102
|
+
Money.new(1000, 'USD').exchange_to('EUR').to_f # => 8.9
|
103
|
+
|
104
|
+
# Format
|
105
|
+
Money.new(1000, 'USD').exchange_to('EUR').format # => €8.90
|
106
|
+
|
107
|
+
# Get the rate
|
108
|
+
Money.default_bank.get_rate('USD', 'CAD') # => 0.9
|
109
|
+
~~~
|
110
|
+
|
111
|
+
See more on https://github.com/RubyMoney/money.
|
112
|
+
|
113
|
+
### Using gem money-rails
|
114
|
+
|
115
|
+
You can also use it in Rails with the gem [money-rails](https://github.com/RubyMoney/money-rails).
|
116
|
+
|
117
|
+
~~~ ruby
|
118
|
+
require 'money/bank/currencylayer_bank'
|
119
|
+
|
120
|
+
MoneyRails.configure do |config|
|
121
|
+
mclb = Money::Bank::CurrencylayerHistoricalBank.new
|
122
|
+
mclb.access_key = 'your access_key from https://currencylayer.com/product'
|
123
|
+
mclb.update_rates
|
124
|
+
|
125
|
+
config.default_bank = mclb
|
126
|
+
end
|
127
|
+
~~~
|
128
|
+
|
129
|
+
### Cache
|
130
|
+
|
131
|
+
You can also provide a Proc as a cache to provide your own caching mechanism
|
132
|
+
perhaps with Redis or just a thread safe `Hash` (global). For example:
|
133
|
+
|
134
|
+
~~~ ruby
|
135
|
+
mclb.cache = Proc.new do |v|
|
136
|
+
key = 'money:currencylayer_bank'
|
137
|
+
if v
|
138
|
+
Thread.current[key] = v
|
139
|
+
else
|
140
|
+
Thread.current[key]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
~~~
|
144
|
+
|
145
|
+
## Process
|
146
|
+
|
147
|
+
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.
|
148
|
+
|
149
|
+
## Tests
|
150
|
+
|
151
|
+
You can place your own key on a file or environment
|
152
|
+
variable named TEST_ACCESS_KEY and then run:
|
153
|
+
|
154
|
+
~~~
|
155
|
+
bundle exec rake
|
156
|
+
~~~
|
157
|
+
|
158
|
+
## Refs
|
159
|
+
|
160
|
+
* Gem [money](https://github.com/RubyMoney/money)
|
161
|
+
* Gem [money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates)
|
162
|
+
* Gem [money-historical-bank](https://github.com/atwam/money-historical-bank)
|
163
|
+
|
164
|
+
## Other Implementations
|
165
|
+
|
166
|
+
* Gem [currencylayer](https://github.com/askuratovsky/currencylayer)
|
167
|
+
* Gem [money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates)
|
168
|
+
* Gem [money-historical-bank](https://github.com/atwam/money-historical-bank)
|
169
|
+
* Gem [eu_central_bank](https://github.com/RubyMoney/eu_central_bank)
|
170
|
+
* Gem [nordea](https://github.com/matiaskorhonen/nordea)
|
171
|
+
* Gem [google_currency](https://github.com/RubyMoney/google_currency)
|
172
|
+
|
173
|
+
## Contributing
|
174
|
+
|
175
|
+
1. Fork it ( https://github.com/[your-username]/currencylayer-historical-bank/fork )
|
176
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
177
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
178
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
179
|
+
5. Create a new Pull Request
|
180
|
+
|
181
|
+
## License
|
182
|
+
|
183
|
+
The MIT License
|
184
|
+
|
185
|
+
Copyright (c) 2017
|
@@ -0,0 +1,301 @@
|
|
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
|
+
# rubocop:disable Metrics/ClassLength
|
18
|
+
class CurrencylayerHistoricalBank < Money::Bank::VariableExchange
|
19
|
+
# CurrencylayerBank url
|
20
|
+
CL_URL = 'http://apilayer.net/api/live'.freeze
|
21
|
+
# CurrencylayerBank historical url
|
22
|
+
CL_HISTORICAL_URL = 'http://apilayer.net/api/historical'.freeze
|
23
|
+
# CurrencylayerBank secure url
|
24
|
+
CL_SECURE_URL = CL_URL.gsub('http:', 'https:').freeze
|
25
|
+
# Default base currency
|
26
|
+
CL_SOURCE = 'USD'.freeze
|
27
|
+
|
28
|
+
# Use https to fetch rates from CurrencylayerBank
|
29
|
+
# CurrencylayerBank only allows http as connection
|
30
|
+
# for the free plan users.
|
31
|
+
attr_accessor :secure_connection
|
32
|
+
|
33
|
+
# API must have a valid access_key
|
34
|
+
attr_accessor :access_key
|
35
|
+
|
36
|
+
# Fetch historical rates on selected date
|
37
|
+
attr_accessor :historical_date
|
38
|
+
|
39
|
+
# Cache accessor, can be a String or a Proc
|
40
|
+
attr_accessor :cache
|
41
|
+
|
42
|
+
# Rates expiration Time
|
43
|
+
attr_reader :rates_expiration
|
44
|
+
|
45
|
+
# Parsed CurrencylayerBank result as Hash
|
46
|
+
attr_reader :rates
|
47
|
+
|
48
|
+
# Seconds after than the current rates are automatically expired
|
49
|
+
attr_reader :ttl_in_seconds
|
50
|
+
|
51
|
+
# Set the base currency for all rates. By default, USD is used.
|
52
|
+
# CurrencylayerBank only allows USD as base currency
|
53
|
+
# for the free plan users.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# source = 'USD'
|
57
|
+
#
|
58
|
+
# @param value [String] Currency code, ISO 3166-1 alpha-3
|
59
|
+
#
|
60
|
+
# @return [String] chosen base currency
|
61
|
+
def source=(value)
|
62
|
+
@source = Money::Currency.find(value.to_s).try(:iso_code) || CL_SOURCE
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the base currency for all rates. By default, USD is used.
|
66
|
+
# @return [String] base currency
|
67
|
+
def source
|
68
|
+
@source ||= CL_SOURCE
|
69
|
+
end
|
70
|
+
|
71
|
+
# Set the seconds after than the current rates are automatically expired
|
72
|
+
# by default, they never expire.
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# ttl_in_seconds = 86400 # will expire the rates in one day
|
76
|
+
#
|
77
|
+
# @param value [Integer] time to live in seconds
|
78
|
+
#
|
79
|
+
# @return [Integer] chosen time to live in seconds
|
80
|
+
def ttl_in_seconds=(value)
|
81
|
+
@ttl_in_seconds = value
|
82
|
+
refresh_rates_expiration!
|
83
|
+
@ttl_in_seconds
|
84
|
+
end
|
85
|
+
|
86
|
+
# Update all rates from CurrencylayerBank JSON
|
87
|
+
# @return [Array] array of exchange rates
|
88
|
+
def update_rates(straight = false)
|
89
|
+
exchange_rates(straight).each do |exchange_rate|
|
90
|
+
currency = exchange_rate.first[3..-1]
|
91
|
+
rate = exchange_rate.last
|
92
|
+
next unless Money::Currency.find(currency)
|
93
|
+
add_rate(source, currency, rate)
|
94
|
+
add_rate(currency, source, 1.0 / rate)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Override Money `get_rate` method for caching
|
99
|
+
# @param [String] from_currency Currency ISO code. ex. 'USD'
|
100
|
+
# @param [String] to_currency Currency ISO code. ex. 'CAD'
|
101
|
+
#
|
102
|
+
# @return [Numeric] rate.
|
103
|
+
def get_rate(from_currency, to_currency, opts = {}) # rubocop:disable all
|
104
|
+
@historical_date = Date.parse(opts[:date]).strftime("%Y-%m-%d") if opts[:date]
|
105
|
+
expire_rates!
|
106
|
+
rate = super
|
107
|
+
unless rate
|
108
|
+
# Tries to calculate an inverse rate
|
109
|
+
inverse_rate = super(to_currency, from_currency, opts)
|
110
|
+
if inverse_rate
|
111
|
+
rate = 1.0 / inverse_rate
|
112
|
+
add_rate(from_currency, to_currency, rate)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
unless rate
|
116
|
+
# Tries to calculate a pair rate using base currency rate
|
117
|
+
from_base_rate = super(source, from_currency, opts)
|
118
|
+
unless from_base_rate
|
119
|
+
from_inverse_rate = super(from_currency, source, opts)
|
120
|
+
from_base_rate = 1.0 / from_inverse_rate if from_inverse_rate
|
121
|
+
end
|
122
|
+
to_base_rate = super(source, to_currency, opts)
|
123
|
+
unless to_base_rate
|
124
|
+
to_inverse_rate = super(to_currency, source, opts)
|
125
|
+
to_base_rate = 1.0 / to_inverse_rate if to_inverse_rate
|
126
|
+
end
|
127
|
+
if to_base_rate && from_base_rate
|
128
|
+
rate = to_base_rate / from_base_rate
|
129
|
+
add_rate(from_currency, to_currency, rate)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
rate
|
133
|
+
end
|
134
|
+
|
135
|
+
# Fetch new rates if cached rates are expired
|
136
|
+
# @return [Boolean] true if rates are expired and updated from remote
|
137
|
+
def expire_rates!
|
138
|
+
if expired?
|
139
|
+
update_rates(true)
|
140
|
+
true
|
141
|
+
else
|
142
|
+
false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Check if rates are expired
|
147
|
+
# @return [Boolean] true if rates are expired
|
148
|
+
def expired?
|
149
|
+
return true if historical_date
|
150
|
+
rates_expiration ? rates_expiration <= Time.now : true
|
151
|
+
end
|
152
|
+
|
153
|
+
# Source url of CurrencylayerBank
|
154
|
+
# defined with access_key and secure_connection
|
155
|
+
# @return [String] the remote API url
|
156
|
+
def source_url
|
157
|
+
raise NoAccessKey if access_key.nil? || access_key.empty?
|
158
|
+
cl_url = historical_date ? CL_HISTORICAL_URL : CL_URL
|
159
|
+
cl_url = CL_SECURE_URL if secure_connection
|
160
|
+
|
161
|
+
base_url = "#{cl_url}?source=#{source}&access_key=#{access_key}"
|
162
|
+
if historical_date
|
163
|
+
date_url = "&date=#{historical_date}"
|
164
|
+
base_url = base_url + date_url
|
165
|
+
end
|
166
|
+
|
167
|
+
base_url
|
168
|
+
end
|
169
|
+
|
170
|
+
# Get the timestamp of rates
|
171
|
+
# @return [Time] time object or nil
|
172
|
+
def rates_timestamp
|
173
|
+
@rates_timestamp ||= init_rates_timestamp
|
174
|
+
end
|
175
|
+
|
176
|
+
protected
|
177
|
+
|
178
|
+
# Sets the rates timestamp from parsed JSON content
|
179
|
+
#
|
180
|
+
# @example
|
181
|
+
# set_rates_timestamp("{\"timestamp\": 1441049528,
|
182
|
+
# \"quotes\": {\"USDAED\": 3.67304}}")
|
183
|
+
#
|
184
|
+
# @param raw_rates [String] parsed JSON content, default is nil
|
185
|
+
# @return [Time] time object with rates timestamp
|
186
|
+
def init_rates_timestamp(raw_rates = nil)
|
187
|
+
raw = raw_rates || raw_rates_careful
|
188
|
+
@rates_timestamp = Time.at(raw['timestamp']) if raw.key?('timestamp')
|
189
|
+
end
|
190
|
+
|
191
|
+
# Store the provided text data by calling the proc method provided
|
192
|
+
# for the cache, or write to the cache file.
|
193
|
+
#
|
194
|
+
# @example
|
195
|
+
# store_in_cache("{\"quotes\": {\"USDAED\": 3.67304}}")
|
196
|
+
#
|
197
|
+
# @param text [String] parsed JSON content
|
198
|
+
# @return [String,Integer]
|
199
|
+
def store_in_cache(text)
|
200
|
+
if cache.is_a?(Proc)
|
201
|
+
cache.call(text)
|
202
|
+
elsif cache.is_a?(String) || cache.is_a?(Pathname)
|
203
|
+
write_to_file(text)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Writes content to file cache
|
208
|
+
# @param text [String] parsed JSON content
|
209
|
+
# @return [String,Integer]
|
210
|
+
def write_to_file(text)
|
211
|
+
open(cache, 'w') do |f|
|
212
|
+
f.write(text)
|
213
|
+
end
|
214
|
+
rescue Errno::ENOENT
|
215
|
+
raise InvalidCache
|
216
|
+
end
|
217
|
+
|
218
|
+
# Read from cache when exist
|
219
|
+
# @return [Proc,String] parsed JSON content
|
220
|
+
def read_from_cache
|
221
|
+
if cache.is_a?(Proc)
|
222
|
+
cache.call(nil)
|
223
|
+
elsif (cache.is_a?(String) || cache.is_a?(Pathname)) &&
|
224
|
+
File.exist?(cache)
|
225
|
+
open(cache).read
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Get remote content and store in cache
|
230
|
+
# @return [String] unparsed JSON content
|
231
|
+
def read_from_url
|
232
|
+
text = open_url
|
233
|
+
if valid_rates?(text)
|
234
|
+
refresh_rates_expiration!
|
235
|
+
store_in_cache(text) if cache
|
236
|
+
end
|
237
|
+
text
|
238
|
+
end
|
239
|
+
|
240
|
+
# Opens an url and reads the content
|
241
|
+
# @return [String] unparsed JSON content
|
242
|
+
def open_url
|
243
|
+
open(source_url).read
|
244
|
+
end
|
245
|
+
|
246
|
+
# Check validity of rates response only for store in cache
|
247
|
+
#
|
248
|
+
# @example
|
249
|
+
# valid_rates?("{\"quotes\": {\"USDAED\": 3.67304}}")
|
250
|
+
#
|
251
|
+
# @param [String] text is JSON content
|
252
|
+
# @return [Boolean] valid or not
|
253
|
+
def valid_rates?(text)
|
254
|
+
parsed = JSON.parse(text)
|
255
|
+
parsed && parsed.key?('quotes')
|
256
|
+
rescue JSON::ParserError
|
257
|
+
false
|
258
|
+
end
|
259
|
+
|
260
|
+
# Get exchange rates with different strategies
|
261
|
+
#
|
262
|
+
# @example
|
263
|
+
# exchange_rates(true)
|
264
|
+
# exchange_rates
|
265
|
+
#
|
266
|
+
# @param straight [Boolean] true for straight, default is careful
|
267
|
+
# @return [Hash] key is country code (ISO 3166-1 alpha-3) value Float
|
268
|
+
def exchange_rates(straight = false)
|
269
|
+
@rates = if straight
|
270
|
+
raw_rates_straight['quotes']
|
271
|
+
else
|
272
|
+
raw_rates_careful['quotes']
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Get raw exchange rates from cache and then from url
|
277
|
+
# @return [String] JSON content
|
278
|
+
def raw_rates_careful
|
279
|
+
JSON.parse(read_from_cache.to_s)
|
280
|
+
rescue JSON::ParserError
|
281
|
+
raw_rates_straight
|
282
|
+
end
|
283
|
+
|
284
|
+
# Get raw exchange rates from url
|
285
|
+
# @return [String] JSON content
|
286
|
+
def raw_rates_straight
|
287
|
+
raw_rates = JSON.parse(read_from_url)
|
288
|
+
init_rates_timestamp(raw_rates)
|
289
|
+
raw_rates
|
290
|
+
rescue JSON::ParserError
|
291
|
+
{ 'quotes' => {} }
|
292
|
+
end
|
293
|
+
|
294
|
+
# Refresh expiration from now
|
295
|
+
# return [Time] new expiration time
|
296
|
+
def refresh_rates_expiration!
|
297
|
+
@rates_expiration = Time.now + ttl_in_seconds unless ttl_in_seconds.nil?
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
your access_key from https://currencylayer.com/product
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
3
|
+
|
4
|
+
describe Money::Bank::CurrencylayerHistoricalBank do
|
5
|
+
subject { Money::Bank::CurrencylayerHistoricalBank.new }
|
6
|
+
let(:url) { Money::Bank::CurrencylayerHistoricalBank::CL_URL }
|
7
|
+
let(:secure_url) { Money::Bank::CurrencylayerHistoricalBank::CL_SECURE_URL }
|
8
|
+
let(:source) { Money::Bank::CurrencylayerHistoricalBank::CL_SOURCE }
|
9
|
+
let(:temp_cache_path) do
|
10
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'temp.json'))
|
11
|
+
end
|
12
|
+
let(:data_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).source_url { data_path }
|
21
|
+
subject.update_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 'cache rates' do
|
65
|
+
before do
|
66
|
+
subject.access_key = TEST_ACCESS_KEY
|
67
|
+
subject.cache = temp_cache_path
|
68
|
+
stub(subject).source_url { data_path }
|
69
|
+
subject.update_rates
|
70
|
+
end
|
71
|
+
|
72
|
+
after do
|
73
|
+
File.delete(temp_cache_path) if File.exist?(temp_cache_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should allow update after save' do
|
77
|
+
begin
|
78
|
+
subject.update_rates
|
79
|
+
rescue
|
80
|
+
assert false, 'Should allow updating after saving'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should not break an existing file if save fails to read' do
|
85
|
+
initial_size = File.read(temp_cache_path).size
|
86
|
+
stub(subject).open_url { '' }
|
87
|
+
subject.update_rates
|
88
|
+
File.read(temp_cache_path).size.must_equal initial_size
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should not break an existing file if save returns json without rates' do
|
92
|
+
initial_size = File.read(temp_cache_path).size
|
93
|
+
stub(subject).open_url { '{ "error": "An error" }' }
|
94
|
+
subject.update_rates
|
95
|
+
File.read(temp_cache_path).size.must_equal initial_size
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should not break an existing file if save returns a invalid json' do
|
99
|
+
initial_size = File.read(temp_cache_path).size
|
100
|
+
stub(subject).open_url { '{ invalid_json: "An error" }' }
|
101
|
+
subject.update_rates
|
102
|
+
File.read(temp_cache_path).size.must_equal initial_size
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'no cache' do
|
107
|
+
before do
|
108
|
+
subject.cache = nil
|
109
|
+
subject.access_key = TEST_ACCESS_KEY
|
110
|
+
stub(subject).source_url { data_path }
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should get from url' do
|
114
|
+
subject.update_rates
|
115
|
+
subject.rates.wont_be_empty
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe 'no valid file for cache' do
|
120
|
+
before do
|
121
|
+
subject.cache = "space_dir#{rand(999_999_999)}/out_space_file.json"
|
122
|
+
subject.access_key = TEST_ACCESS_KEY
|
123
|
+
stub(subject).source_url { data_path }
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should raise an error if invalid path is given' do
|
127
|
+
proc { subject.update_rates }.must_raise Money::Bank::InvalidCache
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe 'using proc for cache' do
|
132
|
+
before :each do
|
133
|
+
@global_rates = nil
|
134
|
+
subject.cache = proc { |v|
|
135
|
+
if v
|
136
|
+
@global_rates = v
|
137
|
+
else
|
138
|
+
@global_rates
|
139
|
+
end
|
140
|
+
}
|
141
|
+
subject.access_key = TEST_ACCESS_KEY
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should get from url normally' do
|
145
|
+
stub(subject).source_url { data_path }
|
146
|
+
subject.update_rates
|
147
|
+
subject.rates.wont_be_empty
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should save from url and get from cache' do
|
151
|
+
stub(subject).source_url { data_path }
|
152
|
+
subject.update_rates
|
153
|
+
@global_rates.wont_be_empty
|
154
|
+
dont_allow(subject).source_url
|
155
|
+
subject.update_rates
|
156
|
+
subject.rates.wont_be_empty
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#secure_connection' do
|
161
|
+
it "should use the non-secure http url if secure_connection isn't set" do
|
162
|
+
subject.secure_connection = nil
|
163
|
+
subject.access_key = TEST_ACCESS_KEY
|
164
|
+
subject.source_url.must_equal "#{url}?source=#{source}&"\
|
165
|
+
"access_key=#{TEST_ACCESS_KEY}"
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should use the non-secure http url if secure_connection is false' do
|
169
|
+
subject.secure_connection = false
|
170
|
+
subject.access_key = TEST_ACCESS_KEY
|
171
|
+
subject.source_url.must_equal "#{url}?source=#{source}&"\
|
172
|
+
"access_key=#{TEST_ACCESS_KEY}"
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'should use the secure https url if secure_connection is set to true' do
|
176
|
+
subject.secure_connection = true
|
177
|
+
subject.access_key = TEST_ACCESS_KEY
|
178
|
+
subject.source_url.must_equal "#{secure_url}?source=#{source}&"\
|
179
|
+
"access_key=#{TEST_ACCESS_KEY}"
|
180
|
+
subject.source_url.must_include 'https://'
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe '#update_rates' do
|
185
|
+
before do
|
186
|
+
subject.access_key = TEST_ACCESS_KEY
|
187
|
+
subject.cache = data_path
|
188
|
+
stub(subject).source_url { data_path }
|
189
|
+
subject.update_rates
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'should update itself with exchange rates from CurrencylayerBank' do
|
193
|
+
subject.rates.keys.each do |currency|
|
194
|
+
next unless Money::Currency.find(currency)
|
195
|
+
subject.get_rate('USD', currency).must_be :>, 0
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'should not return 0 with integer rate' do
|
200
|
+
wtf = {
|
201
|
+
priority: 1,
|
202
|
+
iso_code: 'WTF',
|
203
|
+
name: 'WTF',
|
204
|
+
symbol: 'WTF',
|
205
|
+
subunit: 'Cent',
|
206
|
+
subunit_to_unit: 1000,
|
207
|
+
separator: '.',
|
208
|
+
delimiter: ','
|
209
|
+
}
|
210
|
+
Money::Currency.register(wtf)
|
211
|
+
subject.add_rate('USD', 'WTF', 2)
|
212
|
+
subject.add_rate('WTF', 'USD', 2)
|
213
|
+
subject.exchange_with(5000.to_money('WTF'), 'USD').cents.wont_equal 0
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe '#access_key' do
|
218
|
+
before do
|
219
|
+
subject.cache = temp_cache_path
|
220
|
+
stub(OpenURI::OpenRead).open(url) { File.read data_path }
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'should raise an error if no access key is set' do
|
224
|
+
proc { subject.update_rates }.must_raise Money::Bank::NoAccessKey
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe '#expire_rates!' do
|
229
|
+
before do
|
230
|
+
subject.access_key = TEST_ACCESS_KEY
|
231
|
+
subject.ttl_in_seconds = 1000
|
232
|
+
@old_usd_eur_rate = 0.655
|
233
|
+
# see test/live.json +54
|
234
|
+
@new_usd_eur_rate = 0.886584
|
235
|
+
subject.add_rate('USD', 'EUR', @old_usd_eur_rate)
|
236
|
+
subject.cache = temp_cache_path
|
237
|
+
stub(subject).source_url { data_path }
|
238
|
+
end
|
239
|
+
|
240
|
+
after do
|
241
|
+
File.delete(temp_cache_path) if File.exist?(temp_cache_path)
|
242
|
+
end
|
243
|
+
|
244
|
+
describe 'when the ttl has expired' do
|
245
|
+
it 'should update the rates' do
|
246
|
+
Timecop.freeze(subject.rates_timestamp + 1000) do
|
247
|
+
subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
|
248
|
+
end
|
249
|
+
Timecop.freeze(subject.rates_timestamp + 1001) do
|
250
|
+
subject.get_rate('USD', 'EUR').wont_equal @old_usd_eur_rate
|
251
|
+
subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'updates the next expiration time' do
|
256
|
+
Timecop.freeze(subject.rates_timestamp + 1001) do
|
257
|
+
exp_time = subject.rates_timestamp + 1000
|
258
|
+
subject.expire_rates!
|
259
|
+
subject.rates_expiration.must_equal exp_time
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
describe 'when the ttl has not expired' do
|
265
|
+
it 'not should update the rates' do
|
266
|
+
subject.update_rates
|
267
|
+
exp_time = subject.rates_expiration
|
268
|
+
subject.expire_rates!
|
269
|
+
subject.rates_expiration.must_equal exp_time
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
describe '#rates_timestamp' do
|
275
|
+
before do
|
276
|
+
subject.access_key = TEST_ACCESS_KEY
|
277
|
+
subject.cache = temp_cache_path
|
278
|
+
stub(subject).source_url { data_path }
|
279
|
+
end
|
280
|
+
|
281
|
+
after do
|
282
|
+
File.delete(temp_cache_path) if File.exist?(temp_cache_path)
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'should return 1970-01-01 datetime if no rates' do
|
286
|
+
stub(subject).open_url { '' }
|
287
|
+
subject.update_rates
|
288
|
+
subject.rates_timestamp.must_equal Time.at(0)
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'should return a Time object' do
|
292
|
+
subject.update_rates
|
293
|
+
subject.rates_timestamp.class.must_equal Time
|
294
|
+
end
|
295
|
+
end
|
296
|
+
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_historical_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
|
+
raise "Please add a valid access key to file #{TEST_ACCESS_KEY_PATH} or to " \
|
14
|
+
' TEST_ACCESS_KEY environment'
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: currencylayer-historical-bank
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ildus Sadykov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-13 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.1
|
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.1
|
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.7'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.8'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.8'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-line
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.6'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rr
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 12.0.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 12.0.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: timecop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.8.1
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.8.1
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.47.1
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.47.1
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: inch
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.7.1
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.7.1
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rspec
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 3.5.0
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 3.5.0
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: webmock
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: bundler
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '1.7'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '1.7'
|
195
|
+
description: A gem that calculates the historical exchange rate using published rates
|
196
|
+
from currencylayer.com. Compatible with the money gem.
|
197
|
+
email: ildus@meyvndigital.co.uk
|
198
|
+
executables: []
|
199
|
+
extensions: []
|
200
|
+
extra_rdoc_files:
|
201
|
+
- README.md
|
202
|
+
files:
|
203
|
+
- Gemfile
|
204
|
+
- LICENSE
|
205
|
+
- README.md
|
206
|
+
- lib/money/bank/currencylayer_historical_bank.rb
|
207
|
+
- lib/money/version.rb
|
208
|
+
- test/TEST_ACCESS_KEY
|
209
|
+
- test/currencylayer_bank_test.rb
|
210
|
+
- test/live.json
|
211
|
+
- test/test_helper.rb
|
212
|
+
homepage: http://github.com/IldusSadykov/currencylayer-historical-bank
|
213
|
+
licenses:
|
214
|
+
- MIT
|
215
|
+
metadata: {}
|
216
|
+
post_install_message:
|
217
|
+
rdoc_options: []
|
218
|
+
require_paths:
|
219
|
+
- lib
|
220
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
221
|
+
requirements:
|
222
|
+
- - ">="
|
223
|
+
- !ruby/object:Gem::Version
|
224
|
+
version: 2.1.2
|
225
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
requirements: []
|
231
|
+
rubyforge_project:
|
232
|
+
rubygems_version: 2.5.1
|
233
|
+
signing_key:
|
234
|
+
specification_version: 4
|
235
|
+
summary: A gem that calculates the historical exchange rate using published rates
|
236
|
+
from currencylayer.com.
|
237
|
+
test_files:
|
238
|
+
- test/currencylayer_bank_test.rb
|
239
|
+
has_rdoc:
|