money-openexchangerates-bank 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d1de65480f09a588a4682745d9134a7a1200442
4
+ data.tar.gz: ceedd43fa6d2ab3c856d6adeffdabe43b4342b83
5
+ SHA512:
6
+ metadata.gz: db4fedb4010411e85ca593135944e679c35e879401694c107ecc10848b4bbf048035ffec3a6d508162ff334711ce86ca8552425c27cbd6ed19a27d147c5afd97
7
+ data.tar.gz: 1ae9b5d341306846427c7094b6c6507c56130190c6db7122cb0c2ce4996c7c391db2819b0be3436b072b1843dd6bf57ff61d59dda7a4e97ec93c83497046dd17
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :test, :development do
8
+ platforms :ruby_19, :jruby_19 do
9
+ gem 'tins', '~> 1.6'
10
+ end
11
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Phlegx Systems OG
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,225 @@
1
+ # Money Openexchangerates Bank
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/money-openexchangerates-bank.svg)](https://rubygems.org/gems/money-openexchangerates-bank)
4
+ [![Gem](https://img.shields.io/gem/dt/money-openexchangerates-bank.svg?maxAge=2592000)](https://rubygems.org/gems/money-openexchangerates-bank)
5
+ [![Build Status](https://secure.travis-ci.org/phlegx/money-openexchangerates-bank.svg?branch=master)](https://travis-ci.org/phlegx/money-openexchangerates-bank)
6
+ [![Code Climate](http://img.shields.io/codeclimate/github/phlegx/money-openexchangerates-bank.svg)](https://codeclimate.com/github/phlegx/money-openexchangerates-bank)
7
+ [![Inline Docs](http://inch-ci.org/github/phlegx/money-openexchangerates-bank.svg?branch=master)](http://inch-ci.org/github/phlegx/money-openexchangerates-bank)
8
+ [![Dependency Status](https://gemnasium.com/phlegx/money-openexchangerates-bank.svg)](https://gemnasium.com/phlegx/money-openexchangerates-bank)
9
+ [![License](https://img.shields.io/github/license/phlegx/money-openexchangerates-bank.svg)](http://opensource.org/licenses/MIT)
10
+
11
+ A gem that calculates the exchange rate using published rates from
12
+ [openexchangerates.org](https://openexchangerates.org/)
13
+
14
+ ## Openexchangerates API
15
+
16
+ ~~~ json
17
+ {
18
+ "timestamp": 1441101909,
19
+ "base": "USD",
20
+ "rates": {
21
+ /* 190 currencies */
22
+ "AUD": 1.413637,
23
+ "CAD": 1.316495,
24
+ "CHF": 0.96355,
25
+ "EUR": 0.888466,
26
+ "BTC": 0.004322, /* Includes cryptocurrencies! */
27
+ ...
28
+ }
29
+ }
30
+ ~~~
31
+
32
+ See more about Openexchangerates product plans on https://openexchangerates.org/signup.
33
+
34
+ ## Features
35
+
36
+ * supports 190 currencies
37
+ * includes cryptocurrencies
38
+ * precision of rates up to 6 digits after point
39
+ * uses fast and reliable json api
40
+ * average response time < 20ms
41
+ * supports caching currency rates
42
+ * calculates every pair rate calculating inverse rate or using base currency rate
43
+ * supports multi threading
44
+
45
+ ## Installation
46
+
47
+ Add this line to your application's Gemfile:
48
+
49
+ ```ruby
50
+ gem 'money-openexchangerates-bank'
51
+ ```
52
+
53
+ And then execute:
54
+
55
+ $ bundle
56
+
57
+ Or install it yourself as:
58
+
59
+ $ gem install money-openexchangerates-bank
60
+
61
+ ## Usage
62
+
63
+ ~~~ ruby
64
+ # Minimal requirements
65
+ require 'money/bank/openexchangerates_bank'
66
+ moxb = Money::Bank::OpenexchangeratesBank.new
67
+ moxb.access_key = 'your access_key from https://openexchangerates.com/signup'
68
+
69
+ # Update rates (get new rates from remote if expired or access rates from cache)
70
+ moxb.update_rates
71
+
72
+ # Force update rates from remote and store in cache
73
+ # moxb.update_rates(true)
74
+
75
+ # (optional)
76
+ # Set the base currency for all rates. By default, USD is used.
77
+ # OpenexchangeratesBank only allows USD as base currency for the free plan users.
78
+ moxb.source = 'EUR'
79
+
80
+ # (optional)
81
+ # Set the seconds after than the current rates are automatically expired
82
+ # by default, they never expire, in this example 1 day.
83
+ moxb.ttl_in_seconds = 86400
84
+
85
+ # (optional)
86
+ # Use https to fetch rates from OpenexchangeratesBank
87
+ # OpenexchangeratesBank only allows http as connection for the free plan users.
88
+ moxb.secure_connection = true
89
+
90
+ # Define cache (string or pathname)
91
+ moxb.cache = 'path/to/file/cache'
92
+
93
+ # Set money default bank to Openexchangerates bank
94
+ Money.default_bank = moxb
95
+ ~~~
96
+
97
+ ### More methods
98
+
99
+ ~~~ ruby
100
+ moxb = Money::Bank::OpenexchangeratesBank.new
101
+
102
+ # Returns the base currency set for all rates.
103
+ moxb.source
104
+
105
+ # Include also alternative rates.
106
+ moxb.alternatives = :all
107
+
108
+ # Get only alternative rates.
109
+ moxb.alternatives = :only
110
+
111
+ # Default is nil to get only regular rates.
112
+ # moxb.alternatives = nil
113
+
114
+ # Expires rates if the expiration time is reached.
115
+ moxb.expire_rates!
116
+
117
+ # Returns true if the expiration time is reached.
118
+ moxb.expired?
119
+
120
+ # Get the API source url.
121
+ moxb.source_url
122
+
123
+ # Get the rates timestamp of the last API request.
124
+ moxb.rates_timestamp
125
+
126
+ # Get the rates timestamp of loaded rates in memory.
127
+ moxb.rates_mem_timestamp
128
+ ~~~
129
+
130
+ ### How to exchange
131
+
132
+ ~~~ ruby
133
+ # Exchange 1000 cents (10.0 USD) to EUR
134
+ Money.new(1000, 'USD').exchange_to('EUR') # => #<Money fractional:89 currency:EUR>
135
+ Money.new(1000, 'USD').exchange_to('EUR').to_f # => 8.9
136
+
137
+ # Format
138
+ Money.new(1000, 'USD').exchange_to('EUR').format # => €8.90
139
+
140
+ # Get the rate
141
+ Money.default_bank.get_rate('USD', 'CAD') # => 0.9
142
+ ~~~
143
+
144
+ See more on https://github.com/RubyMoney/money.
145
+
146
+ ### Using gem money-rails
147
+
148
+ You can also use it in Rails with the gem [money-rails](https://github.com/RubyMoney/money-rails).
149
+
150
+ ~~~ ruby
151
+ require 'money/bank/openexchangerates_bank'
152
+
153
+ MoneyRails.configure do |config|
154
+ moxb = Money::Bank::OpenexchangeratesBank.new
155
+ moxb.access_key = 'your access_key from https://openexchangerates.com/signup'
156
+ moxb.update_rates
157
+
158
+ config.default_bank = moxb
159
+ end
160
+ ~~~
161
+
162
+ ### Cache
163
+
164
+ You can also provide a Proc as a cache to provide your own caching mechanism
165
+ perhaps with Redis or just a thread safe `Hash` (global). For example:
166
+
167
+ ~~~ ruby
168
+ moxb.cache = Proc.new do |v|
169
+ key = 'money:openexchangerates_bank'
170
+ if v
171
+ Thread.current[key] = v
172
+ else
173
+ Thread.current[key]
174
+ end
175
+ end
176
+ ~~~
177
+
178
+ ## Process
179
+
180
+ 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.
181
+
182
+ ## Tests
183
+
184
+ You can place your own key on a file or environment
185
+ variable named TEST_ACCESS_KEY and then run:
186
+
187
+ ~~~
188
+ bundle exec rake
189
+ ~~~
190
+
191
+ ## Refs
192
+
193
+ * Gem [money](https://github.com/RubyMoney/money)
194
+ * Gem [money-currencylayer-bank](https://github.com/phlegx/money-currencylayer-bank)
195
+ * Gem [money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates)
196
+ * Gem [money-historical-bank](https://github.com/atwam/money-historical-bank)
197
+
198
+ ## Other Implementations
199
+
200
+ * Gem [money-currencylayer-bank](https://github.com/phlegx/money-currencylayer-bank)
201
+ * Gem [money-open-exchange-rates](https://github.com/spk/money-open-exchange-rates)
202
+ * Gem [money-historical-bank](https://github.com/atwam/money-historical-bank)
203
+ * Gem [currencylayer](https://github.com/askuratovsky/currencylayer)
204
+ * Gem [eu_central_bank](https://github.com/RubyMoney/eu_central_bank)
205
+ * Gem [nordea](https://github.com/matiaskorhonen/nordea)
206
+ * Gem [google_currency](https://github.com/RubyMoney/google_currency)
207
+
208
+ ## Contributors
209
+
210
+ * See [github.com/phlegx/money-openexchangerates-bank](https://github.com/phlegx/money-openexchangerates-bank/graphs/contributors).
211
+ * Inspired by [github.com/phlegx/money-currencylayer-bank](https://github.com/phlegx/money-currencylayer-bank/graphs/contributors).
212
+
213
+ ## Contributing
214
+
215
+ 1. Fork it ( https://github.com/[your-username]/money-openexchangerates-bank/fork )
216
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
217
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
218
+ 4. Push to the branch (`git push origin my-new-feature`)
219
+ 5. Create a new Pull Request
220
+
221
+ ## License
222
+
223
+ The MIT License
224
+
225
+ Copyright (c) 2017 Phlegx Systems OG
@@ -0,0 +1,353 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'open-uri'
5
+ require 'money'
6
+ require 'json'
7
+
8
+ # Money gem class
9
+ class Money
10
+ # Build in memory rates store
11
+ module RatesStore
12
+ # Memory class
13
+ class Memory
14
+ # Add method to reset the build in memory store
15
+ # @param [Hash] rt Optional initial exchange rate data.
16
+ # @return [Object] store.
17
+ def reset!(rt = {})
18
+ transaction { @index = rt }
19
+ end
20
+ end
21
+ end
22
+
23
+ # https://github.com/RubyMoney/money#exchange-rate-stores
24
+ module Bank
25
+ # Invalid cache, file not found or cache empty
26
+ class InvalidCache < StandardError; end
27
+
28
+ # App id not set error
29
+ class NoAccessKey < StandardError; end
30
+
31
+ # CurrencylayerBank base class
32
+ class OpenexchangeratesBank < Money::Bank::VariableExchange
33
+ # CurrencylayerBank url
34
+ CL_URL = 'http://openexchangerates.org/api/latest.json'.freeze
35
+ # CurrencylayerBank secure url
36
+ CL_SECURE_URL = CL_URL.sub('http:', 'https:')
37
+ # Default base currency
38
+ CL_SOURCE = 'USD'.freeze
39
+
40
+ # Use https to fetch rates from CurrencylayerBank
41
+ # CurrencylayerBank only allows http as connection
42
+ # for the free plan users.
43
+ #
44
+ # @param value [Boolean] true for secure connection
45
+ # @return [Boolean] chosen secure connection
46
+ attr_accessor :secure_connection
47
+
48
+ # API must have a valid access_key
49
+ #
50
+ # @param value [String] API access key
51
+ # @return [String] chosen API access key
52
+ attr_accessor :access_key
53
+
54
+ # Cache accessor, can be a String or a Proc
55
+ #
56
+ # @param value [String,Pathname,Proc] cache system
57
+ # @return [String,Pathname,Proc] chosen cache system
58
+ attr_accessor :cache
59
+
60
+ # Parsed CurrencylayerBank result as Hash
61
+ attr_reader :rates
62
+
63
+ # Get the timestamp of rates in memory
64
+ # @return [Time] time object or nil
65
+ attr_reader :rates_mem_timestamp
66
+
67
+ # Set the seconds after than the current rates are automatically expired
68
+ # by default, they never expire.
69
+ #
70
+ # @example
71
+ # ttl_in_seconds = 86400 # will expire the rates in one day
72
+ #
73
+ # @param value [Integer] time to live in seconds
74
+ # @return [Integer] chosen time to live in seconds
75
+ attr_writer :ttl_in_seconds
76
+
77
+ # Set the base currency for all rates. By default, USD is used.
78
+ # CurrencylayerBank only allows USD as base currency
79
+ # for the free plan users.
80
+ #
81
+ # @example
82
+ # source = 'USD'
83
+ #
84
+ # @param value [String] Currency code, ISO 3166-1 alpha-3
85
+ # @return [String] chosen base currency
86
+ def source=(value)
87
+ @source = Money::Currency.find(value.to_s).try(:iso_code) || CL_SOURCE
88
+ end
89
+
90
+ # Get the base currency for all rates. By default, USD is used.
91
+ # @return [String] base currency
92
+ def source
93
+ @source ||= CL_SOURCE
94
+ end
95
+
96
+ # Set alternative system, to request alternative rates
97
+ #
98
+ # @example
99
+ # alternatives = :all
100
+ # alternatives = :only
101
+ # alternatives = nil
102
+ #
103
+ # @param value [String,Symbol] alternatives
104
+ # @return [String,nil] chosen alternative system
105
+ def alternatives=(system)
106
+ @alternatives = case system.to_sym
107
+ when :all
108
+ 'show_alternative'
109
+ when :only
110
+ 'only_alternative'
111
+ end
112
+ end
113
+
114
+ # Get alternative system. Default is nil
115
+ # @return [String,nil] chosen alternative system
116
+ def alternatives
117
+ @alternatives || nil
118
+ end
119
+
120
+ # Get the seconds after than the current rates are automatically expired
121
+ # by default, they never expire.
122
+ # @return [Integer] chosen time to live in seconds
123
+ def ttl_in_seconds
124
+ @ttl_in_seconds ||= 0
125
+ end
126
+
127
+ # Update all rates from CurrencylayerBank JSON
128
+ # @return [Array] array of exchange rates
129
+ def update_rates(straight = false)
130
+ store.reset!
131
+ exchange_rates(straight).each do |exchange_rate|
132
+ currency = exchange_rate.first
133
+ rate = exchange_rate.last
134
+ next unless Money::Currency.find(currency)
135
+ add_rate(source, currency, rate)
136
+ add_rate(currency, source, 1.0 / rate)
137
+ end
138
+ @rates_mem_timestamp = rates_timestamp
139
+ end
140
+
141
+ # Override Money `add_rate` method for caching
142
+ # @param [String] from_currency Currency ISO code. ex. 'USD'
143
+ # @param [String] to_currency Currency ISO code. ex. 'CAD'
144
+ # @param [Numeric] rate Rate to use when exchanging currencies.
145
+ # @return [Numeric] rate.
146
+ def add_rate(from_currency, to_currency, rate)
147
+ super
148
+ end
149
+
150
+ # Alias super method
151
+ alias super_get_rate get_rate
152
+
153
+ # Override Money `get_rate` method for caching
154
+ # @param [String] from_currency Currency ISO code. ex. 'USD'
155
+ # @param [String] to_currency Currency ISO code. ex. 'CAD'
156
+ # @param [Hash] opts Options hash to set special parameters.
157
+ # @return [Numeric] rate.
158
+ def get_rate(from_currency, to_currency, opts = {})
159
+ expire_rates!
160
+ rate = get_rate_or_calc_inverse(from_currency, to_currency, opts)
161
+ rate || calc_pair_rate_using_base(from_currency, to_currency, opts)
162
+ end
163
+
164
+ # Fetch new rates if cached rates are expired or stale
165
+ # @return [Boolean] true if rates are expired and updated from remote
166
+ def expire_rates!
167
+ if expired?
168
+ update_rates(true)
169
+ true
170
+ elsif stale?
171
+ update_rates
172
+ true
173
+ else
174
+ false
175
+ end
176
+ end
177
+
178
+ # Check if rates are expired
179
+ # @return [Boolean] true if rates are expired
180
+ def expired?
181
+ Time.now > rates_expiration
182
+ end
183
+
184
+ # Check if rates are stale
185
+ # Stale is true if rates are updated straight by another thread.
186
+ # The actual thread has always old rates in memory store.
187
+ # @return [Boolean] true if rates are stale
188
+ def stale?
189
+ rates_timestamp != rates_mem_timestamp
190
+ end
191
+
192
+ # Source url of CurrencylayerBank
193
+ # defined with access_key and secure_connection
194
+ # @return [String] the remote API url
195
+ def source_url
196
+ raise NoAccessKey if access_key.nil? || access_key.empty?
197
+ cl_url = CL_URL
198
+ cl_url = CL_SECURE_URL if secure_connection
199
+ params = "#{cl_url}?base=#{source}&app_id=#{access_key}&prettyprint=0"
200
+ params += "&#{alternatives}=1" unless alternatives.nil?
201
+ params
202
+ end
203
+
204
+ # Get rates expiration time based on ttl
205
+ # @return [Time] rates expiration time
206
+ def rates_expiration
207
+ rates_timestamp + ttl_in_seconds
208
+ end
209
+
210
+ # Get the timestamp of rates
211
+ # @return [Time] time object or nil
212
+ def rates_timestamp
213
+ raw = raw_rates_careful
214
+ raw.key?('timestamp') ? Time.at(raw['timestamp']) : Time.at(0)
215
+ end
216
+
217
+ protected
218
+
219
+ # Store the provided text data by calling the proc method provided
220
+ # for the cache, or write to the cache file.
221
+ #
222
+ # @example
223
+ # store_in_cache("{\"rates\": {\"AED\": 3.67304}}")
224
+ #
225
+ # @param text [String] parsed JSON content
226
+ # @return [String,Integer]
227
+ def store_in_cache(text)
228
+ if cache.is_a?(Proc)
229
+ cache.call(text)
230
+ elsif cache.is_a?(String) || cache.is_a?(Pathname)
231
+ write_to_file(text)
232
+ end
233
+ end
234
+
235
+ # Writes content to file cache
236
+ # @param text [String] parsed JSON content
237
+ # @return [String,Integer]
238
+ def write_to_file(text)
239
+ open(cache, 'w') do |f|
240
+ f.write(text)
241
+ end
242
+ rescue Errno::ENOENT
243
+ raise InvalidCache
244
+ end
245
+
246
+ # Read from cache when exist
247
+ # @return [Proc,String] parsed JSON content
248
+ def read_from_cache
249
+ if cache.is_a?(Proc)
250
+ cache.call(nil)
251
+ elsif (cache.is_a?(String) || cache.is_a?(Pathname)) &&
252
+ File.exist?(cache)
253
+ open(cache).read
254
+ end
255
+ end
256
+
257
+ # Get remote content and store in cache
258
+ # @return [String] unparsed JSON content
259
+ def read_from_url
260
+ text = open_url
261
+ store_in_cache(text) if valid_rates?(text) && cache
262
+ text
263
+ end
264
+
265
+ # Opens an url and reads the content
266
+ # @return [String] unparsed JSON content
267
+ def open_url
268
+ open(source_url).read
269
+ rescue OpenURI::HTTPError
270
+ ''
271
+ end
272
+
273
+ # Check validity of rates response only for store in cache
274
+ #
275
+ # @example
276
+ # valid_rates?("{\"rates\": {\"AED\": 3.67304}}")
277
+ #
278
+ # @param [String] text is JSON content
279
+ # @return [Boolean] valid or not
280
+ def valid_rates?(text)
281
+ parsed = JSON.parse(text)
282
+ parsed && parsed.key?('rates')
283
+ rescue JSON::ParserError
284
+ false
285
+ end
286
+
287
+ # Get exchange rates with different strategies
288
+ #
289
+ # @example
290
+ # exchange_rates(true)
291
+ # exchange_rates
292
+ #
293
+ # @param straight [Boolean] true for straight, default is careful
294
+ # @return [Hash] key is country code (ISO 3166-1 alpha-3) value Float
295
+ def exchange_rates(straight = false)
296
+ @rates = if straight
297
+ raw_rates_straight['rates']
298
+ else
299
+ raw_rates_careful['rates']
300
+ end
301
+ end
302
+
303
+ # Get raw exchange rates from cache and then from url
304
+ # @param rescue_straight [Boolean] true for rescue straight, default true
305
+ # @return [String] JSON content
306
+ def raw_rates_careful(rescue_straight = true)
307
+ JSON.parse(read_from_cache.to_s)
308
+ rescue JSON::ParserError
309
+ rescue_straight ? raw_rates_straight : { 'rates' => {} }
310
+ end
311
+
312
+ # Get raw exchange rates from url
313
+ # @return [String] JSON content
314
+ def raw_rates_straight
315
+ JSON.parse(read_from_url)
316
+ rescue JSON::ParserError
317
+ raw_rates_careful(false)
318
+ end
319
+
320
+ # Get rate or calculate it as inverse rate
321
+ # @param [String] from_currency Currency ISO code. ex. 'USD'
322
+ # @param [String] to_currency Currency ISO code. ex. 'CAD'
323
+ # @return [Numeric] rate or rate calculated as inverse rate.
324
+ def get_rate_or_calc_inverse(from_currency, to_currency, opts = {})
325
+ rate = super_get_rate(from_currency, to_currency, opts)
326
+ unless rate
327
+ # Tries to calculate an inverse rate
328
+ inverse_rate = super_get_rate(to_currency, from_currency, opts)
329
+ if inverse_rate
330
+ rate = 1.0 / inverse_rate
331
+ add_rate(from_currency, to_currency, rate)
332
+ end
333
+ end
334
+ rate
335
+ end
336
+
337
+ # Tries to calculate a pair rate using base currency rate
338
+ # @param [String] from_currency Currency ISO code. ex. 'USD'
339
+ # @param [String] to_currency Currency ISO code. ex. 'CAD'
340
+ # @return [Numeric] rate or nil if cannot calculate rate.
341
+ def calc_pair_rate_using_base(from_currency, to_currency, opts = {})
342
+ from_base_rate = get_rate_or_calc_inverse(source, from_currency, opts)
343
+ to_base_rate = get_rate_or_calc_inverse(source, to_currency, opts)
344
+ if to_base_rate && from_base_rate
345
+ rate = to_base_rate / from_base_rate
346
+ add_rate(from_currency, to_currency, rate)
347
+ return rate
348
+ end
349
+ nil
350
+ end
351
+ end
352
+ end
353
+ end
@@ -0,0 +1 @@
1
+ your access_key from https://openexchangerates.org/signup
@@ -0,0 +1,198 @@
1
+ {
2
+ "disclaimer": "Usage subject to terms: https://openexchangerates.org/terms",
3
+ "license": "https://openexchangerates.org/license",
4
+ "timestamp": 1440755169,
5
+ "base": "USD",
6
+ "rates": {
7
+ "AED": 3.673035,
8
+ "AFN": 68.7835,
9
+ "ALL": 120.3,
10
+ "AMD": 485.02,
11
+ "ANG": 1.789597,
12
+ "AOA": 165.9145,
13
+ "ARS": 16.0905,
14
+ "AUD": 1.337896,
15
+ "AWG": 1.800506,
16
+ "AZN": 1.685,
17
+ "BAM": 1.74995,
18
+ "BBD": 2,
19
+ "BDT": 81.152488,
20
+ "BGN": 1.749004,
21
+ "BHD": 0.37695,
22
+ "BIF": 1708.65,
23
+ "BMD": 1,
24
+ "BND": 1.389105,
25
+ "BOB": 6.987637,
26
+ "BRL": 3.269899,
27
+ "BSD": 1,
28
+ "BTC": 0.000424652342,
29
+ "BTN": 64.758477,
30
+ "BTS": 10.6793407974,
31
+ "BWP": 10.39206,
32
+ "BYN": 1.863063,
33
+ "BZD": 2.020847,
34
+ "CAD": 1.350885,
35
+ "CDF": 1428.863033,
36
+ "CHF": 0.97632,
37
+ "CLF": 0.025113,
38
+ "CLP": 674.325,
39
+ "CNH": 6.879369,
40
+ "CNY": 6.8903,
41
+ "COP": 2910.479832,
42
+ "CRC": 578.11,
43
+ "CUC": 1,
44
+ "CUP": 25.5,
45
+ "CVE": 99,
46
+ "CZK": 23.642313,
47
+ "DASH": 0.006760512,
48
+ "DJF": 178.77,
49
+ "DKK": 6.648526,
50
+ "DOGE": 269.551968987,
51
+ "DOP": 47.51621,
52
+ "DZD": 108.444,
53
+ "EAC": 4732.13456667,
54
+ "EGP": 18.0813,
55
+ "EMC": 0.837052105,
56
+ "ERN": 15.335585,
57
+ "ETB": 23.124034,
58
+ "ETH": 0.005376611,
59
+ "EUR": 0.886584,
60
+ "FCT": 0.0634141713,
61
+ "FJD": 2.074747,
62
+ "FKP": 0.771523,
63
+ "FTC": 25.1412108028,
64
+ "GBP": 0.771523,
65
+ "GEL": 2.40678,
66
+ "GGP": 0.771523,
67
+ "GHS": 4.345,
68
+ "GIP": 0.771523,
69
+ "GMD": 46.15,
70
+ "GNF": 9204.45,
71
+ "GTQ": 7.37438,
72
+ "GYD": 209.141431,
73
+ "HKD": 7.788739,
74
+ "HNL": 23.621643,
75
+ "HRK": 6.6359,
76
+ "HTG": 68.95723,
77
+ "HUF": 275.216,
78
+ "IDR": 13305.1241,
79
+ "ILS": 3.59376,
80
+ "IMP": 0.771523,
81
+ "INR": 64.7775,
82
+ "IQD": 1175.736141,
83
+ "IRR": 32454.938798,
84
+ "ISK": 100.464841,
85
+ "JEP": 0.771523,
86
+ "JMD": 130.676848,
87
+ "JOD": 0.709503,
88
+ "JPY": 111.794375,
89
+ "KES": 103.285,
90
+ "KGS": 67.885152,
91
+ "KHR": 4074.45,
92
+ "KMF": 440.2,
93
+ "KPW": 899.91,
94
+ "KRW": 1124.165,
95
+ "KWD": 0.303677,
96
+ "KYD": 0.837868,
97
+ "KZT": 310.93,
98
+ "LAK": 8237.85,
99
+ "LBP": 1517.2,
100
+ "LD": 251.7,
101
+ "LKR": 152.74,
102
+ "LRD": 94.497383,
103
+ "LSL": 13.310652,
104
+ "LTC": 0.031068873,
105
+ "LYD": 1.404403,
106
+ "MAD": 9.76775,
107
+ "MDL": 18.284966,
108
+ "MGA": 3086.6,
109
+ "MKD": 55.054704,
110
+ "MMK": 1377.4,
111
+ "MNT": 2410.880742,
112
+ "MOP": 8.065265,
113
+ "MRO": 362.423894,
114
+ "MUR": 34.746,
115
+ "MVR": 15.570203,
116
+ "MWK": 726.14,
117
+ "MXN": 18.589873,
118
+ "MYR": 4.290006,
119
+ "MZN": 70.932906,
120
+ "NAD": 13.02095,
121
+ "NGN": 320,
122
+ "NIO": 29.939182,
123
+ "NMC": 0.4415677667,
124
+ "NOK": 8.39117,
125
+ "NPR": 104.278757,
126
+ "NVC": 0.2067437432,
127
+ "NXT": 12.0649323229,
128
+ "NZD": 1.422657,
129
+ "OMR": 0.384999,
130
+ "PAB": 1,
131
+ "PEN": 3.281961,
132
+ "PGK": 3.191399,
133
+ "PHP": 49.99,
134
+ "PKR": 105.39,
135
+ "PLN": 3.74213,
136
+ "PPC": 0.4604239038,
137
+ "PYG": 5622.8,
138
+ "QAR": 3.6415,
139
+ "RON": 4.0667,
140
+ "RSD": 109.81,
141
+ "RUB": 56.454783,
142
+ "RWF": 848.135575,
143
+ "SAR": 3.750449,
144
+ "SBD": 7.852215,
145
+ "SCR": 13.488435,
146
+ "SDG": 6.712818,
147
+ "SEK": 8.704256,
148
+ "SGD": 1.388248,
149
+ "SHP": 0.771523,
150
+ "SLL": 7502.169019,
151
+ "SOS": 581.655,
152
+ "SRD": 7.5375,
153
+ "SSP": 118.5751,
154
+ "STD": 21882.388901,
155
+ "STR": 17.4617511685,
156
+ "SVC": 8.797503,
157
+ "SYP": 214.35,
158
+ "SZL": 13.312834,
159
+ "THB": 34.37,
160
+ "TJS": 8.824018,
161
+ "TMT": 3.504979,
162
+ "TND": 2.407735,
163
+ "TOP": 2.311691,
164
+ "TRY": 3.564739,
165
+ "TTD": 6.765558,
166
+ "TWD": 30.17,
167
+ "TZS": 2237.3,
168
+ "UAH": 26.265,
169
+ "UGX": 3631.55,
170
+ "USD": 1,
171
+ "UYU": 28.265527,
172
+ "UZS": 3841.15,
173
+ "VEF": 9.985022,
174
+ "VEF_BLKMKT": 5890.73,
175
+ "VEF_DICOM": 721.33,
176
+ "VEF_DIPRO": 10,
177
+ "VND": 22679.057152,
178
+ "VTC": 2.3128712447,
179
+ "VUV": 108.260636,
180
+ "WST": 2.568925,
181
+ "XAF": 586.424407,
182
+ "XAG": 0.05858746,
183
+ "XAU": 0.00079841,
184
+ "XCD": 2.70255,
185
+ "XDR": 0.722859,
186
+ "XMR": 0.0177529017,
187
+ "XOF": 586.982338,
188
+ "XPD": 0.00129134,
189
+ "XPF": 106.897736,
190
+ "XPM": 1.4437020712,
191
+ "XPT": 0.00105933,
192
+ "XRP": 3.054084697,
193
+ "YER": 250.294142,
194
+ "ZAR": 12.963878,
195
+ "ZMW": 9.315,
196
+ "ZWL": 322.387247
197
+ }
198
+ }
@@ -0,0 +1,305 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
5
+
6
+ describe Money::Bank::OpenexchangeratesBank do
7
+ subject { Money::Bank::OpenexchangeratesBank.new }
8
+ let(:url) { Money::Bank::OpenexchangeratesBank::CL_URL }
9
+ let(:secure_url) { Money::Bank::OpenexchangeratesBank::CL_SECURE_URL }
10
+ let(:source) { Money::Bank::OpenexchangeratesBank::CL_SOURCE }
11
+ let(:temp_cache_path) do
12
+ File.expand_path(File.join(File.dirname(__FILE__), 'temp.json'))
13
+ end
14
+ let(:data_path) do
15
+ File.expand_path(File.join(File.dirname(__FILE__), 'live.json'))
16
+ end
17
+
18
+ describe 'exchange' do
19
+ before do
20
+ subject.access_key = TEST_ACCESS_KEY
21
+ subject.cache = temp_cache_path
22
+ stub(subject).source_url { data_path }
23
+ subject.update_rates
24
+ end
25
+
26
+ after do
27
+ File.unlink(temp_cache_path)
28
+ end
29
+
30
+ describe 'without rates' do
31
+ it 'able to exchange a money to its own currency even without rates' do
32
+ money = Money.new(0, 'USD')
33
+ subject.exchange_with(money, 'USD').must_equal money
34
+ end
35
+
36
+ it "raise if it can't find an exchange rate" do
37
+ money = Money.new(0, 'USD')
38
+ proc { subject.exchange_with(money, 'LTL') }
39
+ .must_raise Money::Bank::UnknownRate
40
+ end
41
+ end
42
+
43
+ describe 'with rates' do
44
+ before do
45
+ subject.update_rates
46
+ end
47
+
48
+ it 'should be able to exchange money from USD to a known exchange rate' do
49
+ money = Money.new(100, 'USD')
50
+ subject.exchange_with(money, 'BBD').must_equal Money.new(200, 'BBD')
51
+ end
52
+
53
+ it 'should be able to exchange money from a known exchange rate to USD' do
54
+ money = Money.new(200, 'BBD')
55
+ subject.exchange_with(money, 'USD').must_equal Money.new(100, 'USD')
56
+ end
57
+
58
+ it "should raise if it can't find an exchange rate" do
59
+ money = Money.new(0, 'USD')
60
+ proc { subject.exchange_with(money, 'LTL') }
61
+ .must_raise Money::Bank::UnknownRate
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'cache rates' do
67
+ before do
68
+ subject.access_key = TEST_ACCESS_KEY
69
+ subject.cache = temp_cache_path
70
+ stub(subject).source_url { data_path }
71
+ subject.update_rates
72
+ end
73
+
74
+ after do
75
+ File.delete(temp_cache_path) if File.exist?(temp_cache_path)
76
+ end
77
+
78
+ it 'should allow update after save' do
79
+ begin
80
+ subject.update_rates
81
+ rescue
82
+ assert false, 'Should allow updating after saving'
83
+ end
84
+ end
85
+
86
+ it 'should not break an existing file if save fails to read' do
87
+ initial_size = File.read(temp_cache_path).size
88
+ stub(subject).open_url { '' }
89
+ subject.update_rates
90
+ File.read(temp_cache_path).size.must_equal initial_size
91
+ end
92
+
93
+ it 'should not break an existing file if save returns json without rates' do
94
+ initial_size = File.read(temp_cache_path).size
95
+ stub(subject).open_url { '{ "error": "An error" }' }
96
+ subject.update_rates
97
+ File.read(temp_cache_path).size.must_equal initial_size
98
+ end
99
+
100
+ it 'should not break an existing file if save returns a invalid json' do
101
+ initial_size = File.read(temp_cache_path).size
102
+ stub(subject).open_url { '{ invalid_json: "An error" }' }
103
+ subject.update_rates
104
+ File.read(temp_cache_path).size.must_equal initial_size
105
+ end
106
+ end
107
+
108
+ describe 'no cache' do
109
+ before do
110
+ subject.cache = nil
111
+ subject.access_key = TEST_ACCESS_KEY
112
+ stub(subject).source_url { data_path }
113
+ end
114
+
115
+ it 'should get from url' do
116
+ subject.update_rates
117
+ subject.rates.wont_be_empty
118
+ end
119
+ end
120
+
121
+ describe 'no valid file for cache' do
122
+ before do
123
+ subject.cache = "space_dir#{rand(999_999_999)}/out_space_file.json"
124
+ subject.access_key = TEST_ACCESS_KEY
125
+ stub(subject).source_url { data_path }
126
+ end
127
+
128
+ it 'should raise an error if invalid path is given' do
129
+ proc { subject.update_rates }.must_raise Money::Bank::InvalidCache
130
+ end
131
+ end
132
+
133
+ describe 'using proc for cache' do
134
+ before :each do
135
+ @global_rates = nil
136
+ subject.cache = proc { |v|
137
+ if v
138
+ @global_rates = v
139
+ else
140
+ @global_rates
141
+ end
142
+ }
143
+ subject.access_key = TEST_ACCESS_KEY
144
+ end
145
+
146
+ it 'should get from url normally' do
147
+ stub(subject).source_url { data_path }
148
+ subject.update_rates
149
+ subject.rates.wont_be_empty
150
+ end
151
+
152
+ it 'should save from url and get from cache' do
153
+ stub(subject).source_url { data_path }
154
+ subject.update_rates
155
+ @global_rates.wont_be_empty
156
+ dont_allow(subject).source_url
157
+ subject.update_rates
158
+ subject.rates.wont_be_empty
159
+ end
160
+ end
161
+
162
+ describe '#secure_connection' do
163
+ it "should use the non-secure http url if secure_connection isn't set" do
164
+ subject.secure_connection = nil
165
+ subject.access_key = TEST_ACCESS_KEY
166
+ subject.source_url.must_equal "#{url}?base=#{source}&"\
167
+ "app_id=#{TEST_ACCESS_KEY}&"\
168
+ 'prettyprint=0'
169
+ end
170
+
171
+ it 'should use the non-secure http url if secure_connection is false' do
172
+ subject.secure_connection = false
173
+ subject.access_key = TEST_ACCESS_KEY
174
+ subject.source_url.must_equal "#{url}?base=#{source}&"\
175
+ "app_id=#{TEST_ACCESS_KEY}&"\
176
+ 'prettyprint=0'
177
+ end
178
+
179
+ it 'should use the secure https url if secure_connection is set to true' do
180
+ subject.secure_connection = true
181
+ subject.access_key = TEST_ACCESS_KEY
182
+ subject.source_url.must_equal "#{secure_url}?base=#{source}&"\
183
+ "app_id=#{TEST_ACCESS_KEY}&"\
184
+ 'prettyprint=0'
185
+ subject.source_url.must_include 'https://'
186
+ end
187
+ end
188
+
189
+ describe '#update_rates' do
190
+ before do
191
+ subject.access_key = TEST_ACCESS_KEY
192
+ subject.cache = data_path
193
+ stub(subject).source_url { data_path }
194
+ subject.update_rates
195
+ end
196
+
197
+ it 'should update itself with exchange rates from OpenexchangeratesBank' do
198
+ subject.rates.keys.each do |currency|
199
+ next unless Money::Currency.find(currency)
200
+ subject.get_rate('USD', currency).must_be :>, 0
201
+ end
202
+ end
203
+
204
+ it 'should not return 0 with integer rate' do
205
+ wtf = {
206
+ priority: 1,
207
+ iso_code: 'WTF',
208
+ name: 'WTF',
209
+ symbol: 'WTF',
210
+ subunit: 'Cent',
211
+ subunit_to_unit: 1000,
212
+ separator: '.',
213
+ delimiter: ','
214
+ }
215
+ Money::Currency.register(wtf)
216
+ Timecop.freeze(subject.rates_timestamp) do
217
+ subject.add_rate('USD', 'WTF', 2)
218
+ subject.add_rate('WTF', 'USD', 2)
219
+ subject.exchange_with(5000.to_money('WTF'), 'USD').cents
220
+ subject.exchange_with(5000.to_money('WTF'), 'USD').cents.wont_equal 0
221
+ end
222
+ end
223
+ end
224
+
225
+ describe '#access_key' do
226
+ before do
227
+ subject.cache = temp_cache_path
228
+ stub(OpenURI::OpenRead).open(url) { File.read data_path }
229
+ end
230
+
231
+ it 'should raise an error if no access key is set' do
232
+ proc { subject.update_rates }.must_raise Money::Bank::NoAccessKey
233
+ end
234
+ end
235
+
236
+ describe '#expire_rates!' do
237
+ before do
238
+ subject.access_key = TEST_ACCESS_KEY
239
+ subject.ttl_in_seconds = 1000
240
+ @old_usd_eur_rate = 0.655
241
+ # see test/live.json +59
242
+ @new_usd_eur_rate = 0.886584
243
+ subject.cache = temp_cache_path
244
+ stub(subject).source_url { data_path }
245
+ subject.update_rates
246
+ subject.add_rate('USD', 'EUR', @old_usd_eur_rate)
247
+ end
248
+
249
+ after do
250
+ File.delete(temp_cache_path) if File.exist?(temp_cache_path)
251
+ end
252
+
253
+ describe 'when the ttl has expired' do
254
+ it 'should update the rates' do
255
+ Timecop.freeze(subject.rates_timestamp + 1000) do
256
+ subject.get_rate('USD', 'EUR').must_equal @old_usd_eur_rate
257
+ end
258
+ Timecop.freeze(subject.rates_timestamp + 1001) do
259
+ subject.get_rate('USD', 'EUR').wont_equal @old_usd_eur_rate
260
+ subject.get_rate('USD', 'EUR').must_equal @new_usd_eur_rate
261
+ end
262
+ end
263
+
264
+ it 'updates the next expiration time' do
265
+ Timecop.freeze(subject.rates_timestamp + 1001) do
266
+ exp_time = subject.rates_timestamp + 1000
267
+ subject.expire_rates!
268
+ subject.rates_expiration.must_equal exp_time
269
+ end
270
+ end
271
+ end
272
+
273
+ describe 'when the ttl has not expired' do
274
+ it 'not should update the rates' do
275
+ subject.update_rates
276
+ exp_time = subject.rates_expiration
277
+ subject.expire_rates!
278
+ subject.rates_expiration.must_equal exp_time
279
+ end
280
+ end
281
+ end
282
+
283
+ describe '#rates_timestamp' do
284
+ before do
285
+ subject.access_key = TEST_ACCESS_KEY
286
+ subject.cache = temp_cache_path
287
+ stub(subject).source_url { data_path }
288
+ end
289
+
290
+ after do
291
+ File.delete(temp_cache_path) if File.exist?(temp_cache_path)
292
+ end
293
+
294
+ it 'should return 1970-01-01 datetime if no rates' do
295
+ stub(subject).open_url { '' }
296
+ subject.update_rates
297
+ subject.rates_timestamp.must_equal Time.at(0)
298
+ end
299
+
300
+ it 'should return a Time object' do
301
+ subject.update_rates
302
+ subject.rates_timestamp.class.must_equal Time
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'minitest/autorun'
5
+ require 'rr'
6
+ require 'money/bank/openexchangerates_bank'
7
+ require 'monetize'
8
+ require 'timecop'
9
+ require 'pry'
10
+
11
+ TEST_ACCESS_KEY_PATH = File.join(File.dirname(__FILE__), 'TEST_ACCESS_KEY')
12
+ TEST_ACCESS_KEY = ENV['TEST_ACCESS_KEY'] || File.read(TEST_ACCESS_KEY_PATH)
13
+
14
+ if TEST_ACCESS_KEY.nil? || TEST_ACCESS_KEY.empty?
15
+ raise "Please add a valid access key to file #{TEST_ACCESS_KEY_PATH} or to " \
16
+ ' TEST_ACCESS_KEY environment'
17
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: money-openexchangerates-bank
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Egon Zemmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-05-25 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.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.7'
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.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-line
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.6'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: 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'
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'
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.48.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.48.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
+ description: A gem that calculates the exchange rate using published rates from openexchangerates.org.
154
+ Compatible with the money gem.
155
+ email: office@phlegx.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files:
159
+ - README.md
160
+ files:
161
+ - Gemfile
162
+ - LICENSE
163
+ - README.md
164
+ - lib/money/bank/openexchangerates_bank.rb
165
+ - test/TEST_ACCESS_KEY
166
+ - test/live.json
167
+ - test/openexchangerates_bank_test.rb
168
+ - test/test_helper.rb
169
+ homepage: http://github.com/phlegx/money-openexchangerates-bank
170
+ licenses:
171
+ - MIT
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: 1.9.3
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubyforge_project:
189
+ rubygems_version: 2.5.2
190
+ signing_key:
191
+ specification_version: 4
192
+ summary: A gem that calculates the exchange rate using published rates from openexchangerates.org.
193
+ test_files:
194
+ - test/openexchangerates_bank_test.rb
195
+ has_rdoc: