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.
- checksums.yaml +7 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +225 -0
- data/lib/money/bank/openexchangerates_bank.rb +353 -0
- data/test/TEST_ACCESS_KEY +1 -0
- data/test/live.json +198 -0
- data/test/openexchangerates_bank_test.rb +305 -0
- data/test/test_helper.rb +17 -0
- metadata +195 -0
checksums.yaml
ADDED
@@ -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
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.
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# Money Openexchangerates Bank
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/money-openexchangerates-bank)
|
4
|
+
[](https://rubygems.org/gems/money-openexchangerates-bank)
|
5
|
+
[](https://travis-ci.org/phlegx/money-openexchangerates-bank)
|
6
|
+
[](https://codeclimate.com/github/phlegx/money-openexchangerates-bank)
|
7
|
+
[](http://inch-ci.org/github/phlegx/money-openexchangerates-bank)
|
8
|
+
[](https://gemnasium.com/phlegx/money-openexchangerates-bank)
|
9
|
+
[](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
|
data/test/live.json
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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:
|