exchange 0.5.1 → 0.6.0
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 +17 -8
- data/Gemfile.lock +12 -6
- data/LICENSE.txt +1 -1
- data/README.rdoc +15 -12
- data/VERSION +1 -1
- data/exchange.gemspec +13 -23
- data/lib/core_extensions/conversability.rb +5 -4
- data/lib/exchange.rb +6 -11
- data/lib/exchange/base.rb +19 -0
- data/lib/exchange/cache/base.rb +57 -32
- data/lib/exchange/cache/file.rb +41 -45
- data/lib/exchange/cache/memcached.rb +28 -29
- data/lib/exchange/cache/no_cache.rb +4 -24
- data/lib/exchange/cache/rails.rb +26 -26
- data/lib/exchange/cache/redis.rb +30 -33
- data/lib/exchange/configuration.rb +167 -73
- data/lib/exchange/currency.rb +61 -30
- data/lib/exchange/external_api.rb +2 -0
- data/lib/exchange/external_api/base.rb +11 -5
- data/lib/exchange/external_api/call.rb +10 -7
- data/lib/exchange/external_api/currency_bot.rb +9 -5
- data/lib/exchange/external_api/ecb.rb +29 -13
- data/lib/exchange/external_api/json.rb +22 -0
- data/lib/exchange/external_api/xavier_media.rb +6 -5
- data/lib/exchange/external_api/xml.rb +22 -0
- data/lib/exchange/gem_loader.rb +35 -0
- data/lib/exchange/helper.rb +22 -15
- data/lib/exchange/iso_4217.rb +55 -44
- data/spec/core_extensions/conversability_spec.rb +2 -2
- data/spec/exchange/cache/base_spec.rb +7 -7
- data/spec/exchange/cache/file_spec.rb +19 -18
- data/spec/exchange/cache/memcached_spec.rb +29 -26
- data/spec/exchange/cache/no_cache_spec.rb +12 -6
- data/spec/exchange/cache/rails_spec.rb +13 -9
- data/spec/exchange/cache/redis_spec.rb +24 -24
- data/spec/exchange/configuration_spec.rb +38 -30
- data/spec/exchange/currency_spec.rb +33 -21
- data/spec/exchange/external_api/base_spec.rb +3 -1
- data/spec/exchange/external_api/call_spec.rb +5 -1
- data/spec/exchange/external_api/currency_bot_spec.rb +5 -1
- data/spec/exchange/external_api/ecb_spec.rb +9 -5
- data/spec/exchange/external_api/xavier_media_spec.rb +5 -1
- data/spec/exchange/gem_loader_spec.rb +29 -0
- data/spec/spec_helper.rb +1 -1
- metadata +12 -87
@@ -0,0 +1,22 @@
|
|
1
|
+
module Exchange
|
2
|
+
module ExternalAPI
|
3
|
+
|
4
|
+
# The json base class takes care of JSON apis. It assumes you would want to use json as a parser and preloads the gem
|
5
|
+
# Also, this may serve as a base for some operations which might be common to the json apis
|
6
|
+
# @author Beat Richartz
|
7
|
+
# @version 0.6
|
8
|
+
# @since 0.6
|
9
|
+
#
|
10
|
+
class JSON < Base
|
11
|
+
|
12
|
+
# Initializer, essentially takes the arguments passed to initialization, loads the json gem on the way
|
13
|
+
# and passes the arguments to the api base
|
14
|
+
#
|
15
|
+
def initialize *args
|
16
|
+
Exchange::GemLoader.new('json').try_load unless defined?(::JSON)
|
17
|
+
super *args
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -6,8 +6,9 @@ module Exchange
|
|
6
6
|
# @author Beat Richartz
|
7
7
|
# @version 0.1
|
8
8
|
# @since 0.1
|
9
|
-
|
10
|
-
class XavierMedia <
|
9
|
+
#
|
10
|
+
class XavierMedia < XML
|
11
|
+
|
11
12
|
# The base of the Xaviermedia API URL
|
12
13
|
API_URL = "http://api.finance.xaviermedia.com/api"
|
13
14
|
# The currencies the Xaviermedia API URL can handle
|
@@ -20,11 +21,11 @@ module Exchange
|
|
20
21
|
# @option opts [Time, String] :at a historical date to get the exchange rates for
|
21
22
|
# @example Update the currency bot API to use the file of March 2, 2010
|
22
23
|
# Exchange::ExternalAPI::XavierMedia.new.update(:at => Time.gm(3,2,2010))
|
23
|
-
|
24
|
+
#
|
24
25
|
def update(opts={})
|
25
26
|
time = Exchange::Helper.assure_time(opts[:at], :default => :now)
|
26
27
|
api_url = api_url(time)
|
27
|
-
retry_urls = Exchange
|
28
|
+
retry_urls = Exchange.configuration.api.retries.times.map{ |i| api_url(time - 86400 * (i+1)) }
|
28
29
|
|
29
30
|
Call.new(api_url, :format => :xml, :at => time, :retry_with => retry_urls) do |result|
|
30
31
|
@base = result.css('basecurrency').children[0].to_s
|
@@ -38,7 +39,7 @@ module Exchange
|
|
38
39
|
# A helper function which build a valid api url for the specified time
|
39
40
|
# @param [Time] time The exchange rate date for which the URL should be built
|
40
41
|
# @return [String] An Xaviermedia API URL for the specified time
|
41
|
-
|
42
|
+
#
|
42
43
|
def api_url(time)
|
43
44
|
[API_URL, "#{time.strftime("%Y/%m/%d")}.xml"].join('/')
|
44
45
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Exchange
|
2
|
+
module ExternalAPI
|
3
|
+
|
4
|
+
# The xml base class takes care of XML apis. It assumes you would want to use nokogiri as a parser and preloads the gem
|
5
|
+
# Also, this may serve as a base for some operations which might be common to the xml apis
|
6
|
+
# @author Beat Richartz
|
7
|
+
# @version 0.6
|
8
|
+
# @since 0.6
|
9
|
+
#
|
10
|
+
class XML < Base
|
11
|
+
|
12
|
+
# Initializer, essentially takes the arguments passed to initialization, loads nokogiri on the way
|
13
|
+
# and passes the arguments to the api base
|
14
|
+
#
|
15
|
+
def initialize *args
|
16
|
+
Exchange::GemLoader.new('nokogiri').try_load unless defined?(Nokogiri)
|
17
|
+
super *args
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Exchange
|
2
|
+
|
3
|
+
# The gem loader takes care of loading gems without adding too many unnecessary dependencies to the gem
|
4
|
+
# @author Beat Richartz
|
5
|
+
# @version 0.6
|
6
|
+
# @since 0.6
|
7
|
+
#
|
8
|
+
class GemLoader
|
9
|
+
|
10
|
+
# The error that gets thrown if a needed gem is not available or loadable
|
11
|
+
#
|
12
|
+
GemNotFoundError = Class.new LoadError
|
13
|
+
|
14
|
+
# initialize the loader with a gem name.
|
15
|
+
# @param [string] gem The gem to require
|
16
|
+
# @return [Exchange::Gemloader] an instance of the gemloader
|
17
|
+
# @example initialize a loader for the nokogiri gem
|
18
|
+
# Exchange::GemLoader.new('nokogiri')
|
19
|
+
#
|
20
|
+
def initialize gem
|
21
|
+
@gem = gem
|
22
|
+
end
|
23
|
+
|
24
|
+
# Try to require the gem specified on initialization.
|
25
|
+
# @raise [GemNotFoundError] an error indicating that the gem could not be found rather than a load error
|
26
|
+
# @example Try to load the JSON gem
|
27
|
+
# Exchange::GemLoader.new('json').try_load
|
28
|
+
#
|
29
|
+
def try_load
|
30
|
+
require @gem
|
31
|
+
rescue LoadError => e
|
32
|
+
raise GemNotFoundError.new("You specified #{@gem} to be used with Exchange, yet it is not loadable. Please install #{@gem} to be able to use it with Exchange")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/exchange/helper.rb
CHANGED
@@ -1,27 +1,34 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'forwardable'
|
3
|
+
|
1
4
|
module Exchange
|
5
|
+
|
2
6
|
# Helper Functions that get used throughout the gem can be placed here
|
3
7
|
# @author Beat Richartz
|
4
|
-
# @version 0.
|
8
|
+
# @version 0.6
|
5
9
|
# @since 0.3
|
6
|
-
|
10
|
+
#
|
7
11
|
class Helper
|
8
|
-
|
12
|
+
include Singleton
|
13
|
+
extend SingleForwardable
|
9
14
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
15
|
+
# A helper function to assure a value is an instance of time
|
16
|
+
# @param [Time, String, NilClass] The value to be asserted
|
17
|
+
# @param [Hash] opts Options for assertion
|
18
|
+
# @option opts [Symbol] :default a method that can be sent to Time if the argument is nil (:now for example)
|
19
|
+
#
|
20
|
+
def assure_time(arg=nil, opts={})
|
21
|
+
if arg
|
22
|
+
arg.kind_of?(Time) ? arg : Time.gm(*arg.split('-'))
|
23
|
+
elsif opts[:default]
|
24
|
+
Time.send(opts[:default])
|
21
25
|
end
|
22
|
-
|
23
26
|
end
|
24
27
|
|
28
|
+
# Forwards the assure_time method to the instance using singleforwardable
|
29
|
+
#
|
30
|
+
def_delegator :instance, :assure_time
|
31
|
+
|
25
32
|
end
|
26
33
|
|
27
34
|
end
|
data/lib/exchange/iso_4217.rb
CHANGED
@@ -1,67 +1,74 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'forwardable'
|
3
|
+
|
1
4
|
module Exchange
|
2
5
|
|
3
6
|
# This class handles everything that has to do with certified formatting of the different currencies. The standard is stored in
|
4
7
|
# the iso4217 YAML file.
|
5
|
-
# @version 0.
|
8
|
+
# @version 0.6
|
6
9
|
# @since 0.3
|
7
10
|
# @author Beat Richartz
|
8
|
-
|
11
|
+
#
|
9
12
|
class ISO4217
|
13
|
+
include Singleton
|
14
|
+
extend SingleForwardable
|
15
|
+
|
10
16
|
class << self
|
11
17
|
|
12
|
-
# The ISO 4217 that have to be loaded. Nothing much to say here. Just use this method to get to the definitions
|
13
|
-
# They are static, so they can be stored in a class variable without many worries
|
14
|
-
# @return [Hash] The iso427 Definitions with the currency code as keys
|
15
|
-
|
16
|
-
def definitions
|
17
|
-
@@definitions ||= YAML.load_file(File.join(EXCHANGE_GEM_ROOT_PATH, 'iso4217.yml'))
|
18
|
-
end
|
19
|
-
|
20
|
-
# Use this to instantiate a currency amount. For one, it is important that we use BigDecimal here so nothing gets lost because
|
21
|
-
# of floating point errors. For the other, This allows us to set the precision exactly according to the iso definition
|
22
|
-
# @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to instantiate
|
23
|
-
# @param [String, Symbol] currency The currency you want to instantiate the money in
|
24
|
-
# @return [BigDecimal] The instantiated currency
|
25
|
-
# @example instantiate a currency from a string
|
26
|
-
# Exchange::ISO4217.instantiate("4523", "usd") #=> #<Bigdecimal 4523.00>
|
27
|
-
|
28
|
-
def instantiate(amount, currency)
|
29
|
-
BigDecimal.new(amount.to_s, definitions[currency.to_s.upcase]['minor_unit'])
|
30
|
-
end
|
31
|
-
|
32
|
-
# Converts the currency to a string in ISO 4217 standardized format, either with or without the currency. This leaves you
|
33
|
-
# with no worries how to display the currency.
|
34
|
-
# @param [BigDecimal, Fixed, Float] amount The amount of currency you want to stringify
|
35
|
-
# @param [String, Symbol] currency The currency you want to stringify
|
36
|
-
# @param [Hash] opts The options for formatting
|
37
|
-
# @option opts [Boolean] :amount_only Whether you want to have the currency in the string or not
|
38
|
-
# @return [String] The formatted string
|
39
|
-
# @example Convert a currency to a string
|
40
|
-
# Exchange::ISO4217.stringify(49.567, :usd) #=> "USD 49.57"
|
41
|
-
# @example Convert a currency without minor to a string
|
42
|
-
# Exchange::ISO4217.stringif(45, :jpy) #=> "JPY 45"
|
43
|
-
# @example Convert a currency with a three decimal minor to a string
|
44
|
-
# Exchange::ISO4217.stringif(34.34, :omr) #=> "OMR 34.340"
|
45
|
-
# @example Convert a currency to a string without the currency
|
46
|
-
# Exchange::ISO4217.stringif(34.34, :omr, :amount_only => true) #=> "34.340"
|
47
|
-
|
48
|
-
def stringify(amount, currency, opts={})
|
49
|
-
format = "%.#{definitions[currency.to_s.upcase]['minor_unit']}f"
|
50
|
-
"#{currency.to_s.upcase + ' ' unless opts[:amount_only]}#{format % amount}"
|
51
|
-
end
|
52
|
-
|
53
18
|
private
|
54
19
|
# @private
|
55
20
|
# @macro [attach] install_operations
|
56
21
|
|
57
22
|
def install_operation op
|
58
23
|
self.class_eval <<-EOV
|
59
|
-
def
|
24
|
+
def #{op}(amount, currency, precision=nil)
|
60
25
|
minor = definitions[currency.to_s.upcase]['minor_unit']
|
61
26
|
(amount.is_a?(BigDecimal) ? amount : BigDecimal.new(amount.to_s, minor)).#{op}(precision || minor)
|
62
27
|
end
|
63
28
|
EOV
|
64
29
|
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# The ISO 4217 that have to be loaded. Nothing much to say here. Just use this method to get to the definitions
|
34
|
+
# They are static, so they can be stored in a class variable without many worries
|
35
|
+
# @return [Hash] The iso427 Definitions with the currency code as keys
|
36
|
+
#
|
37
|
+
def definitions
|
38
|
+
@@definitions ||= YAML.load_file(File.join(ROOT_PATH, 'iso4217.yml'))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Use this to instantiate a currency amount. For one, it is important that we use BigDecimal here so nothing gets lost because
|
42
|
+
# of floating point errors. For the other, This allows us to set the precision exactly according to the iso definition
|
43
|
+
# @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to instantiate
|
44
|
+
# @param [String, Symbol] currency The currency you want to instantiate the money in
|
45
|
+
# @return [BigDecimal] The instantiated currency
|
46
|
+
# @example instantiate a currency from a string
|
47
|
+
# Exchange::ISO4217.instantiate("4523", "usd") #=> #<Bigdecimal 4523.00>
|
48
|
+
#
|
49
|
+
def instantiate(amount, currency)
|
50
|
+
BigDecimal.new(amount.to_s, definitions[currency.to_s.upcase]['minor_unit'])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Converts the currency to a string in ISO 4217 standardized format, either with or without the currency. This leaves you
|
54
|
+
# with no worries how to display the currency.
|
55
|
+
# @param [BigDecimal, Fixed, Float] amount The amount of currency you want to stringify
|
56
|
+
# @param [String, Symbol] currency The currency you want to stringify
|
57
|
+
# @param [Hash] opts The options for formatting
|
58
|
+
# @option opts [Boolean] :amount_only Whether you want to have the currency in the string or not
|
59
|
+
# @return [String] The formatted string
|
60
|
+
# @example Convert a currency to a string
|
61
|
+
# Exchange::ISO4217.stringify(49.567, :usd) #=> "USD 49.57"
|
62
|
+
# @example Convert a currency without minor to a string
|
63
|
+
# Exchange::ISO4217.stringif(45, :jpy) #=> "JPY 45"
|
64
|
+
# @example Convert a currency with a three decimal minor to a string
|
65
|
+
# Exchange::ISO4217.stringif(34.34, :omr) #=> "OMR 34.340"
|
66
|
+
# @example Convert a currency to a string without the currency
|
67
|
+
# Exchange::ISO4217.stringif(34.34, :omr, :amount_only => true) #=> "34.340"
|
68
|
+
#
|
69
|
+
def stringify(amount, currency, opts={})
|
70
|
+
format = "%.#{definitions[currency.to_s.upcase]['minor_unit']}f"
|
71
|
+
"#{currency.to_s.upcase + ' ' unless opts[:amount_only]}#{format % amount}"
|
65
72
|
end
|
66
73
|
|
67
74
|
# Use this to round a currency amount. This allows us to round exactly to the number of minors the currency has in the
|
@@ -91,5 +98,9 @@ module Exchange
|
|
91
98
|
|
92
99
|
install_operation :floor
|
93
100
|
|
101
|
+
# Forwards the assure_time method to the instance using singleforwardable
|
102
|
+
#
|
103
|
+
def_delegators :instance, :definitions, :instantiate, :stringify, :round, :ceil, :floor
|
104
|
+
|
94
105
|
end
|
95
106
|
end
|
@@ -2,14 +2,14 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe "Exchange::Conversability" do
|
4
4
|
before(:all) do
|
5
|
-
Exchange::Configuration.cache =
|
5
|
+
Exchange.configuration = Exchange::Configuration.new { |c| c.cache = { :subclass => :no_cache } }
|
6
6
|
end
|
7
7
|
before(:each) do
|
8
8
|
@time = Time.gm(2012,8,27)
|
9
9
|
Time.stub! :now => @time
|
10
10
|
end
|
11
11
|
after(:all) do
|
12
|
-
Exchange::Configuration.cache = :memcached
|
12
|
+
Exchange::configuration = Exchange::Configuration.new { |c| c.cache = { :subclass => :memcached } }
|
13
13
|
end
|
14
14
|
it "should define all currencies on Fixnum, Float and BigDecimal" do
|
15
15
|
Exchange::ISO4217.definitions.keys.each do |c|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "Exchange::Cache::Base" do
|
4
|
-
subject { Exchange::Cache::Base }
|
4
|
+
subject { Exchange::Cache::Base.instance }
|
5
5
|
describe "key generation" do
|
6
6
|
before(:each) do
|
7
7
|
time = Time.gm 2012, 03, 01, 23, 23, 23
|
@@ -9,20 +9,20 @@ describe "Exchange::Cache::Base" do
|
|
9
9
|
end
|
10
10
|
context "with a daily cache" do
|
11
11
|
it "should build a timestamped key with the class given, the yearday and the year" do
|
12
|
-
|
13
|
-
|
12
|
+
subject.send(:key, :xavier_media).should == 'exchange_xavier_media_2012_61'
|
13
|
+
subject.send(:key, :currency_bot).should == 'exchange_currency_bot_2012_61'
|
14
14
|
end
|
15
15
|
end
|
16
16
|
context "with an hourly cache" do
|
17
17
|
before(:each) do
|
18
|
-
Exchange
|
18
|
+
Exchange.configuration.cache.expire = :hourly
|
19
19
|
end
|
20
20
|
after(:each) do
|
21
|
-
Exchange
|
21
|
+
Exchange.configuration.cache.expire = :daily
|
22
22
|
end
|
23
23
|
it "should build a timestamped key with the class given, the yearday, the year and the hour" do
|
24
|
-
|
25
|
-
|
24
|
+
subject.send(:key, :xavier_media).should == 'exchange_xavier_media_2012_61_23'
|
25
|
+
subject.send(:key, :currency_bot).should == 'exchange_currency_bot_2012_61_23'
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -1,16 +1,21 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "Exchange::Cache::File" do
|
4
|
-
subject { Exchange::Cache::File }
|
4
|
+
subject { Exchange::Cache::File.instance }
|
5
5
|
before(:each) do
|
6
|
-
Exchange::Configuration.
|
7
|
-
c.cache =
|
8
|
-
|
6
|
+
Exchange.configuration = Exchange::Configuration.new { |c|
|
7
|
+
c.cache = {
|
8
|
+
:subclass => :file,
|
9
|
+
:path => 'STORE/PATH'
|
10
|
+
}
|
11
|
+
}
|
9
12
|
end
|
10
13
|
after(:each) do
|
11
|
-
Exchange::Configuration.
|
12
|
-
c.cache =
|
13
|
-
|
14
|
+
Exchange.configuration = Exchange::Configuration.new { |c|
|
15
|
+
c.cache = {
|
16
|
+
:subclass => :memcached
|
17
|
+
}
|
18
|
+
}
|
14
19
|
end
|
15
20
|
describe "cached" do
|
16
21
|
it "should raise an error if no block was given" do
|
@@ -20,9 +25,8 @@ describe "Exchange::Cache::File" do
|
|
20
25
|
context "with a daily cache" do
|
21
26
|
before(:each) do
|
22
27
|
subject.should_receive(:key).with('API_CLASS', nil).and_return('KEY')
|
23
|
-
|
24
|
-
::File.should_receive(:
|
25
|
-
::File.should_receive(:read).with('STORE/KEY').and_return 'CONTENT'
|
28
|
+
::File.should_receive(:exists?).with('STORE/PATH/KEY').and_return(true)
|
29
|
+
::File.should_receive(:read).with('STORE/PATH/KEY').and_return 'CONTENT'
|
26
30
|
end
|
27
31
|
it "should return the file contents" do
|
28
32
|
subject.cached('API_CLASS') { 'something' }.should == 'CONTENT'
|
@@ -31,9 +35,8 @@ describe "Exchange::Cache::File" do
|
|
31
35
|
context "with an monthly cache" do
|
32
36
|
before(:each) do
|
33
37
|
subject.should_receive(:key).with('API_CLASS', an_instance_of(Symbol)).and_return('KEY')
|
34
|
-
|
35
|
-
::File.should_receive(:
|
36
|
-
::File.should_receive(:read).with('STORE/KEY').and_return 'CONTENT'
|
38
|
+
::File.should_receive(:exists?).with('STORE/PATH/KEY').and_return(true)
|
39
|
+
::File.should_receive(:read).with('STORE/PATH/KEY').and_return 'CONTENT'
|
37
40
|
end
|
38
41
|
it "should return the file contents" do
|
39
42
|
subject.cached('API_CLASS', :cache_period => :monthly) { 'something' }.should == 'CONTENT'
|
@@ -43,9 +46,8 @@ describe "Exchange::Cache::File" do
|
|
43
46
|
context "when no file is cached yet" do
|
44
47
|
before(:each) do
|
45
48
|
subject.should_receive(:key).with('API_CLASS', an_instance_of(Symbol)).at_most(3).times.and_return('KEY')
|
46
|
-
|
47
|
-
|
48
|
-
Dir.should_receive(:entries).with('STORE').once.and_return(%W(entry entry2))
|
49
|
+
::File.should_receive(:exists?).with('STORE/PATH/KEY').and_return(false)
|
50
|
+
Dir.should_receive(:entries).with('STORE/PATH').once.and_return(%W(entry entry2))
|
49
51
|
::File.should_receive(:delete).with(an_instance_of(String)).twice
|
50
52
|
FileUtils.should_receive(:mkdir_p).once
|
51
53
|
::File.should_receive(:open).once
|
@@ -57,8 +59,7 @@ describe "Exchange::Cache::File" do
|
|
57
59
|
context "when no result is returned" do
|
58
60
|
before(:each) do
|
59
61
|
subject.should_receive(:key).with('API_CLASS', an_instance_of(Symbol)).at_most(3).times.and_return('KEY')
|
60
|
-
|
61
|
-
::File.should_receive(:exists?).with('STORE/KEY').and_return(false)
|
62
|
+
::File.should_receive(:exists?).with('STORE/PATH/KEY').and_return(false)
|
62
63
|
FileUtils.should_receive(:mkdir_p).never
|
63
64
|
::File.should_receive(:open).never
|
64
65
|
end
|
@@ -1,41 +1,48 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe "Exchange::
|
4
|
-
subject { Exchange::Cache::Memcached }
|
3
|
+
describe "Exchange::CacheDalli::Client" do
|
4
|
+
subject { Exchange::Cache::Memcached.instance }
|
5
5
|
before(:each) do
|
6
|
-
Exchange::Configuration.
|
7
|
-
c.
|
8
|
-
|
9
|
-
|
6
|
+
Exchange.configuration = Exchange::Configuration.new { |c|
|
7
|
+
c.cache = {
|
8
|
+
:host => 'HOST',
|
9
|
+
:port => 'PORT'
|
10
|
+
}
|
11
|
+
}
|
10
12
|
end
|
11
13
|
after(:each) do
|
12
|
-
Exchange::Configuration.
|
13
|
-
c.
|
14
|
-
|
15
|
-
|
14
|
+
Exchange.configuration = Exchange::Configuration.new { |c|
|
15
|
+
c.cache = {
|
16
|
+
:host => 'localhost',
|
17
|
+
:port => 11211
|
18
|
+
}
|
19
|
+
}
|
16
20
|
end
|
17
21
|
describe "client" do
|
18
22
|
let(:client) { mock('memcached') }
|
23
|
+
before(:each) do
|
24
|
+
Exchange::GemLoader.new('dalli').try_load
|
25
|
+
end
|
19
26
|
after(:each) do
|
20
|
-
subject.
|
27
|
+
subject.instance_variable_set "@client", nil
|
21
28
|
end
|
22
29
|
it "should set up a client on the specified host and port for the cache" do
|
23
|
-
::
|
30
|
+
Dalli::Client.should_receive(:new).with("HOST:PORT").and_return(client)
|
24
31
|
subject.client.should == client
|
25
32
|
end
|
26
33
|
end
|
27
34
|
describe "cached" do
|
35
|
+
let(:client) { mock('memcached', :get => '') }
|
36
|
+
before(:each) do
|
37
|
+
Dalli::Client.should_receive(:new).with("HOST:PORT").and_return(client)
|
38
|
+
end
|
39
|
+
after(:each) do
|
40
|
+
subject.instance_variable_set "@client", nil
|
41
|
+
end
|
28
42
|
it "should raise an error if no block was given" do
|
29
43
|
lambda { subject.cached('API_CLASS') }.should raise_error(Exchange::Cache::CachingWithoutBlockError)
|
30
44
|
end
|
31
45
|
context "when a cached result exists" do
|
32
|
-
let(:client) { mock('memcached') }
|
33
|
-
before(:each) do
|
34
|
-
::Memcached.should_receive(:new).with("HOST:PORT").and_return(client)
|
35
|
-
end
|
36
|
-
after(:each) do
|
37
|
-
subject.send(:remove_class_variable, "@@client")
|
38
|
-
end
|
39
46
|
context "when loading json" do
|
40
47
|
before(:each) do
|
41
48
|
subject.should_receive(:key).with('API_CLASS', {}).and_return('KEY')
|
@@ -63,11 +70,7 @@ describe "Exchange::Cache::Memcached" do
|
|
63
70
|
let(:client) { mock('memcached') }
|
64
71
|
before(:each) do
|
65
72
|
subject.should_receive(:key).with('API_CLASS', {}).twice.and_return('KEY')
|
66
|
-
|
67
|
-
client.should_receive(:get).with('KEY').and_raise(::Memcached::NotFound)
|
68
|
-
end
|
69
|
-
after(:each) do
|
70
|
-
subject.send(:remove_class_variable, "@@client")
|
73
|
+
client.should_receive(:get).with('KEY').and_return(nil)
|
71
74
|
end
|
72
75
|
context "with daily cache" do
|
73
76
|
it "should call the block and set and return the result" do
|
@@ -77,10 +80,10 @@ describe "Exchange::Cache::Memcached" do
|
|
77
80
|
end
|
78
81
|
context "with hourly cache" do
|
79
82
|
before(:each) do
|
80
|
-
Exchange
|
83
|
+
Exchange.configuration.cache.expire = :hourly
|
81
84
|
end
|
82
85
|
after(:each) do
|
83
|
-
Exchange
|
86
|
+
Exchange.configuration.cache.expire = :daily
|
84
87
|
end
|
85
88
|
it "should call the block and set and return the result" do
|
86
89
|
client.should_receive(:set).with('KEY', "{\"RESULT\":\"YAY\"}", 3600).once
|