money-openexchangerates-bank 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: