currencylayer-historical-bank 0.0.2

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 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: