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 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
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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
+ }
@@ -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: