simple_currency 1.0.2 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|