benschwarz-merb-cache 1.0.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.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README +224 -0
  3. data/Rakefile +17 -0
  4. data/lib/merb-cache.rb +15 -0
  5. data/lib/merb-cache/cache.rb +91 -0
  6. data/lib/merb-cache/cache_request.rb +48 -0
  7. data/lib/merb-cache/core_ext/enumerable.rb +9 -0
  8. data/lib/merb-cache/core_ext/hash.rb +21 -0
  9. data/lib/merb-cache/merb_ext/controller/class_methods.rb +244 -0
  10. data/lib/merb-cache/merb_ext/controller/instance_methods.rb +163 -0
  11. data/lib/merb-cache/stores/fundamental/abstract_store.rb +101 -0
  12. data/lib/merb-cache/stores/fundamental/file_store.rb +113 -0
  13. data/lib/merb-cache/stores/fundamental/memcached_store.rb +110 -0
  14. data/lib/merb-cache/stores/strategy/abstract_strategy_store.rb +119 -0
  15. data/lib/merb-cache/stores/strategy/action_store.rb +61 -0
  16. data/lib/merb-cache/stores/strategy/adhoc_store.rb +69 -0
  17. data/lib/merb-cache/stores/strategy/gzip_store.rb +63 -0
  18. data/lib/merb-cache/stores/strategy/mintcache_store.rb +75 -0
  19. data/lib/merb-cache/stores/strategy/page_store.rb +68 -0
  20. data/lib/merb-cache/stores/strategy/sha1_store.rb +62 -0
  21. data/spec/merb-cache/cache_request_spec.rb +78 -0
  22. data/spec/merb-cache/cache_spec.rb +88 -0
  23. data/spec/merb-cache/core_ext/enumerable_spec.rb +26 -0
  24. data/spec/merb-cache/core_ext/hash_spec.rb +51 -0
  25. data/spec/merb-cache/merb_ext/controller_spec.rb +5 -0
  26. data/spec/merb-cache/stores/fundamental/abstract_store_spec.rb +118 -0
  27. data/spec/merb-cache/stores/fundamental/file_store_spec.rb +205 -0
  28. data/spec/merb-cache/stores/fundamental/memcached_store_spec.rb +258 -0
  29. data/spec/merb-cache/stores/strategy/abstract_strategy_store_spec.rb +78 -0
  30. data/spec/merb-cache/stores/strategy/action_store_spec.rb +208 -0
  31. data/spec/merb-cache/stores/strategy/adhoc_store_spec.rb +227 -0
  32. data/spec/merb-cache/stores/strategy/gzip_store_spec.rb +68 -0
  33. data/spec/merb-cache/stores/strategy/mintcache_store_spec.rb +59 -0
  34. data/spec/merb-cache/stores/strategy/page_store_spec.rb +146 -0
  35. data/spec/merb-cache/stores/strategy/sha1_store_spec.rb +84 -0
  36. data/spec/spec_helper.rb +95 -0
  37. metadata +112 -0
