money-open-exchange-rates 0.0.7 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +23 -2
- data/lib/money/bank/open_exchange_rates_bank.rb +45 -26
- data/test/open_exchange_rates_bank_test.rb +72 -8
- data/test/test_helper.rb +7 -0
- metadata +14 -13
data/README.markdown
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Money Open Exchange Rates
|
2
2
|
|
3
|
-
A gem that calculates the exchange rate using published rates from
|
3
|
+
A gem that calculates the exchange rate using published rates from
|
4
|
+
[open-exchange-rates](http://josscrowcroft.github.com/open-exchange-rates/)
|
4
5
|
|
5
6
|
## Usage
|
6
7
|
|
@@ -8,14 +9,33 @@ A gem that calculates the exchange rate using published rates from [open-exchang
|
|
8
9
|
require 'money/bank/open_exchange_rates_bank'
|
9
10
|
moe = Money::Bank::OpenExchangeRatesBank.new
|
10
11
|
moe.cache = 'path/to/file/cache'
|
12
|
+
moe.app_id = 'your app id from https://openexchangerates.org/signup'
|
11
13
|
moe.update_rates
|
12
14
|
|
13
15
|
Money.default_bank = moe
|
14
16
|
```
|
15
17
|
|
18
|
+
You can also provide a Proc as a cache to provide your own caching mechanism
|
19
|
+
perhaps with Redis or just a thread safe `Hash` (global). For example:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
moe.cache = Proc.new do |v|
|
23
|
+
key = 'money:exchange_rates']
|
24
|
+
if v
|
25
|
+
Thread.current[key] = v
|
26
|
+
else
|
27
|
+
Thread.current[key]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
16
32
|
## Tests
|
17
33
|
|
18
|
-
|
34
|
+
As of the end of August 2012 all requests to the Open Exchange Rates API must
|
35
|
+
have a valid app_id. You can place your own key on a file named TEST_APP_ID and
|
36
|
+
then run:
|
37
|
+
|
38
|
+
```bundle exec ruby test/open_exchange_rates_bank_test.rb```
|
19
39
|
|
20
40
|
## Refs
|
21
41
|
|
@@ -31,6 +51,7 @@ Money.default_bank = moe
|
|
31
51
|
* [Kevin Ball](https://github.com/kball)
|
32
52
|
* [Michael Morris](https://github.com/mtcmorris)
|
33
53
|
|
54
|
+
|
34
55
|
## License
|
35
56
|
The MIT License
|
36
57
|
|
@@ -1,18 +1,20 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
require 'open-uri'
|
3
|
-
require '
|
3
|
+
require 'multi_json'
|
4
4
|
require 'money'
|
5
5
|
|
6
6
|
class Money
|
7
7
|
module Bank
|
8
8
|
class InvalidCache < StandardError ; end
|
9
9
|
|
10
|
+
class NoAppId < StandardError ; end
|
11
|
+
|
10
12
|
class OpenExchangeRatesBank < Money::Bank::VariableExchange
|
11
13
|
|
12
14
|
OER_URL = 'http://openexchangerates.org/latest.json'
|
13
15
|
|
14
|
-
attr_accessor :cache
|
15
|
-
attr_reader :doc, :oer_rates
|
16
|
+
attr_accessor :cache, :app_id
|
17
|
+
attr_reader :doc, :oer_rates
|
16
18
|
|
17
19
|
def update_rates
|
18
20
|
exchange_rates.each do |exchange_rate|
|
@@ -23,25 +25,11 @@ class Money
|
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
|
-
def read_from_url
|
27
|
-
open(OER_URL).read
|
28
|
-
end
|
29
|
-
|
30
|
-
def has_valid_rates?(text)
|
31
|
-
parsed = Yajl::Parser.parse(text)
|
32
|
-
parsed && parsed.has_key?('rates')
|
33
|
-
rescue Yajl::ParseError
|
34
|
-
false
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
28
|
def save_rates
|
39
29
|
raise InvalidCache unless cache
|
40
|
-
|
41
|
-
if has_valid_rates?(
|
42
|
-
|
43
|
-
f.write(new_text)
|
44
|
-
end
|
30
|
+
text = read_from_url
|
31
|
+
if has_valid_rates?(text)
|
32
|
+
store_in_cache(text)
|
45
33
|
end
|
46
34
|
rescue Errno::ENOENT
|
47
35
|
raise InvalidCache
|
@@ -65,17 +53,48 @@ class Money
|
|
65
53
|
|
66
54
|
protected
|
67
55
|
|
68
|
-
|
69
|
-
|
70
|
-
|
56
|
+
# Store the provided text data by calling the proc method provided
|
57
|
+
# for the cache, or write to the cache file.
|
58
|
+
def store_in_cache(text)
|
59
|
+
if cache.is_a?(Proc)
|
60
|
+
cache.call(text)
|
61
|
+
elsif cache.is_a?(String)
|
62
|
+
open(cache, 'w') do |f|
|
63
|
+
f.write(text)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def read_from_cache
|
71
|
+
if cache.is_a?(Proc)
|
72
|
+
cache.call(nil)
|
73
|
+
elsif cache.is_a?(String) && File.exist?(cache)
|
74
|
+
open(cache).read
|
71
75
|
else
|
72
|
-
|
76
|
+
nil
|
73
77
|
end
|
74
78
|
end
|
75
79
|
|
80
|
+
def source_url
|
81
|
+
raise NoAppId if app_id.nil? || app_id.empty?
|
82
|
+
"#{OER_URL}?app_id=#{app_id}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def read_from_url
|
86
|
+
open(source_url).read
|
87
|
+
end
|
88
|
+
|
89
|
+
def has_valid_rates?(text)
|
90
|
+
parsed = MultiJson.decode(text)
|
91
|
+
parsed && parsed.has_key?('rates')
|
92
|
+
rescue MultiJson::DecodeError
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
76
96
|
def exchange_rates
|
77
|
-
@
|
78
|
-
@doc = Yajl::Parser.parse(open(rates_source).read)
|
97
|
+
@doc = MultiJson.decode(read_from_cache || read_from_url)
|
79
98
|
@oer_rates = @doc['rates']
|
80
99
|
@doc['rates']
|
81
100
|
end
|
@@ -4,11 +4,16 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
|
4
4
|
|
5
5
|
describe Money::Bank::OpenExchangeRatesBank do
|
6
6
|
|
7
|
+
before do
|
8
|
+
@cache_path = File.expand_path(File.join(File.dirname(__FILE__), 'latest.json'))
|
9
|
+
end
|
10
|
+
|
7
11
|
describe 'exchange' do
|
8
12
|
include RR::Adapters::TestUnit
|
9
13
|
|
10
14
|
before do
|
11
15
|
@bank = Money::Bank::OpenExchangeRatesBank.new
|
16
|
+
@bank.app_id = TEST_APP_ID
|
12
17
|
@temp_cache_path = File.expand_path(File.join(File.dirname(__FILE__), 'tmp.json'))
|
13
18
|
@bank.cache = @temp_cache_path
|
14
19
|
stub(OpenURI::OpenRead).open(Money::Bank::OpenExchangeRatesBank::OER_URL) { File.read @cache_path }
|
@@ -16,20 +21,20 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
16
21
|
end
|
17
22
|
|
18
23
|
it "should be able to exchange a money to its own currency even without rates" do
|
19
|
-
money = Money.new(0, "USD")
|
24
|
+
money = Money.new(0, "USD")
|
20
25
|
@bank.exchange_with(money, "USD").must_equal money
|
21
26
|
end
|
22
27
|
|
23
28
|
it "should raise if it can't find an exchange rate" do
|
24
|
-
money = Money.new(0, "USD")
|
29
|
+
money = Money.new(0, "USD")
|
25
30
|
assert_raises(Money::Bank::UnknownRateFormat){ @bank.exchange_with(money, "AUD") }
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
29
34
|
describe 'update_rates' do
|
30
35
|
before do
|
31
|
-
@cache_path = File.expand_path(File.join(File.dirname(__FILE__), 'latest.json'))
|
32
36
|
@bank = Money::Bank::OpenExchangeRatesBank.new
|
37
|
+
@bank.app_id = TEST_APP_ID
|
33
38
|
@bank.cache = @cache_path
|
34
39
|
@bank.update_rates
|
35
40
|
end
|
@@ -45,7 +50,8 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
45
50
|
@bank.oer_rates.keys.each do |currency|
|
46
51
|
next unless Money::Currency.find(currency)
|
47
52
|
subunit = Money::Currency.wrap(currency).subunit_to_unit
|
48
|
-
@bank.exchange(100, "USD", currency).cents.
|
53
|
+
@bank.exchange(100, "USD", currency).cents.
|
54
|
+
must_equal((@bank.oer_rates[currency].to_f * subunit).round)
|
49
55
|
end
|
50
56
|
end
|
51
57
|
|
@@ -53,8 +59,11 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
53
59
|
@bank.oer_rates.keys.each do |currency|
|
54
60
|
next unless Money::Currency.find(currency)
|
55
61
|
subunit = Money::Currency.wrap(currency).subunit_to_unit
|
56
|
-
@bank.exchange_with(Money.new(100, "USD"), currency).cents.
|
57
|
-
|
62
|
+
@bank.exchange_with(Money.new(100, "USD"), currency).cents.
|
63
|
+
must_equal((@bank.oer_rates[currency].to_f * subunit).round)
|
64
|
+
|
65
|
+
@bank.exchange_with(1.to_money("USD"), currency).cents.
|
66
|
+
must_equal((@bank.oer_rates[currency].to_f * subunit).round)
|
58
67
|
end
|
59
68
|
@bank.exchange_with(5000.to_money('JPY'), 'USD').cents.must_equal 6441
|
60
69
|
end
|
@@ -94,18 +103,38 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
94
103
|
end
|
95
104
|
end
|
96
105
|
|
106
|
+
describe 'App ID' do
|
107
|
+
include RR::Adapters::TestUnit
|
108
|
+
|
109
|
+
before do
|
110
|
+
@bank = Money::Bank::OpenExchangeRatesBank.new
|
111
|
+
@temp_cache_path = File.expand_path(File.join(File.dirname(__FILE__), 'tmp.json'))
|
112
|
+
@bank.cache = @temp_cache_path
|
113
|
+
stub(OpenURI::OpenRead).open(Money::Bank::OpenExchangeRatesBank::OER_URL) { File.read @cache_path }
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should raise an error if no App ID is set' do
|
117
|
+
proc {@bank.save_rates}.must_raise Money::Bank::NoAppId
|
118
|
+
end
|
119
|
+
|
120
|
+
#TODO: As App IDs are compulsory soon, need to add more tests handle
|
121
|
+
# app_id-specific errors from
|
122
|
+
# https://openexchangerates.org/documentation#errors
|
123
|
+
end
|
124
|
+
|
97
125
|
describe 'no cache' do
|
98
126
|
include RR::Adapters::TestUnit
|
99
127
|
|
100
128
|
before do
|
101
129
|
@bank = Money::Bank::OpenExchangeRatesBank.new
|
102
130
|
@bank.cache = nil
|
131
|
+
@bank.app_id = TEST_APP_ID
|
103
132
|
end
|
104
133
|
|
105
134
|
it 'should get from url' do
|
106
135
|
stub(OpenURI::OpenRead).open(Money::Bank::OpenExchangeRatesBank::OER_URL) { File.read @cache_path }
|
107
136
|
@bank.update_rates
|
108
|
-
@bank.
|
137
|
+
@bank.oer_rates.wont_be_empty
|
109
138
|
end
|
110
139
|
|
111
140
|
it 'should raise an error if invalid path is given to save_rates' do
|
@@ -118,12 +147,13 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
118
147
|
before do
|
119
148
|
@bank = Money::Bank::OpenExchangeRatesBank.new
|
120
149
|
@bank.cache = "space_dir#{rand(999999999)}/out_space_file.json"
|
150
|
+
@bank.app_id = TEST_APP_ID
|
121
151
|
end
|
122
152
|
|
123
153
|
it 'should get from url' do
|
124
154
|
stub(OpenURI::OpenRead).open(Money::Bank::OpenExchangeRatesBank::OER_URL) { File.read @cache_path }
|
125
155
|
@bank.update_rates
|
126
|
-
@bank.
|
156
|
+
@bank.oer_rates.wont_be_empty
|
127
157
|
end
|
128
158
|
|
129
159
|
it 'should raise an error if invalid path is given to save_rates' do
|
@@ -131,11 +161,45 @@ describe Money::Bank::OpenExchangeRatesBank do
|
|
131
161
|
end
|
132
162
|
end
|
133
163
|
|
164
|
+
describe 'using proc for cache' do
|
165
|
+
include RR::Adapters::TestUnit
|
166
|
+
|
167
|
+
before :each do
|
168
|
+
$global_rates = nil
|
169
|
+
@bank = Money::Bank::OpenExchangeRatesBank.new
|
170
|
+
@bank.cache = Proc.new {|v|
|
171
|
+
if v
|
172
|
+
$global_rates = v
|
173
|
+
else
|
174
|
+
$global_rates
|
175
|
+
end
|
176
|
+
}
|
177
|
+
@bank.app_id = TEST_APP_ID
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'should get from url normally' do
|
181
|
+
stub(@bank).source_url() { @cache_path }
|
182
|
+
@bank.update_rates
|
183
|
+
@bank.oer_rates.wont_be_empty
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'should save from url and get from cache' do
|
187
|
+
stub(@bank).source_url { @cache_path }
|
188
|
+
@bank.save_rates
|
189
|
+
$global_rates.wont_be_empty
|
190
|
+
dont_allow(@bank).source_url
|
191
|
+
@bank.update_rates
|
192
|
+
@bank.oer_rates.wont_be_empty
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
134
197
|
describe 'save rates' do
|
135
198
|
include RR::Adapters::TestUnit
|
136
199
|
|
137
200
|
before do
|
138
201
|
@bank = Money::Bank::OpenExchangeRatesBank.new
|
202
|
+
@bank.app_id = "temp-e091fc14b3884a516d6cc2c299a"
|
139
203
|
@temp_cache_path = File.expand_path(File.join(File.dirname(__FILE__), 'tmp.json'))
|
140
204
|
@bank.cache = @temp_cache_path
|
141
205
|
stub(OpenURI::OpenRead).open(Money::Bank::OpenExchangeRatesBank::OER_URL) { File.read @cache_path }
|
data/test/test_helper.rb
CHANGED
@@ -2,3 +2,10 @@
|
|
2
2
|
require 'minitest/autorun'
|
3
3
|
require 'rr'
|
4
4
|
require 'money/bank/open_exchange_rates_bank'
|
5
|
+
|
6
|
+
TEST_APP_ID_PATH = File.join(File.dirname(__FILE__), '..', 'TEST_APP_ID')
|
7
|
+
TEST_APP_ID = ENV['TEST_APP_ID'] || File.read(TEST_APP_ID_PATH)
|
8
|
+
|
9
|
+
if TEST_APP_ID.nil? || TEST_APP_ID.empty?
|
10
|
+
raise "Please add a valid app id to file #{TEST_APP_ID_PATH} or to TEST_APP_ID environment"
|
11
|
+
end
|
metadata
CHANGED
@@ -1,30 +1,31 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: money-open-exchange-rates
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Laurent Arnoud
|
9
|
+
- Sam Lown
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2012-
|
13
|
+
date: 2012-09-30 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
16
|
-
requirement: &
|
16
|
+
name: multi_json
|
17
|
+
requirement: &17136700 !ruby/object:Gem::Requirement
|
17
18
|
none: false
|
18
19
|
requirements:
|
19
|
-
- -
|
20
|
+
- - ~>
|
20
21
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0
|
22
|
+
version: '1.0'
|
22
23
|
type: :runtime
|
23
24
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
+
version_requirements: *17136700
|
25
26
|
- !ruby/object:Gem::Dependency
|
26
27
|
name: money
|
27
|
-
requirement: &
|
28
|
+
requirement: &17136000 !ruby/object:Gem::Requirement
|
28
29
|
none: false
|
29
30
|
requirements:
|
30
31
|
- - ! '>='
|
@@ -32,10 +33,10 @@ dependencies:
|
|
32
33
|
version: 3.7.1
|
33
34
|
type: :runtime
|
34
35
|
prerelease: false
|
35
|
-
version_requirements: *
|
36
|
+
version_requirements: *17136000
|
36
37
|
- !ruby/object:Gem::Dependency
|
37
38
|
name: minitest
|
38
|
-
requirement: &
|
39
|
+
requirement: &17135280 !ruby/object:Gem::Requirement
|
39
40
|
none: false
|
40
41
|
requirements:
|
41
42
|
- - ! '>='
|
@@ -43,10 +44,10 @@ dependencies:
|
|
43
44
|
version: '2.0'
|
44
45
|
type: :development
|
45
46
|
prerelease: false
|
46
|
-
version_requirements: *
|
47
|
+
version_requirements: *17135280
|
47
48
|
- !ruby/object:Gem::Dependency
|
48
49
|
name: rr
|
49
|
-
requirement: &
|
50
|
+
requirement: &17134700 !ruby/object:Gem::Requirement
|
50
51
|
none: false
|
51
52
|
requirements:
|
52
53
|
- - ! '>='
|
@@ -54,7 +55,7 @@ dependencies:
|
|
54
55
|
version: 1.0.4
|
55
56
|
type: :development
|
56
57
|
prerelease: false
|
57
|
-
version_requirements: *
|
58
|
+
version_requirements: *17134700
|
58
59
|
description: A gem that calculates the exchange rate using published rates from open-exchange-rates.
|
59
60
|
Compatible with the money gem.
|
60
61
|
email: laurent@spkdev.net
|