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 CHANGED
@@ -1,6 +1,6 @@
1
1
  source :gemcutter
2
2
 
3
- gem "json"
3
+ gem "crack"
4
4
 
5
5
  group :test do
6
6
  gem "rspec", ">= 2.0.0.beta.20"
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 API.
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 API."
10
- gem.description = "A really simple currency converter using the Xurrency API. It's Ruby 1.8, 1.9 and JRuby compatible, and it also takes advantage of Rails cache when available."
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 'json', ">= 1.4.3"
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.0.2
1
+ 1.1.1
@@ -1,6 +1,6 @@
1
1
  require 'open-uri'
2
2
  require 'timeout'
3
- require 'json'
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
- negative = (self < 0)
31
- amount = self.abs
52
+ amount = self
32
53
 
33
54
  # Check if there's a cached exchange rate for today
34
- if defined?(Rails) && cached_amount = check_cache(original, target, amount)
35
- return cached_amount
36
- end
55
+ return cached_amount(original, target, amount) if cached_rate(original, target)
37
56
 
38
- # If not, perform a call to the api
39
- json_response = call_api(original, target, amount)
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
- result = sprintf("%.2f", parsed_response["result"]["value"]).to_f
60
+ # Cache methods
61
+ cache_currency_methods(original, target)
44
62
 
45
- # Cache exchange rate for today only
46
- Rails.cache.write("#{original}_#{target}_#{Time.now.to_a[3..5].join('-')}", calculate_rate(amount,result)) if defined?(Rails)
63
+ result
64
+ end
47
65
 
48
- # Cache the _from method for faster reuse
49
- self.class.send(:define_method, original.to_sym) do
50
- _from(original)
51
- end unless self.respond_to?(original.to_sym)
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
- # Cache the _to method for faster reuse
54
- self.class.send(:define_method, :"to_#{target}") do
55
- _to(target)
56
- end unless self.respond_to?(:"to_#{target}")
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
- def check_cache(original, target, amount)
63
- if rate = Rails.cache.read("#{original}_#{target}_#{Time.now.to_a[3..5].join('-')}")
64
- result = (amount * rate).to_f
65
- return result = (result * 100).round.to_f / 100
66
- end
67
- nil
68
- end
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
- def calculate_rate(amount,result)
86
- (result / amount).to_f
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
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{simple_currency}
8
- s.version = "1.0.2"
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-08-31}
13
- s.description = %q{A really simple currency converter using the Xurrency API. It's Ruby 1.8, 1.9 and JRuby compatible, and it also takes advantage of Rails cache when available.}
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 API.}
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<json>, [">= 1.4.3"])
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<json>, [">= 1.4.3"])
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<json>, [">= 1.4.3"])
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
- def mock_uri(from_currency, to_currency, amount, result, options = {})
6
- args = [from_currency, to_currency, amount]
5
+ describe "in its basic behavior" do
7
6
 
8
- response = "{\"result\":{\"value\":#{result},\"target\":\"#{to_currency}\",\"base\":\"#{from_currency}\"},\"status\":\"ok\"}"
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
- response = "{\"message\":\"#{options[:fail_with]}\", \"status\":\"fail\"\}" if options[:fail_with]
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
- it "enhances instances of Numeric with currency methods" do
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
- it "returns a converted amount from one currency to another" do
20
- mock_uri('eur', 'usd', 2, 1.542)
21
- 2.eur.to_usd.should == 1.54
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
- mock_uri('gbp', 'chf', 2, 3.4874)
24
- 2.gbp.to_chf.should == 3.49
25
- end
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
- it "caches methods for faster reuse" do
28
- mock_uri('usd', 'eur', 1, 1.5)
30
+ 1.usd.to_eur.should == 1.5
29
31
 
30
- 1.usd.to_eur.should == 1.5
32
+ 8.should respond_to(:usd)
33
+ 8.should respond_to(:to_eur)
34
+ end
31
35
 
32
- 8.should respond_to(:usd)
33
- 8.should respond_to(:to_eur)
34
- end
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
- it "raises any error returned by the api call" do
37
- mock_uri('usd', 'xxx', 1, 1.5, :fail_with => "The currencies are not valid")
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
- it "handles a negative value returning a negative as well" do
45
- mock_uri('usd', 'eur', 1, 1.5)
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
- it "caches the exchange rate" do
88
+ context "using Xurrency API for right-now exchange" do
57
89
 
58
- Rails.stub_chain("cache.write").and_return(true)
59
- Rails.stub_chain("cache.read").and_return(1.5)
90
+ before(:each) do
91
+ Time.stub(:now).and_return(now)
60
92
 
61
- mock_uri('usd', 'eur', 1, 1.5)
62
- 1.usd.to_eur.should == 1.5
93
+ Rails.stub_chain("cache.read").and_return(false)
94
+ Rails.stub_chain("cache.write").and_return(true)
95
+ end
63
96
 
64
- URI.should_not_receive(:parse)
65
- 2.usd.to_eur.should == 3
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
- it "ensures the cache is valid only for today" do
69
- now = Time.parse('2010-08-30')
100
+ mock_xurrency_api('usd', 'eur', 1, 1.5)
101
+ 1.usd.to_eur.should == 1.5
70
102
 
71
- Time.stub(:now).and_return(now)
103
+ URI.should_not_receive(:parse)
104
+ 2.usd.to_eur.should == 3
105
+ end
72
106
 
73
- Rails.stub_chain("cache.write").and_return(true)
74
- Rails.stub_chain("cache.read").with('usd_eur_30-8-2010').and_return(1.5)
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
- mock_uri('usd', 'eur', 1, 1.5)
77
- 1.usd.to_eur.should == 1.5
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
- Time.stub(:now).and_return(now + 86400) # One day later
80
- Rails.stub_chain("cache.read").with('usd_eur_31-8-2010').and_return(nil)
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
- # Exchange rate has changed next day, so forget cache rate!
83
- mock_uri('usd', 'eur', 2, 4)
84
- 2.usd.to_eur.should == 4
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
- end
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
- - 0
8
- - 2
9
- version: 1.0.2
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-08-31 00:00:00 +02:00
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: json
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
- - 4
32
- - 3
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 API. It's Ruby 1.8, 1.9 and JRuby compatible, and it also takes advantage of Rails cache when available.
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 API.
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