@@ -0,0 +1,63 @@
1
+ # Pinched from Tobias Luetke's "cacheable" rails plugin (http://github.com/tobi/cacheable/tree/master)
2
+ require 'zlib'
3
+ require 'stringio'
4
+
5
+ module Merb::Cache
6
+ # Store that compresses cached data using GZip.
7
+ # Usually wraps other stores and good for caching of
8
+ # large pages.
9
+ class GzipStore < AbstractStrategyStore
10
+ def writable?(key, parameters = {}, conditions = {})
11
+ @stores.any? {|c| c.writable?(key, parameters, conditions)}
12
+ end
13
+
14
+ def read(key, parameters = {})
15
+ decompress(@stores.capture_first {|c| c.read(key, parameters)})
16
+ end
17
+
18
+ def write(key, data = nil, parameters = {}, conditions = {})
19
+ if writable?(key, parameters, conditions)
20
+ @stores.capture_first {|c| c.write(key, compress(data), parameters, conditions)}
21
+ end
22
+ end
23
+
24
+ def write_all(key, data = nil, parameters = {}, conditions = {})
25
+ if writable?(key, parameters, conditions)
26
+ @stores.map {|c| c.write_all(key, compress(data), parameters, conditions)}.all?
27
+ end
28
+ end
29
+
30
+ def fetch(key, parameters = {}, conditions = {}, &blk)
31
+ wrapper_blk = lambda { compress(blk.call) }
32
+ decompress(read(key, parameters) || @stores.capture_first {|s| s.fetch(key, parameters, conditions, &wrapper_blk)})
33
+ end
34
+
35
+ def exists?(key, parameters = {})
36
+ @stores.any? {|c| c.exists?(key, parameters)}
37
+ end
38
+
39
+ def delete(key, parameters = {})
40
+ @stores.map {|c| c.delete(key, parameters)}.any?
41
+ end
42
+
43
+ def delete_all!
44
+ @stores.map {|c| c.delete_all! }.all?
45
+ end
46
+
47
+ def compress(data)
48
+ return if data.nil?
49
+
50
+ output = StringIO.new
51
+ gz = Zlib::GzipWriter.new(output)
52
+ gz.write(Marshal.dump(data))
53
+ gz.close
54
+ output.string
55
+ end
56
+
57
+ def decompress(data)
58
+ return if data.nil?
59
+
60
+ Marshal.load(Zlib::GzipReader.new(StringIO.new(data)).read)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,75 @@
1
+ module Merb::Cache
2
+ class MintCacheStore < AbstractStrategyStore
3
+
4
+ def writable?(key, parameters = {}, conditions = {})
5
+ @stores.capture_first {|s| s.writable?(key, parameters, conditions)}
6
+ end
7
+
8
+ def read(key, parameters = {})
9
+ cache_read = @stores.capture_first {|c| c.read(key, parameters)}
10
+ return cache_read || read_mint_cache(key, parameters)
11
+ end
12
+
13
+ def write(key, data = nil, parameters = {}, conditions = {})
14
+ if writable?(key, parameters, conditions)
15
+ write_mint_cache(key, data, parameters, conditions)
16
+ @stores.capture_first {|c| c.write(key, data, parameters, conditions)}
17
+ end
18
+ end
19
+
20
+ # if you're wrapping multiple stores in a strategy store,
21
+ # it will write to all the wrapped stores, not just the first store that is successful
22
+ def write_all(key, data = nil, parameters = {}, conditions = {})
23
+ if writable?(key, parameters, conditions)
24
+ key_write = @stores.map {|c| c.write_all(key, data, parameters, conditions)}.all?
25
+ validity_write = @stores.map {|c| c.write_all(validity_key(key), data, parameters, conditions)}.all?
26
+ data_write = @stores.map {|c| c.write_all(data_key(key), data, parameters, conditions)}.all?
27
+
28
+ return (key_write and validity_write and data_write) ? true : false
29
+ end
30
+ end
31
+
32
+ def fetch(key, parameters = {}, conditions = {}, &blk)
33
+ wrapper_blk = lambda { blk.call }
34
+ cache_read = read(key, parameters) || @stores.capture_first {|s| s.fetch(key, parameters, conditions, &wrapper_blk)}
35
+ return cache_read || read_mint_cache(key, parameters)
36
+ end
37
+
38
+ def exists?(key, parameters = {})
39
+ @stores.any?{|store| store.exists?(key, parameters)} || @stores.any? {|store| store.exists?(validity_key(key), parameters)}
40
+ end
41
+
42
+ def delete(key, parameters = {})
43
+ [key, validity_key(key), data_key(key)].map{|k| @stores.map {|c| c.delete(k, parameters)} }.flatten.any?
44
+ end
45
+
46
+ def delete_all!
47
+ @stores.map {|c| c.delete_all! }.all?
48
+ end
49
+
50
+ private
51
+ def validity_key(key); "#{key}_validity"; end
52
+ def data_key(key); "#{key}_data"; end
53
+
54
+ def write_mint_cache(key, data = nil, parameters = {}, conditions = {})
55
+ expiry = (conditions[:expire_in].nil?) ? 3600 : (conditions[:expire_in] * 2)
56
+
57
+ @stores.capture_first {|c| c.write(validity_key(key), (Time.now + expiry), parameters, conditions.merge({:expire_in => expiry}))}
58
+ @stores.capture_first {|c| c.write(data_key(key), data, parameters, conditions.merge({:expire_in => expiry}))}
59
+ end
60
+
61
+ def read_mint_cache(key, parameters = {})
62
+ validity_time = @stores.capture_first {|c| c.read(validity_key(key), parameters)}
63
+ data = @stores.capture_first {|c| c.read(data_key(key), parameters)}
64
+
65
+ unless validity_time.nil?
66
+ if Time.now < validity_time
67
+ write(key, data, parameters)
68
+ return nil
69
+ end
70
+ end
71
+
72
+ return data
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,68 @@
1
+ module Merb::Cache
2
+ # Store well suited for page caching.
3
+ class PageStore < AbstractStrategyStore
4
+ def writable?(dispatch, parameters = {}, conditions = {})
5
+ if Merb::Controller === dispatch && dispatch.request.method == :get &&
6
+ !dispatch.request.uri.nil? && !dispatch.request.uri.empty? &&
7
+ !conditions.has_key?(:if) && !conditions.has_key?(:unless) &&
8
+ query_string_present?(dispatch)
9
+ @stores.any? {|s| s.writable?(normalize(dispatch), parameters, conditions)}
10
+ else
11
+ false
12
+ end
13
+ end
14
+
15
+ def read(dispatch, parameters = {})
16
+ nil
17
+ end
18
+
19
+ def write(dispatch, data = nil, parameters = {}, conditions = {})
20
+ if writable?(dispatch, parameters, conditions)
21
+ return @stores.capture_first {|s| s.write(normalize(dispatch), data || dispatch.body, {}, conditions)}
22
+ else
23
+ return false
24
+ end
25
+ end
26
+
27
+ def write_all(dispatch, data = nil, parameters = {}, conditions = {})
28
+ if writable?(dispatch, parameters, conditions)
29
+ @stores.map {|s| s.write_all(normalize(dispatch), data || dispatch.body, {}, conditions)}.all?
30
+ end
31
+ end
32
+
33
+ def fetch(dispatch, parameters = {}, conditions = {}, &blk)
34
+ if writable?(dispatch, parameters, conditions)
35
+ read(dispatch, parameters) || @stores.capture_first {|s| s.fetch(normalize(dispatch), data || dispatch.body, {}, conditions, &blk)}
36
+ end
37
+ end
38
+
39
+ def exists?(dispatch, parameters = {})
40
+ if writable?(dispatch, parameters)
41
+ return @stores.any? {|s| s.exists?(normalize(dispatch), {})}
42
+ else
43
+ return false
44
+ end
45
+ end
46
+
47
+ def delete(dispatch, parameters = {})
48
+ if writable?(dispatch, parameters)
49
+ @stores.map {|s| s.delete(normalize(dispatch), {})}.any?
50
+ end
51
+ end
52
+
53
+ def delete_all!
54
+ @stores.map {|s| s.delete_all!}.all?
55
+ end
56
+
57
+ def normalize(dispatch)
58
+ key = dispatch.request.uri.split('?').first
59
+ key << "index" if key =~ /\/$/
60
+ key << ".#{dispatch.content_type}" unless key =~ /\.\w{2,6}/
61
+ key
62
+ end
63
+
64
+ def query_string_present?(dispatch)
65
+ dispatch.request.env["REQUEST_URI"] == dispatch.request.uri
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ require 'digest/sha1'
2
+
3
+ module Merb::Cache
4
+ # Strategy store that uses SHA1 hex of
5
+ # base cache key and parameters as
6
+ # cache key.
7
+ #
8
+ # It is good for caching of expensive
9
+ # search queries that use multiple
10
+ # parameters passed via query string
11
+ # of request.
12
+ class SHA1Store < AbstractStrategyStore
13
+ def initialize(config = {})
14
+ super(config)
15
+ @map = {}
16
+ end
17
+
18
+ def writable?(key, parameters = {}, conditions = {})
19
+ case key
20
+ when String, Numeric, Symbol
21
+ @stores.any? {|c| c.writable?(digest(key, parameters), {}, conditions)}
22
+ else nil
23
+ end
24
+ end
25
+
26
+ def read(key, parameters = {})
27
+ @stores.capture_first {|c| c.read(digest(key, parameters))}
28
+ end
29
+
30
+ def write(key, data = nil, parameters = {}, conditions = {})
31
+ if writable?(key, parameters, conditions)
32
+ @stores.capture_first {|c| c.write(digest(key, parameters), data, {}, conditions)}
33
+ end
34
+ end
35
+
36
+ def write_all(key, data = nil, parameters = {}, conditions = {})
37
+ if writable?(key, parameters, conditions)
38
+ @stores.map {|c| c.write_all(digest(key, parameters), data, {}, conditions)}.all?
39
+ end
40
+ end
41
+
42
+ def fetch(key, parameters = {}, conditions = {}, &blk)
43
+ read(key, parameters) || (writable?(key, parameters, conditions) && @stores.capture_first {|c| c.fetch(digest(key, parameters), {}, conditions, &blk)})
44
+ end
45
+
46
+ def exists?(key, parameters = {})
47
+ @stores.any? {|c| x = c.exists?(digest(key, parameters))}
48
+ end
49
+
50
+ def delete(key, parameters = {})
51
+ @stores.map {|c| c.delete(digest(key, parameters))}.any?
52
+ end
53
+
54
+ def delete_all!
55
+ @stores.map {|c| c.delete_all! }.all?
56
+ end
57
+
58
+ def digest(key, parameters = {})
59
+ @map[[key, parameters]] ||= Digest::SHA1.hexdigest("#{key}#{parameters.to_sha2}")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,78 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Merb::Cache::CacheRequest do
4
+ it "should subclass Merb::Request" do
5
+ Merb::Cache::CacheRequest.superclass.should == Merb::Request
6
+ end
7
+
8
+ describe "#env" do
9
+ it "can be specified in the constructor" do
10
+ Merb::Cache::CacheRequest.new('', {}, 'foo' => 'bar').env['foo'].should == 'bar'
11
+ end
12
+ end
13
+
14
+ describe "#env[Merb::Const::REQUEST_URI]" do
15
+ it "should give the uri with the query string" do
16
+ Merb::Cache::CacheRequest.new('/test?q=1').env[Merb::Const::REQUEST_URI].should == '/test?q=1'
17
+ end
18
+ end
19
+
20
+ describe "#host" do
21
+ it "should return the correct host if the uri is absolute" do
22
+ Merb::Cache::CacheRequest.new('http://example.org:453/').host.should == "example.org:453"
23
+ end
24
+ end
25
+
26
+ describe "#method" do
27
+ it "should be :get by default" do
28
+ Merb::Cache::CacheRequest.new('/test?q=1').method.should == :get
29
+ end
30
+
31
+ it "should be set by the :method parameter" do
32
+ Merb::Cache::CacheRequest.new('/test?q=1', {}, :method => :put).method.should == :put
33
+ end
34
+ end
35
+
36
+ describe "#path" do
37
+ it "can be specified without manipulating the env" do
38
+ Merb::Cache::CacheRequest.new('/path/to/foo').path.should == '/path/to/foo'
39
+ end
40
+
41
+ it "should return the path without the query string" do
42
+ Merb::Cache::CacheRequest.new('/path/to/foo?q=1').path.should == '/path/to/foo'
43
+ end
44
+ end
45
+
46
+ describe "#params" do
47
+ it "can be specified without manipulating the env" do
48
+ Merb::Cache::CacheRequest.new('/', 'foo' => 'bar').params.should == {'foo' => 'bar'}
49
+ end
50
+ end
51
+
52
+ describe "#subdomains" do
53
+ it "should return the correct subdomains if the uri is absolute" do
54
+ Merb::Cache::CacheRequest.new('http://test.example.org:453/').subdomains.should == ['test']
55
+ end
56
+ end
57
+
58
+ describe "#uri" do
59
+ it "should give the uri without the query string" do
60
+ Merb::Cache::CacheRequest.new('/test?q=1').uri.should == '/test'
61
+ end
62
+ end
63
+
64
+ it "should be compatiple with page store's way of detecting the presence of a query string" do
65
+ request = Merb::Cache::CacheRequest.new("/test?q=1")
66
+ (request.env[Merb::Const::REQUEST_URI] == request.uri).should be_false
67
+ request = Merb::Cache::CacheRequest.new("/test")
68
+ (request.env[Merb::Const::REQUEST_URI] == request.uri).should be_true
69
+ end
70
+
71
+ it "should not setup the uri if no uri is provided and env[Merb::Const::REQUEST_URI] is not nil" do
72
+ Merb::Cache::CacheRequest.new(nil, {}, {Merb::Const::REQUEST_URI => '/test'}).uri.should == '/test'
73
+ end
74
+
75
+ it "should setup a default env" do
76
+ Merb::Cache::CacheRequest.new('').env.should_not be_empty
77
+ end
78
+ end
@@ -0,0 +1,88 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Merb::Cache do
4
+ before(:each) do
5
+ Merb::Cache.stores.clear
6
+ Thread.current[:'merb-cache'] = nil
7
+ end
8
+
9
+ describe ".setup" do
10
+ it "should have have access to the Merb::Cache.register method from the block" do
11
+ Merb::Cache.setup do
12
+ self.respond_to?(:register).should == true
13
+ end
14
+ end
15
+ end
16
+
17
+ describe ".register" do
18
+ it "should add the store name and instance to the store hash" do
19
+ Merb::Cache.stores.should_not have_key(:foo)
20
+ Merb::Cache.register(:foo, DummyStore)
21
+ Merb::Cache.stores.should have_key(:foo)
22
+ end
23
+
24
+ it "should use :default when no name is supplied" do
25
+ Merb::Cache.stores.should_not have_key(:default)
26
+ Merb::Cache.register(DummyStore)
27
+ Merb::Cache.stores.should have_key(:default)
28
+ end
29
+
30
+ it "should not allow a store to be redefined" do
31
+ Merb::Cache.register(DummyStore)
32
+ lambda do
33
+ Merb::Cache.register(DummyStore)
34
+ end.should raise_error(Merb::Cache::StoreExists)
35
+ end
36
+ end
37
+
38
+ describe ".exists?" do
39
+ it "should return true if a repository is setup" do
40
+ Merb::Cache.register(DummyStore)
41
+ Merb::Cache.register(:store_that_exists, DummyStore)
42
+ Merb::Cache.exists?(:default).should be_true
43
+ Merb::Cache.exists?(:store_that_exists).should be_true
44
+ end
45
+
46
+ it "should return false if a repository is not setup" do
47
+ Merb::Cache.exists?(:not_here).should be_false
48
+ end
49
+ end
50
+
51
+ describe ".[]" do
52
+ it "should clone the stores so to keep them threadsafe" do
53
+ Merb::Cache.register(DummyStore)
54
+ Merb::Cache[:default].should_not be_nil
55
+ Merb::Cache[:default].should_not == Merb::Cache.stores[:default]
56
+ end
57
+
58
+ it "should cache the thread local stores in Thread.current" do
59
+ Merb::Cache.register(DummyStore)
60
+ Thread.current[:'merb-cache'].should be_nil
61
+ Merb::Cache[:default]
62
+ Thread.current[:'merb-cache'].should_not be_nil
63
+ end
64
+
65
+ it "should create an adhoc store if multiple store names are supplied" do
66
+ Merb::Cache.register(DummyStore)
67
+ Merb::Cache.register(:dummy, DummyStore)
68
+ Merb::Cache[:default, :dummy].class.should == Merb::Cache::AdhocStore
69
+ end
70
+
71
+ it "should let you create new stores after accessing the old ones" do
72
+ Merb::Cache.register(DummyStore)
73
+ Merb::Cache.register(:one, DummyStore)
74
+ Merb::Cache[:default].should_not be_nil
75
+ Merb::Cache[:one].should_not be_nil
76
+ Merb::Cache.register(:two, DummyStore)
77
+ Merb::Cache[:two].should_not be_nil
78
+ end
79
+
80
+ it "should raise an error if the cache has not been setup" do
81
+ Merb::Cache.register(DummyStore)
82
+ Merb::Cache[:default].should_not be_nil
83
+ lambda do
84
+ Merb::Cache[:does_not_exist]
85
+ end.should raise_error(Merb::Cache::StoreNotFound)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,26 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Enumerable do
4
+ describe "#capture_first" do
5
+ it "should return the result of the first block call that is non-nil, not the item sent to the block" do
6
+ [1, 2, 3].capture_first {|i| i ** i if i % 2 == 0}.should == 4
7
+ end
8
+
9
+ it "should return nil if all block calls are nil" do
10
+ [1, 2, 3].capture_first {|i| nil }.should be_nil
11
+ end
12
+
13
+ it "should stop calling the block once a block evaluates to non-nil" do
14
+ lambda {
15
+ [1, 2, 3].capture_first do |i|
16
+ raise "#{i} is divisible by 3!" if i % 3 == 0
17
+ i ** i if i % 2 == 0
18
+ end
19
+ }.should_not raise_error
20
+ end
21
+
22
+ it "should return the first item 'captured'" do
23
+ [1, 2, 3].capture_first{|i| i }.should == 1
24
+ end
25
+ end
26
+ end