exchange 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +262 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/exchange.gemspec +102 -0
- data/lib/core_extensions/conversability.rb +32 -0
- data/lib/exchange.rb +11 -0
- data/lib/exchange/cache.rb +4 -0
- data/lib/exchange/cache/base.rb +51 -0
- data/lib/exchange/cache/memcached.rb +52 -0
- data/lib/exchange/cache/rails.rb +45 -0
- data/lib/exchange/cache/redis.rb +54 -0
- data/lib/exchange/configuration.rb +72 -0
- data/lib/exchange/currency.rb +237 -0
- data/lib/exchange/external_api.rb +4 -0
- data/lib/exchange/external_api/base.rb +114 -0
- data/lib/exchange/external_api/call.rb +76 -0
- data/lib/exchange/external_api/currency_bot.rb +47 -0
- data/lib/exchange/external_api/xavier_media.rb +47 -0
- data/spec/core_extensions/conversability_spec.rb +64 -0
- data/spec/exchange/cache/base_spec.rb +29 -0
- data/spec/exchange/cache/memcached_spec.rb +72 -0
- data/spec/exchange/cache/rails_spec.rb +67 -0
- data/spec/exchange/cache/redis_spec.rb +76 -0
- data/spec/exchange/configuration_spec.rb +47 -0
- data/spec/exchange/currency_spec.rb +219 -0
- data/spec/exchange/external_api/base_spec.rb +31 -0
- data/spec/exchange/external_api/call_spec.rb +68 -0
- data/spec/exchange/external_api/currency_bot_spec.rb +61 -0
- data/spec/exchange/external_api/xavier_media_spec.rb +59 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/api_responses/example_historic_json.json +167 -0
- data/spec/support/api_responses/example_json_api.json +167 -0
- data/spec/support/api_responses/example_xml_api.xml +156 -0
- metadata +191 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
module Exchange
|
2
|
+
# The external API module. Every class Handling an API has to be placed here and inherit from base. It has to call an api and define
|
3
|
+
# a rates hash, an exchange base and a unix timestamp. The call will get cached automatically with the right structure
|
4
|
+
# Allows for easy extension with an own api, as shown below
|
5
|
+
# @author Beat Richartz
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
# @example Easily connect to your custom API by writing an ExternalAPI Class
|
9
|
+
# module Exchange
|
10
|
+
# module ExternalAPI
|
11
|
+
# class MyCustom < Base
|
12
|
+
# # Define here which currencies your API can handle
|
13
|
+
# CURRENCIES = %W(usd chf)
|
14
|
+
#
|
15
|
+
# # Every instance of ExternalAPI Class has to have an update function which gets the rates from the API
|
16
|
+
# def update(opts={})
|
17
|
+
# # assure that you will get a Time object for the historical dates
|
18
|
+
# time = assure_time(opts[:at])
|
19
|
+
#
|
20
|
+
# # call your API (shown here with a helper function that builds your API URL). Like this, your calls will get cached.
|
21
|
+
# Call.new(api_url(time), :at => time) do |result|
|
22
|
+
#
|
23
|
+
# # assign the currency conversion base, attention, this is readonly, so don't do self.base =
|
24
|
+
# @base = result['base']
|
25
|
+
#
|
26
|
+
# # assign the rates, this has to be a hash with the following format: {'USD' => 1.23242, 'CHF' => 1.34323}. Attention, this is readonly.
|
27
|
+
# @rates = result['rates']
|
28
|
+
#
|
29
|
+
# # timestamp the api call result. This may come in handy to assure you have the right result. Attention, this is readonly.
|
30
|
+
# @timestamp = result['timestamp'].to_i
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# private
|
35
|
+
#
|
36
|
+
# def api_url(time)
|
37
|
+
# # code a helper function that builds your api url for the specified time
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# # Now, you can configure your API in the configuration. The Symbol will get camelcased and constantized
|
44
|
+
# Exchange::Configuration.api = :my_custom
|
45
|
+
# # Have fun, and don't forget to write tests.
|
46
|
+
|
47
|
+
module ExternalAPI
|
48
|
+
|
49
|
+
# The Base class of all External APIs, handling basic exchange rates and conversion
|
50
|
+
# @author Beat Richartz
|
51
|
+
# @version 0.1
|
52
|
+
# @since 0.1
|
53
|
+
|
54
|
+
class Base
|
55
|
+
# @attr_reader
|
56
|
+
# @return [String] The currency which was the base for the rates
|
57
|
+
attr_reader :base
|
58
|
+
|
59
|
+
# @attr_reader
|
60
|
+
# @return [Integer] A unix timestamp for the rates, delivered by the API
|
61
|
+
attr_reader :timestamp
|
62
|
+
|
63
|
+
# @attr_reader
|
64
|
+
# @return [Hash] A Hash which delivers the exchange rate of every available currency to the base currency
|
65
|
+
attr_reader :rates
|
66
|
+
|
67
|
+
|
68
|
+
# Delivers an exchange rate from one currency to another with the option of getting a historical exchange rate. This rate
|
69
|
+
# has to be multiplied with the amount of the currency which you define in from
|
70
|
+
# @param [String, Symbol] from The currency which should be converted
|
71
|
+
# @param [String, Symbol] to The currency which the should be converted to
|
72
|
+
# @param [Hash] opts The options to throw at the rate
|
73
|
+
# @option opts [Time] :at Define a Time here to get a historical rate
|
74
|
+
# @return [Float] The exchange rate for those two currencies
|
75
|
+
# @example Get the exchange rate for a conversion from USD to EUR at March 23 2009
|
76
|
+
# Exchange::ExternalAPI::Base.new.rate(:usd, :eur, :at => Time.gm(3,23,2009))
|
77
|
+
# #=> 1.232231231
|
78
|
+
def rate(from, to, opts={})
|
79
|
+
update(opts)
|
80
|
+
self.rates[to.to_s.upcase] / self.rates[from.to_s.upcase]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Converts an amount of one currency into another
|
84
|
+
# @param [Fixed, Float] amount The amount of the currency to be converted
|
85
|
+
# @param [String, Symbol] from The currency to be converted from
|
86
|
+
# @param [String, Symbol] to The currency which should be converted to
|
87
|
+
# @param [Hash] opts Options to throw at the conversion
|
88
|
+
# @option opts [Time] :at Define a Time here to convert at a historical rate
|
89
|
+
# @return [Float] The amount in the currency converted to, rounded to two decimals
|
90
|
+
# @example Convert 23 EUR to CHF at the rate of December 1 2011
|
91
|
+
# Exchange::ExternalAPI::Base.new.convert(23, :eur, :chf, :at => Time.gm(12,1,2011))
|
92
|
+
# #=> 30.12
|
93
|
+
def convert(amount, from, to, opts={})
|
94
|
+
(amount.to_f * rate(from, to, opts) * 100).round.to_f / 100
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
# A helper function to assure a value is an instance of time
|
100
|
+
# @param [Time, String, NilClass] The value to be asserted
|
101
|
+
# @param [Hash] opts Options for assertion
|
102
|
+
# @option opts [Symbol] :default If the argument is nil, you can define :default as :now to be delivered with Time.now instead of nil
|
103
|
+
|
104
|
+
def assure_time(arg=nil, opts={})
|
105
|
+
if arg
|
106
|
+
arg.kind_of?(Time) ? arg : Time.gm(*arg.split('-'))
|
107
|
+
elsif opts[:default] == :now
|
108
|
+
Time.now
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Exchange
|
2
|
+
module ExternalAPI
|
3
|
+
|
4
|
+
# A class to handle API calls in a standardized way for all APIs
|
5
|
+
# @author Beat Richartz
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
|
9
|
+
class Call
|
10
|
+
|
11
|
+
# Initialization of the Call class is the call itself. This means that every instance of the class will only exist during the call
|
12
|
+
# @param [String] url The url of the API to call
|
13
|
+
# @param [Hash] options The options of the API call
|
14
|
+
# @option options [Time] :at The time of the historical exchange rate file to get
|
15
|
+
# @option options [Integer] :retries The number of retries if the API Call should fail with a HTTP Error
|
16
|
+
# @option options [Array] :retry_with an Array of urls to retry the call with (if the API does not have a file for the specified date). These values will be shifted until a call succeeds or the number of maximum retries is reached.
|
17
|
+
# @option options [Symbol] :format The format to return / yield the API call result in, defaults to :json
|
18
|
+
# @yield [Nokogiri::XML, Hash] The result of the API call, either nokogiri parsed XML or a hash loaded from JSON
|
19
|
+
# @return [Nokogiri::XML, Hash] Returns the result of the API call if no block is given, either nokogiri parsed XML or a hash loaded from JSON
|
20
|
+
# @example Call an API an yield the result
|
21
|
+
# Exchange::ExternalAPI::Call.new('http://yourapiurl.com', :format => :xml) do |result|
|
22
|
+
# # Do something with the result here, for example
|
23
|
+
# rates = {}
|
24
|
+
# result.css('rates').each do |rate|
|
25
|
+
# rates.merge! rate.css('currency').children.to_s => rate.css('rate').children.to_s.to_f
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# @example Call the API and do something with the result
|
29
|
+
# result = Exchange::ExternalAPI::Call.new('http://yourapiurl.com', :format => :xml)
|
30
|
+
# # Do something with that result
|
31
|
+
|
32
|
+
def initialize url, options={}, &block
|
33
|
+
if Exchange::Configuration.cache
|
34
|
+
result = Exchange::Configuration.cache_class.cached(Exchange::Configuration.api_class, :at => options[:at]) do
|
35
|
+
load_url(url, options[:retries] || 5, options[:retry_with])
|
36
|
+
end
|
37
|
+
else
|
38
|
+
result = load_url(url, options[:retries] || 5, options[:retry_with])
|
39
|
+
end
|
40
|
+
|
41
|
+
parsed = options[:format] == :xml ? Nokogiri.parse(result) : JSON.load(result)
|
42
|
+
|
43
|
+
return parsed unless block_given?
|
44
|
+
|
45
|
+
yield parsed
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# A helper function to load the API URL with
|
51
|
+
# @param [String] url The url to be loaded
|
52
|
+
# @param [Integer] retries The number of retries to do if the API Call should fail with a HTTP Error
|
53
|
+
# @param [Array] retry_with An array of urls to retry the API call with if the call to the original URL should fail. These values will be shifted until a call succeeds or the number of maximum retries is reached
|
54
|
+
|
55
|
+
def load_url(url, retries, retry_with)
|
56
|
+
begin
|
57
|
+
result = URI.parse(url).open.read
|
58
|
+
rescue SocketError
|
59
|
+
raise APIError.new("Calling API #{url} produced a socket error")
|
60
|
+
rescue OpenURI::HTTPError
|
61
|
+
if retries > 0
|
62
|
+
retries -= 1
|
63
|
+
url = retry_with.shift if retry_with && !retry_with.empty?
|
64
|
+
retry
|
65
|
+
else
|
66
|
+
raise APIError.new("API #{url} was not reachable")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
# The Api Error to throw when an API Call fails
|
74
|
+
APIError = Class.new(StandardError)
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Exchange
|
2
|
+
module ExternalAPI
|
3
|
+
# The Currency Bot API class, handling communication with the Open Source Currency bot API
|
4
|
+
# You can find further information on the currency bot API here: http://currencybot.github.com/
|
5
|
+
# @author Beat Richartz
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
|
9
|
+
class CurrencyBot < Base
|
10
|
+
# The base of the Currency Bot exchange API
|
11
|
+
API_URL = 'https://raw.github.com/currencybot/open-exchange-rates/master'
|
12
|
+
# The currencies the Currency Bot API can convert
|
13
|
+
CURRENCIES = %W(xcd usd sar rub nio lak nok omr amd cdf kpw cny kes zwd khr pln mvr gtq clp inr bzd myr hkd sek cop dkk byr lyd ron dzd bif ars gip bob xof std ngn pgk aed mwk cup gmd zwl tzs cve btn xaf ugx syp mad mnt lsl top shp rsd htg mga mzn lvl fkp bwp hnl eur egp chf ils pyg lbp ang kzt wst gyd thb npr kmf irr uyu srd jpy brl szl mop bmd xpf etb jod idr mdl mro yer bam awg nzd pen vef try sll aoa tnd tjs sgd scr lkr mxn ltl huf djf bsd gnf isk vuv sdg gel fjd dop xdr mur php mmk krw lrd bbd zmk zar vnd uah tmt iqd bgn gbp kgs ttd hrk rwf clf bhd uzs twd crc aud mkd pkr afn nad bdt azn czk sos iep pab qar svc sbd all jmd bnd cad kwd ghs)
|
14
|
+
|
15
|
+
# Updates the rates by getting the information from Currency Bot for today or a defined historical date
|
16
|
+
# The call gets cached for a maximum of 24 hours.
|
17
|
+
# @param [Hash] opts Options to define for the API Call
|
18
|
+
# @option opts [Time, String] :at a historical date to get the exchange rates for
|
19
|
+
# @example Update the currency bot API to use the file of March 2, 2010
|
20
|
+
# Exchange::ExternalAPI::CurrencyBot.new.update(:at => Time.gm(3,2,2010))
|
21
|
+
|
22
|
+
def update(opts={})
|
23
|
+
time = assure_time(opts[:at])
|
24
|
+
|
25
|
+
Call.new(api_url(time), :at => time) do |result|
|
26
|
+
@base = result['base']
|
27
|
+
@rates = result['rates']
|
28
|
+
@timestamp = result['timestamp'].to_i
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# A helper function to build an api url for either a specific time or the latest available rates
|
35
|
+
# @param [Time] time The time to build the api url for
|
36
|
+
# @return [String] an api url for the time specified
|
37
|
+
# @since 0.1
|
38
|
+
# @version 0.2.6
|
39
|
+
|
40
|
+
def api_url(time=nil)
|
41
|
+
today = Time.now
|
42
|
+
[API_URL, time && (time.year != today.year || time.yday != today.yday) ? "historical/#{time.strftime("%Y-%m-%d")}.json" : 'latest.json'].join('/')
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Exchange
|
2
|
+
module ExternalAPI
|
3
|
+
|
4
|
+
# The XavierMedia API class, handling communication with the Xavier Media Currency API
|
5
|
+
# You can find further information on the Xaviermedia API here: http://www.xavierforum.com/viewtopic.php?f=5&t=10979&sid=671a685edbfa5dbec219fbc6793d5057
|
6
|
+
# @author Beat Richartz
|
7
|
+
# @version 0.1
|
8
|
+
# @since 0.1
|
9
|
+
|
10
|
+
class XavierMedia < Base
|
11
|
+
# The base of the Xaviermedia API URL
|
12
|
+
API_URL = "http://api.finance.xaviermedia.com/api"
|
13
|
+
# The currencies the Xaviermedia API URL can handle
|
14
|
+
CURRENCIES = %W(eur usd jpy gbp cyp czk dkk eek huf ltl mtl pln sek sit skk chf isk nok bgn hrk rol ron rub trl aud cad cny hkd idr krw myr nzd php sgd thb zar)
|
15
|
+
|
16
|
+
# Updates the rates by getting the information from Xaviermedia API for today or a defined historical date
|
17
|
+
# The call gets cached for a maximum of 24 hours.
|
18
|
+
# @param [Hash] opts Options to define for the API Call
|
19
|
+
# @option opts [Time, String] :at a historical date to get the exchange rates for
|
20
|
+
# @example Update the currency bot API to use the file of March 2, 2010
|
21
|
+
# Exchange::ExternalAPI::XavierMedia.new.update(:at => Time.gm(3,2,2010))
|
22
|
+
|
23
|
+
def update(opts={})
|
24
|
+
time = assure_time(opts[:at], :default => :now)
|
25
|
+
api_url = api_url(time)
|
26
|
+
retry_urls = [api_url(time - 86400), api_url(time - 172800), api_url(time - 259200)]
|
27
|
+
|
28
|
+
Call.new(api_url, :format => :xml, :at => time, :retry_with => retry_urls) do |result|
|
29
|
+
@base = result.css('basecurrency').children[0].to_s
|
30
|
+
@rates = Hash[*result.css('fx currency_code').children.map(&:to_s).zip(result.css('fx rate').children.map{|c| c.to_s.to_f }).flatten]
|
31
|
+
@timestamp = Time.gm(*result.css('fx_date').children[0].to_s.split('-')).to_i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# A helper function which build a valid api url for the specified time
|
38
|
+
# @param [Time] time The exchange rate date for which the URL should be built
|
39
|
+
# @return [String] An Xaviermedia API URL for the specified time
|
40
|
+
|
41
|
+
def api_url(time)
|
42
|
+
[API_URL, "#{time.strftime("%Y/%m/%d")}.xml"].join('/')
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Exchange::Conversability" do
|
4
|
+
before(:all) do
|
5
|
+
Exchange::Configuration.cache = false
|
6
|
+
end
|
7
|
+
after(:all) do
|
8
|
+
Exchange::Configuration.cache = :memcached
|
9
|
+
end
|
10
|
+
context "with a fixnum" do
|
11
|
+
it "should allow to convert to a currency" do
|
12
|
+
3.eur.should be_kind_of Exchange::Currency
|
13
|
+
3.eur.value.should == 3
|
14
|
+
end
|
15
|
+
it "should allow to convert to a curreny with a negative number" do
|
16
|
+
-3.eur.should be_kind_of Exchange::Currency
|
17
|
+
-3.eur.value.should == -3
|
18
|
+
end
|
19
|
+
it "should allow to do full conversions" do
|
20
|
+
mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
|
21
|
+
3.eur.to_chf.should be_kind_of Exchange::Currency
|
22
|
+
3.eur.to_chf.value.should == 3.62
|
23
|
+
3.eur.to_chf.currency.should == 'chf'
|
24
|
+
end
|
25
|
+
it "should allow to do full conversions with negative numbers" do
|
26
|
+
mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
|
27
|
+
-3.eur.to_chf.should be_kind_of Exchange::Currency
|
28
|
+
-3.eur.to_chf.value.should == -3.62
|
29
|
+
-3.eur.to_chf.currency.should == 'chf'
|
30
|
+
end
|
31
|
+
it "should allow to define a historic time in which the currency should be interpreted" do
|
32
|
+
3.chf(:at => Time.gm(2010,1,1)).time.yday.should == 1
|
33
|
+
3.chf(:at => Time.gm(2010,1,1)).time.year.should == 2010
|
34
|
+
3.chf(:at => '2010-01-01').time.year.should == 2010
|
35
|
+
end
|
36
|
+
end
|
37
|
+
context "with a float" do
|
38
|
+
it "should allow to convert to a currency" do
|
39
|
+
3.25.eur.should be_kind_of Exchange::Currency
|
40
|
+
3.25.eur.value.should == 3.25
|
41
|
+
end
|
42
|
+
it "should allow to convert to a curreny with a negative number" do
|
43
|
+
-3.25.eur.should be_kind_of Exchange::Currency
|
44
|
+
-3.25.eur.value.should == -3.25
|
45
|
+
end
|
46
|
+
it "should allow to do full conversions" do
|
47
|
+
mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
|
48
|
+
3.25.eur.to_chf.should be_kind_of Exchange::Currency
|
49
|
+
3.25.eur.to_chf.value.should == 3.92
|
50
|
+
3.25.eur.to_chf.currency.should == 'chf'
|
51
|
+
end
|
52
|
+
it "should allow to do full conversions with negative numbers" do
|
53
|
+
mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
|
54
|
+
-3.25.eur.to_chf.should be_kind_of Exchange::Currency
|
55
|
+
-3.25.eur.to_chf.value.should == -3.92
|
56
|
+
-3.25.eur.to_chf.currency.should == 'chf'
|
57
|
+
end
|
58
|
+
it "should allow to define a historic time in which the currency should be interpreted" do
|
59
|
+
3.25.chf(:at => Time.gm(2010,1,1)).time.yday.should == 1
|
60
|
+
3.25.chf(:at => Time.gm(2010,1,1)).time.year.should == 2010
|
61
|
+
3.25.chf(:at => '2010-01-01').time.year.should == 2010
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Exchange::Cache::Base" do
|
4
|
+
subject { Exchange::Cache::Base }
|
5
|
+
describe "key generation" do
|
6
|
+
before(:each) do
|
7
|
+
time = Time.gm 2012, 03, 01, 23, 23, 23
|
8
|
+
Time.stub! :now => time
|
9
|
+
end
|
10
|
+
context "with a daily cache" do
|
11
|
+
it "should build a timestamped key with the class given, the yearday and the year" do
|
12
|
+
Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::XavierMedia).should == 'Exchange_ExternalAPI_XavierMedia_2012_61'
|
13
|
+
Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::CurrencyBot).should == 'Exchange_ExternalAPI_CurrencyBot_2012_61'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
context "with an hourly cache" do
|
17
|
+
before(:each) do
|
18
|
+
Exchange::Configuration.update = :hourly
|
19
|
+
end
|
20
|
+
after(:each) do
|
21
|
+
Exchange::Configuration.update = :daily
|
22
|
+
end
|
23
|
+
it "should build a timestamped key with the class given, the yearday, the year and the hour" do
|
24
|
+
Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::XavierMedia).should == 'Exchange_ExternalAPI_XavierMedia_2012_61_23'
|
25
|
+
Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::CurrencyBot).should == 'Exchange_ExternalAPI_CurrencyBot_2012_61_23'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Exchange::Cache::Memcached" do
|
4
|
+
subject { Exchange::Cache::Memcached }
|
5
|
+
before(:each) do
|
6
|
+
Exchange::Configuration.define do |c|
|
7
|
+
c.cache_host = 'HOST'
|
8
|
+
c.cache_port = 'PORT'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
after(:each) do
|
12
|
+
Exchange::Configuration.define do |c|
|
13
|
+
c.cache_host = 'localhost'
|
14
|
+
c.cache_port = 11211
|
15
|
+
end
|
16
|
+
end
|
17
|
+
describe "client" do
|
18
|
+
let(:client) { mock('memcached') }
|
19
|
+
after(:each) do
|
20
|
+
subject.send(:remove_class_variable, "@@client")
|
21
|
+
end
|
22
|
+
it "should set up a client on the specified host and port for the cache" do
|
23
|
+
::Memcached.should_receive(:new).with("HOST:PORT").and_return(client)
|
24
|
+
subject.client.should == client
|
25
|
+
end
|
26
|
+
end
|
27
|
+
describe "cached" do
|
28
|
+
context "when a cached result exists" do
|
29
|
+
let(:client) { mock('memcached') }
|
30
|
+
before(:each) do
|
31
|
+
subject.should_receive(:key).with('API_CLASS', nil).and_return('KEY')
|
32
|
+
::Memcached.should_receive(:new).with("HOST:PORT").and_return(client)
|
33
|
+
client.should_receive(:get).with('KEY').and_return "{\"RESULT\":\"YAY\"}"
|
34
|
+
end
|
35
|
+
after(:each) do
|
36
|
+
subject.send(:remove_class_variable, "@@client")
|
37
|
+
end
|
38
|
+
it "should return the JSON loaded result" do
|
39
|
+
subject.cached('API_CLASS') { 'something' }.should == {'RESULT' => 'YAY'}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
context "when no cached result exists" do
|
43
|
+
let(:client) { mock('memcached') }
|
44
|
+
before(:each) do
|
45
|
+
subject.should_receive(:key).with('API_CLASS', nil).twice.and_return('KEY')
|
46
|
+
::Memcached.should_receive(:new).with("HOST:PORT").and_return(client)
|
47
|
+
client.should_receive(:get).with('KEY').and_raise(::Memcached::NotFound)
|
48
|
+
end
|
49
|
+
after(:each) do
|
50
|
+
subject.send(:remove_class_variable, "@@client")
|
51
|
+
end
|
52
|
+
context "with daily cache" do
|
53
|
+
it "should call the block and set and return the result" do
|
54
|
+
client.should_receive(:set).with('KEY', "{\"RESULT\":\"YAY\"}", 86400).once
|
55
|
+
subject.cached('API_CLASS') { {'RESULT' => 'YAY'} }.should == {'RESULT' => 'YAY'}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
context "with hourly cache" do
|
59
|
+
before(:each) do
|
60
|
+
Exchange::Configuration.update = :hourly
|
61
|
+
end
|
62
|
+
after(:each) do
|
63
|
+
Exchange::Configuration.update = :daily
|
64
|
+
end
|
65
|
+
it "should call the block and set and return the result" do
|
66
|
+
client.should_receive(:set).with('KEY', "{\"RESULT\":\"YAY\"}", 3600).once
|
67
|
+
subject.cached('API_CLASS') { {'RESULT' => 'YAY'} }.should == {'RESULT' => 'YAY'}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|