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 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