simple_currency 1.0.2 → 1.1.1
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.
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -3
- data/README.rdoc +27 -16
- data/Rakefile +3 -3
- data/VERSION +1 -1
- data/lib/simple_currency/currency_convertible.rb +171 -34
- data/simple_currency.gemspec +9 -8
- data/spec/simple_currency_spec.rb +156 -50
- data/spec/spec_helper.rb +29 -0
- data/spec/support/xavier.xml +156 -0
- metadata +11 -10
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -31,13 +31,12 @@ GEM
|
|
31
31
|
arel (1.0.1)
|
32
32
|
activesupport (~> 3.0.0)
|
33
33
|
builder (2.1.2)
|
34
|
+
crack (0.1.8)
|
34
35
|
diff-lcs (1.1.2)
|
35
36
|
erubis (2.6.6)
|
36
37
|
abstract (>= 1.0.0)
|
37
38
|
fakeweb (1.3.0)
|
38
39
|
i18n (0.4.1)
|
39
|
-
json (1.4.6)
|
40
|
-
json (1.4.6-java)
|
41
40
|
mail (2.2.5)
|
42
41
|
activesupport (>= 2.3.6)
|
43
42
|
mime-types
|
@@ -81,7 +80,7 @@ PLATFORMS
|
|
81
80
|
ruby
|
82
81
|
|
83
82
|
DEPENDENCIES
|
83
|
+
crack
|
84
84
|
fakeweb
|
85
|
-
json
|
86
85
|
rails
|
87
86
|
rspec (>= 2.0.0.beta.20)
|
data/README.rdoc
CHANGED
@@ -1,8 +1,34 @@
|
|
1
1
|
= simple_currency
|
2
2
|
|
3
|
-
A really simple currency converter using Xurrency
|
3
|
+
A really simple currency converter using Xurrency and XavierMedia APIs.
|
4
4
|
Compatible with Ruby 1.8, 1.9 and JRuby.
|
5
5
|
|
6
|
+
== Usage
|
7
|
+
|
8
|
+
Just require it and all your numeric stuff gets this fancy DSL for free:
|
9
|
+
|
10
|
+
30.eur.to_usd
|
11
|
+
# => 38.08
|
12
|
+
|
13
|
+
150.eur.to_usd
|
14
|
+
# => 190.4
|
15
|
+
239.usd.to_eur
|
16
|
+
# => 187.98
|
17
|
+
# These don't even hit the Internets!
|
18
|
+
# (They take advantage of Rails cache)
|
19
|
+
|
20
|
+
70.usd.to_chf
|
21
|
+
# => 71.9
|
22
|
+
|
23
|
+
But what if you want to do the currency exchange according to a specific date
|
24
|
+
in the past? Just do this:
|
25
|
+
|
26
|
+
# You can use any time or date object
|
27
|
+
42.eur.at(1.year.ago).to_usd
|
28
|
+
# => 60.12
|
29
|
+
42.eur.at(Time.parse('2009-09-01')).to_usd
|
30
|
+
# => 60.12
|
31
|
+
|
6
32
|
== Installation
|
7
33
|
|
8
34
|
=== Rails 3
|
@@ -21,21 +47,6 @@ And manually require it as well:
|
|
21
47
|
|
22
48
|
require "simple_currency"
|
23
49
|
|
24
|
-
== Usage
|
25
|
-
|
26
|
-
Now all your numeric stuff gets this fancy DSL for free:
|
27
|
-
|
28
|
-
30.eur.to_usd
|
29
|
-
# => 38.08
|
30
|
-
|
31
|
-
150.eur.to_usd
|
32
|
-
# => 190.4
|
33
|
-
# This one doesn't even hit Xurrency API,
|
34
|
-
# it takes advantage of Rails cache!
|
35
|
-
|
36
|
-
70.usd.to_chf
|
37
|
-
# => 71.9
|
38
|
-
|
39
50
|
== Note on Patches/Pull Requests
|
40
51
|
|
41
52
|
* Fork the project.
|
data/Rakefile
CHANGED
@@ -6,13 +6,13 @@ begin
|
|
6
6
|
require 'jeweler'
|
7
7
|
Jeweler::Tasks.new do |gem|
|
8
8
|
gem.name = "simple_currency"
|
9
|
-
gem.summary = "A really simple currency converter using the Xurrency
|
10
|
-
gem.description = "A really simple currency converter using the Xurrency
|
9
|
+
gem.summary = "A really simple currency converter using the Xurrency and XavierMedia APIs."
|
10
|
+
gem.description = "A really simple currency converter using the Xurrency and XavierMedia APIs. It's Ruby 1.8, 1.9 and JRuby compatible, and it also takes advantage of Rails cache when available."
|
11
11
|
gem.email = "info@codegram.com"
|
12
12
|
gem.homepage = "http://github.com/codegram/simple_currency"
|
13
13
|
gem.authors = ["Oriol Gual", "Josep M. Bach", "Josep Jaume Rey"]
|
14
14
|
|
15
|
-
gem.add_dependency '
|
15
|
+
gem.add_dependency 'crack', ">= 0.1.8"
|
16
16
|
|
17
17
|
gem.add_development_dependency "jeweler", '>= 1.4.0'
|
18
18
|
gem.add_development_dependency "rspec", '>= 2.0.0.beta.20'
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.1.1
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'open-uri'
|
2
2
|
require 'timeout'
|
3
|
-
require '
|
3
|
+
require 'crack'
|
4
4
|
|
5
5
|
module CurrencyConvertible
|
6
6
|
|
@@ -15,75 +15,212 @@ module CurrencyConvertible
|
|
15
15
|
super(method,*args,&block)
|
16
16
|
end
|
17
17
|
|
18
|
+
# Historical exchange lookup
|
19
|
+
def at(exchange = nil)
|
20
|
+
begin
|
21
|
+
@exchange_date = exchange.send(:to_date)
|
22
|
+
rescue
|
23
|
+
raise "Must use 'at' with a time or date object"
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
18
28
|
private
|
19
|
-
|
29
|
+
|
30
|
+
# Called from first currency metamethod to set the original currency.
|
31
|
+
#
|
32
|
+
# 30.eur # => Calls _from and sets @original to 'eur'
|
33
|
+
#
|
20
34
|
def _from(currency)
|
35
|
+
@exchange_date = nil
|
21
36
|
@original = currency
|
22
37
|
self
|
23
38
|
end
|
24
39
|
|
40
|
+
# Called from last currency metamethod to set the target currency.
|
41
|
+
#
|
42
|
+
# 30.eur.to_usd
|
43
|
+
# # => Calls _to and returns the final value, say 38.08
|
44
|
+
#
|
25
45
|
def _to(target)
|
26
46
|
raise unless @original # Must be called after a _from have set the @original currency
|
27
47
|
|
48
|
+
return 0.0 if self == 0 # Obviously
|
49
|
+
|
28
50
|
original = @original
|
29
51
|
|
30
|
-
|
31
|
-
amount = self.abs
|
52
|
+
amount = self
|
32
53
|
|
33
54
|
# Check if there's a cached exchange rate for today
|
34
|
-
|
35
|
-
return cached_amount
|
36
|
-
end
|
55
|
+
return cached_amount(original, target, amount) if cached_rate(original, target)
|
37
56
|
|
38
|
-
# If not, perform
|
39
|
-
|
40
|
-
return nil unless json_response && parsed_response = JSON.parse(json_response)
|
41
|
-
raise parsed_response["message"] if parsed_response["status"] != "ok"
|
57
|
+
# If not, perform calls to APIs, deal with caches...
|
58
|
+
result = exchange(original, target, amount.abs)
|
42
59
|
|
43
|
-
|
60
|
+
# Cache methods
|
61
|
+
cache_currency_methods(original, target)
|
44
62
|
|
45
|
-
|
46
|
-
|
63
|
+
result
|
64
|
+
end
|
47
65
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
66
|
+
# Main method (called by _to) which calls Xavier or Xurrency strategies
|
67
|
+
# and returns a nice result.
|
68
|
+
#
|
69
|
+
def exchange(original, target, amount)
|
70
|
+
negative = (self < 0)
|
52
71
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
72
|
+
result = @exchange_date ? call_xavier_api(original, target, amount) : call_xurrency_api(original, target, amount)
|
73
|
+
|
74
|
+
# Round result to 2 decimals
|
75
|
+
result = sprintf("%.2f", result).to_f
|
57
76
|
|
58
77
|
return -(result) if negative
|
59
78
|
result
|
60
79
|
end
|
61
80
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
def call_api(original, target, amount)
|
81
|
+
##
|
82
|
+
# API call strategies
|
83
|
+
##
|
84
|
+
|
85
|
+
# Calls Xurrency API to perform the exchange without a specific date (assuming today)
|
86
|
+
#
|
87
|
+
def call_xurrency_api(original, target, amount)
|
71
88
|
api_url = "http://xurrency.com/api/#{[original, target, amount].join('/')}"
|
72
89
|
uri = URI.parse(api_url)
|
73
90
|
|
74
91
|
retries = 10
|
92
|
+
json_response = nil
|
75
93
|
begin
|
76
94
|
Timeout::timeout(1){
|
77
|
-
uri.open.read || nil # Returns the raw response
|
95
|
+
json_response = uri.open.read || nil # Returns the raw response
|
78
96
|
}
|
79
97
|
rescue Timeout::Error
|
80
98
|
retries -= 1
|
81
99
|
retries > 0 ? sleep(0.42) && retry : raise
|
82
100
|
end
|
101
|
+
|
102
|
+
return nil unless json_response && parsed_response = Crack::JSON.parse(json_response)
|
103
|
+
raise parsed_response['message'] if parsed_response['status'] != 'ok'
|
104
|
+
|
105
|
+
result = parsed_response['result']['value'].to_f
|
106
|
+
cache_rate(original, target, (result / amount))
|
107
|
+
result
|
83
108
|
end
|
84
109
|
|
85
|
-
|
86
|
-
|
110
|
+
# Calls Xavier API to perform the exchange with a specific date.
|
111
|
+
# This method is called when using :at method, like this:
|
112
|
+
#
|
113
|
+
# 30.eur.at(1.year.ago).to_usd
|
114
|
+
#
|
115
|
+
def call_xavier_api(original, target, amount)
|
116
|
+
|
117
|
+
# Check if there is any cached XML for the specified date
|
118
|
+
if defined?(Rails)
|
119
|
+
parsed_response = Rails.cache.read("xaviermedia_#{stringified_exchange_date}")
|
120
|
+
end
|
121
|
+
|
122
|
+
unless parsed_response # Unless there is a cached XML response, ask for it
|
123
|
+
date = @exchange_date
|
124
|
+
args = [date.year, date.month.to_s.rjust(2, '0'), date.day.to_s.rjust(2,'0')]
|
125
|
+
api_url = "http://api.finance.xaviermedia.com/api/#{args.join('/')}.xml"
|
126
|
+
uri = URI.parse(api_url)
|
127
|
+
|
128
|
+
retries = 10
|
129
|
+
xml_response = nil
|
130
|
+
begin
|
131
|
+
Timeout::timeout(1){
|
132
|
+
# Returns the raw response or
|
133
|
+
# raises OpenURI::HTTPError when no data available
|
134
|
+
xml_response = uri.open.read || nil
|
135
|
+
}
|
136
|
+
rescue Timeout::Error
|
137
|
+
retries -= 1
|
138
|
+
retries > 0 ? sleep(0.42) && retry : raise
|
139
|
+
end
|
140
|
+
|
141
|
+
return nil unless xml_response && parsed_response = Crack::XML.parse(xml_response)["xavierresponse"]["exchange_rates"]["fx"]
|
142
|
+
|
143
|
+
# Cache successful XML response for later reuse
|
144
|
+
if defined?(Rails)
|
145
|
+
Rails.cache.write("xaviermedia_#{stringified_exchange_date}", parsed_response)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Calculate the exchange rate from the XML
|
150
|
+
if parsed_response.first['basecurrency'].downcase == original
|
151
|
+
rate = parsed_response.select{|element| element["currency_code"].downcase == target}.first['rate'].to_f
|
152
|
+
elsif parsed_response.first['basecurrency'].downcase == target
|
153
|
+
target_rate = parsed_response.select{|element| element["currency_code"].downcase == original}.first['rate'].to_f
|
154
|
+
rate = 1 / target_rate if target_rate
|
155
|
+
else
|
156
|
+
original_rate = parsed_response.select{|element| element["currency_code"].downcase == original}.first['rate'].to_f
|
157
|
+
target_rate = parsed_response.select{|element| element["currency_code"].downcase == target}.first['rate'].to_f
|
158
|
+
rate = target_rate / original_rate
|
159
|
+
end
|
160
|
+
|
161
|
+
if rate
|
162
|
+
cache_rate(original, target, rate)
|
163
|
+
return amount * rate
|
164
|
+
end
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
|
168
|
+
# Caches currency methods to avoid method missing abuse.
|
169
|
+
#
|
170
|
+
def cache_currency_methods(original, target)
|
171
|
+
# Cache the _from method for faster reuse
|
172
|
+
self.class.send(:define_method, original.to_sym) do
|
173
|
+
_from(original)
|
174
|
+
end unless self.respond_to?(original.to_sym)
|
175
|
+
|
176
|
+
# Cache the _to method for faster reuse
|
177
|
+
self.class.send(:define_method, :"to_#{target}") do
|
178
|
+
_to(target)
|
179
|
+
end unless self.respond_to?(:"to_#{target}")
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Cache helper methods (only useful in a Rails app)
|
184
|
+
##
|
185
|
+
|
186
|
+
# Returns a suitable string-like date (like "25-8-2010"),
|
187
|
+
# aware of possible @exchange_date set by :at method
|
188
|
+
#
|
189
|
+
def stringified_exchange_date
|
190
|
+
value = (@exchange_date || Time.now.send(:to_date))
|
191
|
+
[value.day, value.month, value.year].join('-')
|
192
|
+
end
|
193
|
+
|
194
|
+
# Writes rate to the cache.
|
195
|
+
#
|
196
|
+
def cache_rate(original, target, rate)
|
197
|
+
Rails.cache.write("#{original}_#{target}_#{stringified_exchange_date}", rate) if defined?(Rails)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Tries to either get rate or calculate the inverse rate from cache.
|
201
|
+
#
|
202
|
+
# First looks for an "usd_eur_25-8-2010" entry in the cache,
|
203
|
+
# and if it does not find it, it looks for "eur_usd_25-8-2010" and
|
204
|
+
# inverts it.
|
205
|
+
#
|
206
|
+
def cached_rate(original, target)
|
207
|
+
if defined?(Rails)
|
208
|
+
unless rate = Rails.cache.read("#{original}_#{target}_#{stringified_exchange_date}")
|
209
|
+
rate = (1.0 / Rails.cache.read("#{target}_#{original}_#{stringified_exchange_date}")) rescue nil
|
210
|
+
end
|
211
|
+
rate
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Checks if there's a cached rate and calculates the result
|
216
|
+
# from the amount (or returns nil).
|
217
|
+
#
|
218
|
+
def cached_amount(original, target, amount)
|
219
|
+
if rate = cached_rate(original, target)
|
220
|
+
result = (amount * rate).to_f
|
221
|
+
return result = (result * 100).round.to_f / 100
|
222
|
+
end
|
223
|
+
nil
|
87
224
|
end
|
88
225
|
|
89
226
|
end
|
data/simple_currency.gemspec
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{simple_currency}
|
8
|
-
s.version = "1.
|
8
|
+
s.version = "1.1.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Oriol Gual", "Josep M. Bach", "Josep Jaume Rey"]
|
12
|
-
s.date = %q{2010-
|
13
|
-
s.description = %q{A really simple currency converter using the Xurrency
|
12
|
+
s.date = %q{2010-09-01}
|
13
|
+
s.description = %q{A really simple currency converter using the Xurrency and XavierMedia APIs. It's Ruby 1.8, 1.9 and JRuby compatible, and it also takes advantage of Rails cache when available.}
|
14
14
|
s.email = %q{info@codegram.com}
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
@@ -34,13 +34,14 @@ Gem::Specification.new do |s|
|
|
34
34
|
"lib/simple_currency/currency_convertible.rb",
|
35
35
|
"simple_currency.gemspec",
|
36
36
|
"spec/simple_currency_spec.rb",
|
37
|
-
"spec/spec_helper.rb"
|
37
|
+
"spec/spec_helper.rb",
|
38
|
+
"spec/support/xavier.xml"
|
38
39
|
]
|
39
40
|
s.homepage = %q{http://github.com/codegram/simple_currency}
|
40
41
|
s.rdoc_options = ["--charset=UTF-8"]
|
41
42
|
s.require_paths = ["lib"]
|
42
43
|
s.rubygems_version = %q{1.3.6}
|
43
|
-
s.summary = %q{A really simple currency converter using the Xurrency
|
44
|
+
s.summary = %q{A really simple currency converter using the Xurrency and XavierMedia APIs.}
|
44
45
|
s.test_files = [
|
45
46
|
"spec/simple_currency_spec.rb",
|
46
47
|
"spec/spec_helper.rb"
|
@@ -51,14 +52,14 @@ Gem::Specification.new do |s|
|
|
51
52
|
s.specification_version = 3
|
52
53
|
|
53
54
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
54
|
-
s.add_runtime_dependency(%q<
|
55
|
+
s.add_runtime_dependency(%q<crack>, [">= 0.1.8"])
|
55
56
|
s.add_development_dependency(%q<jeweler>, [">= 1.4.0"])
|
56
57
|
s.add_development_dependency(%q<rspec>, [">= 2.0.0.beta.20"])
|
57
58
|
s.add_development_dependency(%q<fakeweb>, [">= 1.3.0"])
|
58
59
|
s.add_development_dependency(%q<rails>, [">= 3.0.0"])
|
59
60
|
s.add_development_dependency(%q<bundler>, [">= 1.0.0"])
|
60
61
|
else
|
61
|
-
s.add_dependency(%q<
|
62
|
+
s.add_dependency(%q<crack>, [">= 0.1.8"])
|
62
63
|
s.add_dependency(%q<jeweler>, [">= 1.4.0"])
|
63
64
|
s.add_dependency(%q<rspec>, [">= 2.0.0.beta.20"])
|
64
65
|
s.add_dependency(%q<fakeweb>, [">= 1.3.0"])
|
@@ -66,7 +67,7 @@ Gem::Specification.new do |s|
|
|
66
67
|
s.add_dependency(%q<bundler>, [">= 1.0.0"])
|
67
68
|
end
|
68
69
|
else
|
69
|
-
s.add_dependency(%q<
|
70
|
+
s.add_dependency(%q<crack>, [">= 0.1.8"])
|
70
71
|
s.add_dependency(%q<jeweler>, [">= 1.4.0"])
|
71
72
|
s.add_dependency(%q<rspec>, [">= 2.0.0.beta.20"])
|
72
73
|
s.add_dependency(%q<fakeweb>, [">= 1.3.0"])
|
@@ -2,89 +2,195 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
2
|
|
3
3
|
describe "SimpleCurrency" do
|
4
4
|
|
5
|
-
|
6
|
-
args = [from_currency, to_currency, amount]
|
5
|
+
describe "in its basic behavior" do
|
7
6
|
|
8
|
-
|
7
|
+
it "enhances instances of Numeric with currency methods" do
|
8
|
+
expect { 30.eur && 30.usd && 30.gbp }.to_not raise_error
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
+
it "handles zero values (by returning them straight away)" do
|
12
|
+
0.usd.to_eur.should == 0.0
|
13
|
+
end
|
11
14
|
|
12
|
-
FakeWeb.register_uri(:get, "http://xurrency.com/api/#{args.join('/')}", :body => response)
|
13
15
|
end
|
14
16
|
|
15
|
-
|
16
|
-
expect { 30.eur && 30.usd && 30.gbp }.to_not raise_error
|
17
|
-
end
|
17
|
+
context "using Xurrency API for right-now exchange" do
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
it "returns a converted amount from one currency to another" do
|
20
|
+
mock_xurrency_api('eur', 'usd', 2, 1.542)
|
21
|
+
2.eur.to_usd.should == 1.54
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
mock_xurrency_api('gbp', 'chf', 2, 3.4874)
|
24
|
+
2.gbp.to_chf.should == 3.49
|
25
|
+
end
|
26
|
+
|
27
|
+
it "caches methods for faster reuse" do
|
28
|
+
mock_xurrency_api('usd', 'eur', 1, 1.5)
|
26
29
|
|
27
|
-
|
28
|
-
mock_uri('usd', 'eur', 1, 1.5)
|
30
|
+
1.usd.to_eur.should == 1.5
|
29
31
|
|
30
|
-
|
32
|
+
8.should respond_to(:usd)
|
33
|
+
8.should respond_to(:to_eur)
|
34
|
+
end
|
31
35
|
|
32
|
-
|
33
|
-
|
34
|
-
|
36
|
+
it "raises any error returned by the api call" do
|
37
|
+
mock_xurrency_api('usd', 'xxx', 1, 1.5, :fail_with => "The currencies are not valid")
|
38
|
+
mock_xurrency_api('usd', 'eur', 1_000_000_000, 1.5, :fail_with => "The amount should be between 0 and 999999999")
|
39
|
+
|
40
|
+
expect {1.usd.to_xxx}.to raise_error("The currencies are not valid")
|
41
|
+
expect {1_000_000_000.usd.to_eur}.to raise_error("The amount should be between 0 and 999999999")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "handles a negative value returning a negative as well" do
|
45
|
+
mock_xurrency_api('usd', 'eur', 1, 1.5)
|
35
46
|
|
36
|
-
|
37
|
-
|
38
|
-
mock_uri('usd', 'eur', 1_000_000_000, 1.5, :fail_with => "The amount should be between 0 and 999999999")
|
47
|
+
-1.usd.to_eur.should == -1.5
|
48
|
+
end
|
39
49
|
|
40
|
-
expect {1.usd.to_xxx}.to raise_error("The currencies are not valid")
|
41
|
-
expect {1_000_000_000.usd.to_eur}.to raise_error("The amount should be between 0 and 999999999")
|
42
50
|
end
|
43
51
|
|
44
|
-
|
45
|
-
|
52
|
+
context "using XavierMedia API for exchange with historical date" do
|
53
|
+
|
54
|
+
let(:the_past) { Time.parse('2010-08-25')}
|
55
|
+
let(:the_ancient_past) { Time.parse('1964-08-25')}
|
56
|
+
|
57
|
+
it "enhances instances of Numeric with :at method" do
|
58
|
+
expect { 30.eur.at(Time.now) }.to_not raise_error
|
59
|
+
expect { 30.usd.at("no date")}.to raise_error("Must use 'at' with a time or date object")
|
60
|
+
expect { 30.gbp.at(:whatever_arg) }.to raise_error("Must use 'at' with a time or date object")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "returns a converted amount from one currency to another" do
|
64
|
+
mock_xavier_api(the_past)
|
65
|
+
2.eur.at(the_past).to_usd.should == 2.54
|
66
|
+
2.usd.at(the_past).to_eur.should == 1.58
|
67
|
+
2.gbp.at(the_past).to_usd.should == 3.07
|
68
|
+
end
|
69
|
+
|
70
|
+
it "raises an error when no data available" do
|
71
|
+
mock_xavier_api(the_past, :fail => true) # Currency does not exist!
|
72
|
+
mock_xavier_api(the_ancient_past, :fail => true) # Too old!
|
73
|
+
|
74
|
+
expect {1.usd.at(the_past).to_xxx}.to raise_error(OpenURI::HTTPError)
|
75
|
+
expect {1.usd.at(the_ancient_past).to_eur}.to raise_error(OpenURI::HTTPError)
|
76
|
+
end
|
46
77
|
|
47
|
-
-1.usd.to_eur.should == -1.5
|
48
78
|
end
|
49
79
|
|
50
|
-
context "when Rails is present" do
|
80
|
+
context "when Rails (and its cache goodness) is present" do
|
81
|
+
|
82
|
+
let(:now) { Time.parse('2010-08-30') }
|
51
83
|
|
52
84
|
before(:all) do
|
53
85
|
require 'rails'
|
54
86
|
end
|
55
87
|
|
56
|
-
|
88
|
+
context "using Xurrency API for right-now exchange" do
|
57
89
|
|
58
|
-
|
59
|
-
|
90
|
+
before(:each) do
|
91
|
+
Time.stub(:now).and_return(now)
|
60
92
|
|
61
|
-
|
62
|
-
|
93
|
+
Rails.stub_chain("cache.read").and_return(false)
|
94
|
+
Rails.stub_chain("cache.write").and_return(true)
|
95
|
+
end
|
63
96
|
|
64
|
-
|
65
|
-
|
66
|
-
end
|
97
|
+
it "reads the exchange rate from the cache" do
|
98
|
+
Rails.stub_chain("cache.read").and_return(1.5)
|
67
99
|
|
68
|
-
|
69
|
-
|
100
|
+
mock_xurrency_api('usd', 'eur', 1, 1.5)
|
101
|
+
1.usd.to_eur.should == 1.5
|
70
102
|
|
71
|
-
|
103
|
+
URI.should_not_receive(:parse)
|
104
|
+
2.usd.to_eur.should == 3
|
105
|
+
end
|
72
106
|
|
73
|
-
|
74
|
-
|
107
|
+
it "reads the inverse exchange rate from the cache" do
|
108
|
+
Rails.stub_chain("cache.read").with('usd_eur_30-8-2010').and_return(1.5)
|
109
|
+
Rails.stub_chain("cache.read").with('gbp_eur_30-8-2010').and_return(3)
|
110
|
+
Rails.stub_chain("cache.read").with('gbp_usd_30-8-2010').and_return(false)
|
75
111
|
|
76
|
-
|
77
|
-
|
112
|
+
mock_xurrency_api('usd', 'eur', 1, 1.5)
|
113
|
+
mock_xurrency_api('gbp', 'eur', 1, 3)
|
114
|
+
|
115
|
+
1.usd.to_eur.should == 1.5
|
116
|
+
1.gbp.to_eur.should == 3
|
117
|
+
|
118
|
+
URI.should_not_receive(:parse)
|
119
|
+
3.eur.to_usd.should == 2
|
120
|
+
3.eur.to_gbp.should == 1
|
121
|
+
end
|
122
|
+
|
123
|
+
it "caches the exchange rate" do
|
124
|
+
Rails.stub_chain("cache.read").with('usd_eur_30-8-2010').and_return(false)
|
125
|
+
|
126
|
+
Rails.cache.should_receive(:write).with('usd_eur_30-8-2010', 1.5)
|
127
|
+
|
128
|
+
mock_xurrency_api('usd', 'eur', 1, 1.5)
|
129
|
+
1.usd.to_eur.should == 1.5
|
130
|
+
end
|
78
131
|
|
79
|
-
|
80
|
-
|
132
|
+
it "ensures the cache is valid only for today" do
|
133
|
+
Rails.stub_chain("cache.read").with('usd_eur_30-8-2010').and_return(1.5)
|
81
134
|
|
82
|
-
|
83
|
-
|
84
|
-
|
135
|
+
mock_xurrency_api('usd', 'eur', 1, 1.5)
|
136
|
+
1.usd.to_eur.should == 1.5
|
137
|
+
|
138
|
+
Time.stub(:now).and_return(now + 86400) # One day later
|
139
|
+
Rails.stub_chain("cache.read").with('usd_eur_31-8-2010').and_return(nil)
|
140
|
+
|
141
|
+
# Exchange rate has changed next day, so forget cache rate!
|
142
|
+
mock_xurrency_api('usd', 'eur', 2, 4)
|
143
|
+
2.usd.to_eur.should == 4
|
144
|
+
end
|
85
145
|
|
86
146
|
end
|
87
147
|
|
88
|
-
|
148
|
+
context "using XavierMedia API for exchange with historical date" do
|
149
|
+
|
150
|
+
let(:the_past) { Time.parse('2010-08-25') }
|
151
|
+
|
152
|
+
before(:each) do
|
153
|
+
Rails.stub_chain("cache.read").and_return(false)
|
154
|
+
Rails.stub_chain("cache.write").and_return(true)
|
155
|
+
|
156
|
+
mock_xavier_api(the_past)
|
157
|
+
end
|
89
158
|
|
159
|
+
it "reads the exchange rate from the cache" do
|
160
|
+
Rails.stub_chain("cache.read").with('usd_eur_25-8-2010').and_return(0.79)
|
161
|
+
|
162
|
+
URI.should_not_receive(:parse)
|
163
|
+
1.usd.at(the_past).to_eur.should == 0.79
|
164
|
+
2.usd.at(the_past).to_eur.should == 1.58
|
165
|
+
end
|
166
|
+
|
167
|
+
it "caches the exchange rate with full precision" do
|
168
|
+
Rails.cache.should_receive(:write).with('eur_jpy_25-8-2010', 107.07).ordered
|
169
|
+
Rails.cache.should_receive(:write).with("eur_usd_25-8-2010", 1.268).ordered
|
170
|
+
Rails.cache.should_receive(:write).with("eur_huf_25-8-2010", 287.679993).ordered
|
171
|
+
|
172
|
+
1.eur.at(the_past).to_jpy
|
173
|
+
1.eur.at(the_past).to_usd
|
174
|
+
2.eur.at(the_past).to_huf
|
175
|
+
end
|
176
|
+
|
177
|
+
it "caches the XML response" do
|
178
|
+
Rails.cache.should_receive(:write).with('xaviermedia_25-8-2010', an_instance_of(Array)).and_return(true)
|
179
|
+
|
180
|
+
1.eur.at(the_past).to_usd
|
181
|
+
end
|
182
|
+
|
183
|
+
it "uses the base currency (EUR) cache to calculate other rates" do
|
184
|
+
1.eur.at(the_past).to_jpy.should == 107.07
|
185
|
+
|
186
|
+
Rails.stub_chain("cache.read").with('xaviermedia_25-8-2010').and_return(Crack::XML.parse(fixture('xavier.xml'))["xavierresponse"]["exchange_rates"]["fx"])
|
187
|
+
|
188
|
+
URI.should_not_receive(:parse)
|
189
|
+
|
190
|
+
1.usd.at(the_past).to_eur.should == 0.79
|
191
|
+
1.eur.at(the_past).to_gbp.should == 0.82
|
192
|
+
2.gbp.at(the_past).to_usd.should == 3.07
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
90
196
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -7,3 +7,32 @@ require 'fakeweb'
|
|
7
7
|
require 'simple_currency'
|
8
8
|
require 'rspec'
|
9
9
|
require 'rspec/autorun'
|
10
|
+
|
11
|
+
module HelperMethods
|
12
|
+
def fixture(name)
|
13
|
+
File.read(File.dirname(__FILE__) + "/support/#{name}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def mock_xurrency_api(from_currency, to_currency, amount, result, options = {})
|
17
|
+
args = [from_currency, to_currency, amount]
|
18
|
+
|
19
|
+
response = "{\"result\":{\"value\":#{result},\"target\":\"#{to_currency}\",\"base\":\"#{from_currency}\"},\"status\":\"ok\"}"
|
20
|
+
|
21
|
+
response = "{\"message\":\"#{options[:fail_with]}\", \"status\":\"fail\"\}" if options[:fail_with]
|
22
|
+
|
23
|
+
FakeWeb.register_uri(:get, "http://xurrency.com/api/#{args.join('/')}", :body => response)
|
24
|
+
end
|
25
|
+
|
26
|
+
def mock_xavier_api(date, options = {})
|
27
|
+
date = date.send(:to_date)
|
28
|
+
args = [date.year, date.month.to_s.rjust(2, '0'), date.day.to_s.rjust(2,'0')]
|
29
|
+
|
30
|
+
response = {:body => fixture("xavier.xml")}
|
31
|
+
response = {:body => "No exchange rate available", :status => ["404", "Not Found"]} if options[:fail]
|
32
|
+
|
33
|
+
FakeWeb.register_uri(:get, "http://api.finance.xaviermedia.com/api/#{args.join('/')}.xml", response)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec.configuration.include(HelperMethods)
|
@@ -0,0 +1,156 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
2
|
+
<?xml-stylesheet href="http://api.finance.xaviermedia.com/style/daily_rates.xsl" type="text/xsl"?>
|
3
|
+
<xavierresponse responsecode="200">
|
4
|
+
<fx_date>2010-08-31</fx_date>
|
5
|
+
<title>Xavier Finance - Exchange rates for 2010-08-31</title>
|
6
|
+
<link>http://finance.xaviermedia.com/</link>
|
7
|
+
<exchange_rates>
|
8
|
+
<basecurrency>EUR</basecurrency>
|
9
|
+
<fx_date>2010-08-31</fx_date>
|
10
|
+
<fx basecurrency="EUR">
|
11
|
+
<currency_code>EUR</currency_code>
|
12
|
+
<rate>1.0</rate>
|
13
|
+
</fx>
|
14
|
+
<fx basecurrency="EUR">
|
15
|
+
<currency_code>USD</currency_code>
|
16
|
+
<rate>1.268000</rate>
|
17
|
+
</fx>
|
18
|
+
<fx basecurrency="EUR">
|
19
|
+
<currency_code>JPY</currency_code>
|
20
|
+
<rate>107.070000</rate>
|
21
|
+
</fx>
|
22
|
+
<fx basecurrency="EUR">
|
23
|
+
<currency_code>GBP</currency_code>
|
24
|
+
<rate>0.824800</rate>
|
25
|
+
</fx>
|
26
|
+
<fx basecurrency="EUR">
|
27
|
+
<currency_code>CYP</currency_code>
|
28
|
+
<rate>0.000000</rate>
|
29
|
+
</fx>
|
30
|
+
<fx basecurrency="EUR">
|
31
|
+
<currency_code>CZK</currency_code>
|
32
|
+
<rate>24.850000</rate>
|
33
|
+
</fx>
|
34
|
+
<fx basecurrency="EUR">
|
35
|
+
<currency_code>DKK</currency_code>
|
36
|
+
<rate>7.444800</rate>
|
37
|
+
</fx>
|
38
|
+
<fx basecurrency="EUR">
|
39
|
+
<currency_code>EEK</currency_code>
|
40
|
+
<rate>15.646600</rate>
|
41
|
+
</fx>
|
42
|
+
<fx basecurrency="EUR">
|
43
|
+
<currency_code>HUF</currency_code>
|
44
|
+
<rate>287.679993</rate>
|
45
|
+
</fx>
|
46
|
+
<fx basecurrency="EUR">
|
47
|
+
<currency_code>LTL</currency_code>
|
48
|
+
<rate>3.452800</rate>
|
49
|
+
</fx>
|
50
|
+
<fx basecurrency="EUR">
|
51
|
+
<currency_code>MTL</currency_code>
|
52
|
+
<rate>0.000000</rate>
|
53
|
+
</fx>
|
54
|
+
<fx basecurrency="EUR">
|
55
|
+
<currency_code>PLN</currency_code>
|
56
|
+
<rate>4.013500</rate>
|
57
|
+
</fx>
|
58
|
+
<fx basecurrency="EUR">
|
59
|
+
<currency_code>SEK</currency_code>
|
60
|
+
<rate>9.389000</rate>
|
61
|
+
</fx>
|
62
|
+
<fx basecurrency="EUR">
|
63
|
+
<currency_code>SIT</currency_code>
|
64
|
+
<rate>0.000000</rate>
|
65
|
+
</fx>
|
66
|
+
<fx basecurrency="EUR">
|
67
|
+
<currency_code>SKK</currency_code>
|
68
|
+
<rate>0.000000</rate>
|
69
|
+
</fx>
|
70
|
+
<fx basecurrency="EUR">
|
71
|
+
<currency_code>CHF</currency_code>
|
72
|
+
<rate>1.293500</rate>
|
73
|
+
</fx>
|
74
|
+
<fx basecurrency="EUR">
|
75
|
+
<currency_code>ISK</currency_code>
|
76
|
+
<rate>0.000000</rate>
|
77
|
+
</fx>
|
78
|
+
<fx basecurrency="EUR">
|
79
|
+
<currency_code>NOK</currency_code>
|
80
|
+
<rate>8.024500</rate>
|
81
|
+
</fx>
|
82
|
+
<fx basecurrency="EUR">
|
83
|
+
<currency_code>BGN</currency_code>
|
84
|
+
<rate>1.955800</rate>
|
85
|
+
</fx>
|
86
|
+
<fx basecurrency="EUR">
|
87
|
+
<currency_code>HRK</currency_code>
|
88
|
+
<rate>7.272500</rate>
|
89
|
+
</fx>
|
90
|
+
<fx basecurrency="EUR">
|
91
|
+
<currency_code>ROL</currency_code>
|
92
|
+
<rate>0.000000</rate>
|
93
|
+
</fx>
|
94
|
+
<fx basecurrency="EUR">
|
95
|
+
<currency_code>RON</currency_code>
|
96
|
+
<rate>4.256800</rate>
|
97
|
+
</fx>
|
98
|
+
<fx basecurrency="EUR">
|
99
|
+
<currency_code>RUB</currency_code>
|
100
|
+
<rate>39.103802</rate>
|
101
|
+
</fx>
|
102
|
+
<fx basecurrency="EUR">
|
103
|
+
<currency_code>TRL</currency_code>
|
104
|
+
<rate>0.000000</rate>
|
105
|
+
</fx>
|
106
|
+
<fx basecurrency="EUR">
|
107
|
+
<currency_code>AUD</currency_code>
|
108
|
+
<rate>1.430400</rate>
|
109
|
+
</fx>
|
110
|
+
<fx basecurrency="EUR">
|
111
|
+
<currency_code>CAD</currency_code>
|
112
|
+
<rate>1.348900</rate>
|
113
|
+
</fx>
|
114
|
+
<fx basecurrency="EUR">
|
115
|
+
<currency_code>CNY</currency_code>
|
116
|
+
<rate>8.631800</rate>
|
117
|
+
</fx>
|
118
|
+
<fx basecurrency="EUR">
|
119
|
+
<currency_code>HKD</currency_code>
|
120
|
+
<rate>9.865300</rate>
|
121
|
+
</fx>
|
122
|
+
<fx basecurrency="EUR">
|
123
|
+
<currency_code>IDR</currency_code>
|
124
|
+
<rate>11471.820</rate>
|
125
|
+
</fx>
|
126
|
+
<fx basecurrency="EUR">
|
127
|
+
<currency_code>KRW</currency_code>
|
128
|
+
<rate>1520.050049</rate>
|
129
|
+
</fx>
|
130
|
+
<fx basecurrency="EUR">
|
131
|
+
<currency_code>MYR</currency_code>
|
132
|
+
<rate>3.993600</rate>
|
133
|
+
</fx>
|
134
|
+
<fx basecurrency="EUR">
|
135
|
+
<currency_code>NZD</currency_code>
|
136
|
+
<rate>1.818700</rate>
|
137
|
+
</fx>
|
138
|
+
<fx basecurrency="EUR">
|
139
|
+
<currency_code>PHP</currency_code>
|
140
|
+
<rate>57.456001</rate>
|
141
|
+
</fx>
|
142
|
+
<fx basecurrency="EUR">
|
143
|
+
<currency_code>SGD</currency_code>
|
144
|
+
<rate>1.719300</rate>
|
145
|
+
</fx>
|
146
|
+
<fx basecurrency="EUR">
|
147
|
+
<currency_code>THB</currency_code>
|
148
|
+
<rate>39.669998</rate>
|
149
|
+
</fx>
|
150
|
+
<fx basecurrency="EUR">
|
151
|
+
<currency_code>ZAR</currency_code>
|
152
|
+
<rate>9.404400</rate>
|
153
|
+
</fx>
|
154
|
+
</exchange_rates>
|
155
|
+
</xavierresponse>
|
156
|
+
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 1
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 1.
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 1.1.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Oriol Gual
|
@@ -16,21 +16,21 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2010-
|
19
|
+
date: 2010-09-01 00:00:00 +02:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
23
|
-
name:
|
23
|
+
name: crack
|
24
24
|
prerelease: false
|
25
25
|
requirement: &id001 !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
segments:
|
30
|
+
- 0
|
30
31
|
- 1
|
31
|
-
-
|
32
|
-
|
33
|
-
version: 1.4.3
|
32
|
+
- 8
|
33
|
+
version: 0.1.8
|
34
34
|
type: :runtime
|
35
35
|
version_requirements: *id001
|
36
36
|
- !ruby/object:Gem::Dependency
|
@@ -105,7 +105,7 @@ dependencies:
|
|
105
105
|
version: 1.0.0
|
106
106
|
type: :development
|
107
107
|
version_requirements: *id006
|
108
|
-
description: A really simple currency converter using the Xurrency
|
108
|
+
description: A really simple currency converter using the Xurrency and XavierMedia APIs. It's Ruby 1.8, 1.9 and JRuby compatible, and it also takes advantage of Rails cache when available.
|
109
109
|
email: info@codegram.com
|
110
110
|
executables: []
|
111
111
|
|
@@ -133,6 +133,7 @@ files:
|
|
133
133
|
- simple_currency.gemspec
|
134
134
|
- spec/simple_currency_spec.rb
|
135
135
|
- spec/spec_helper.rb
|
136
|
+
- spec/support/xavier.xml
|
136
137
|
has_rdoc: true
|
137
138
|
homepage: http://github.com/codegram/simple_currency
|
138
139
|
licenses: []
|
@@ -162,7 +163,7 @@ rubyforge_project:
|
|
162
163
|
rubygems_version: 1.3.6
|
163
164
|
signing_key:
|
164
165
|
specification_version: 3
|
165
|
-
summary: A really simple currency converter using the Xurrency
|
166
|
+
summary: A really simple currency converter using the Xurrency and XavierMedia APIs.
|
166
167
|
test_files:
|
167
168
|
- spec/simple_currency_spec.rb
|
168
169
|
- spec/spec_helper.rb
|