rack-cache 0.2.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.

Potentially problematic release.


This version of rack-cache might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/CHANGES +27 -0
  2. data/COPYING +18 -0
  3. data/README +96 -0
  4. data/Rakefile +144 -0
  5. data/TODO +40 -0
  6. data/doc/configuration.markdown +224 -0
  7. data/doc/events.dot +27 -0
  8. data/doc/faq.markdown +133 -0
  9. data/doc/index.markdown +113 -0
  10. data/doc/layout.html.erb +33 -0
  11. data/doc/license.markdown +24 -0
  12. data/doc/rack-cache.css +362 -0
  13. data/doc/storage.markdown +162 -0
  14. data/lib/rack/cache.rb +51 -0
  15. data/lib/rack/cache/config.rb +65 -0
  16. data/lib/rack/cache/config/busters.rb +16 -0
  17. data/lib/rack/cache/config/default.rb +134 -0
  18. data/lib/rack/cache/config/no-cache.rb +13 -0
  19. data/lib/rack/cache/context.rb +95 -0
  20. data/lib/rack/cache/core.rb +271 -0
  21. data/lib/rack/cache/entitystore.rb +224 -0
  22. data/lib/rack/cache/headers.rb +237 -0
  23. data/lib/rack/cache/metastore.rb +309 -0
  24. data/lib/rack/cache/options.rb +119 -0
  25. data/lib/rack/cache/request.rb +37 -0
  26. data/lib/rack/cache/response.rb +76 -0
  27. data/lib/rack/cache/storage.rb +50 -0
  28. data/lib/rack/utils/environment_headers.rb +78 -0
  29. data/rack-cache.gemspec +74 -0
  30. data/test/cache_test.rb +35 -0
  31. data/test/config_test.rb +66 -0
  32. data/test/context_test.rb +465 -0
  33. data/test/core_test.rb +84 -0
  34. data/test/entitystore_test.rb +176 -0
  35. data/test/environment_headers_test.rb +71 -0
  36. data/test/headers_test.rb +215 -0
  37. data/test/logging_test.rb +45 -0
  38. data/test/metastore_test.rb +210 -0
  39. data/test/options_test.rb +64 -0
  40. data/test/pony.jpg +0 -0
  41. data/test/response_test.rb +37 -0
  42. data/test/spec_setup.rb +189 -0
  43. data/test/storage_test.rb +94 -0
  44. metadata +120 -0
@@ -0,0 +1,45 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/context'
3
+
4
+ describe "Rack::Cache::Context logging" do
5
+
6
+ before(:each) do
7
+ respond_with 200
8
+ @errors = StringIO.new
9
+ @cache = Rack::Cache::Context.new(@app)
10
+ @cache.errors = @errors
11
+ @cache.metaclass.send :public, :log, :trace, :warn, :info
12
+ end
13
+
14
+ it 'responds to #log by writing message to #errors' do
15
+ @cache.log :test, 'is this thing on?'
16
+ @errors.string.should.be == "[cache] test: is this thing on?\n"
17
+ end
18
+
19
+ it 'allows printf formatting arguments' do
20
+ @cache.log :test, '%s %p %i %x', 'hello', 'goodbye', 42, 66
21
+ @errors.string.should.be == "[cache] test: hello \"goodbye\" 42 42\n"
22
+ end
23
+
24
+ it 'responds to #info by logging an :info message' do
25
+ @cache.info 'informative stuff'
26
+ @errors.string.should.be == "[cache] info: informative stuff\n"
27
+ end
28
+
29
+ it 'responds to #warn by logging an :warn message' do
30
+ @cache.warn 'kinda/maybe bad stuff'
31
+ @errors.string.should.be == "[cache] warn: kinda/maybe bad stuff\n"
32
+ end
33
+
34
+ it 'responds to #trace by logging a :trace message' do
35
+ @cache.trace 'some insignifacant event'
36
+ @errors.string.should.be == "[cache] trace: some insignifacant event\n"
37
+ end
38
+
39
+ it "doesn't log trace messages when not in verbose mode" do
40
+ @cache.verbose = false
41
+ @cache.trace 'some insignifacant event'
42
+ @errors.string.should.be == ""
43
+ end
44
+
45
+ end
@@ -0,0 +1,210 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/metastore'
3
+
4
+ describe_shared 'A Rack::Cache::MetaStore Implementation' do
5
+
6
+ before do
7
+ @request = mock_request('/', {})
8
+ @response = mock_response(200, {}, ['hello world'])
9
+ @entity_store = nil
10
+ end
11
+ after do
12
+ @store = nil
13
+ @entity_store = nil
14
+ end
15
+
16
+ # Low-level implementation methods ===========================================
17
+
18
+ it 'writes a list of negotation tuples with #write' do
19
+ lambda { @store.write('/test', [[{}, {}]]) }.should.not.raise
20
+ end
21
+
22
+ it 'reads a list of negotation tuples with #read' do
23
+ @store.write('/test', [[{},{}],[{},{}]])
24
+ tuples = @store.read('/test')
25
+ tuples.should.be == [ [{},{}], [{},{}] ]
26
+ end
27
+
28
+ it 'reads an empty list with #read when nothing cached at key' do
29
+ @store.read('/nothing').should.be.empty
30
+ end
31
+
32
+ it 'removes entries for key with #purge' do
33
+ @store.write('/test', [[{},{}]])
34
+ @store.read('/test').should.not.be.empty
35
+
36
+ @store.purge('/test')
37
+ @store.read('/test').should.be.empty
38
+ end
39
+
40
+ it 'succeeds when purging non-existing entries' do
41
+ @store.read('/test').should.be.empty
42
+ @store.purge('/test')
43
+ end
44
+
45
+ it 'returns nil from #purge' do
46
+ @store.write('/test', [[{},{}]])
47
+ @store.purge('/test').should.be nil
48
+ @store.read('/test').should.be == []
49
+ end
50
+
51
+ %w[/test http://example.com:8080/ /test?x=y /test?x=y&p=q].each do |key|
52
+ it "can read and write key: '#{key}'" do
53
+ lambda { @store.write(key, [[{},{}]]) }.should.not.raise
54
+ @store.read(key).should.be == [[{},{}]]
55
+ end
56
+ end
57
+
58
+ it "can read and write fairly large keys" do
59
+ key = "b" * 4096
60
+ lambda { @store.write(key, [[{},{}]]) }.should.not.raise
61
+ @store.read(key).should.be == [[{},{}]]
62
+ end
63
+
64
+ # Abstract methods ===========================================================
65
+
66
+ define_method :store_simple_entry do
67
+ @request = mock_request('/test', {})
68
+ @response = mock_response(200, {'Cache-Control' => 'max-age=420'}, ['test'])
69
+ body = @response.body
70
+ @store.store(@request, @response, @entity_store)
71
+ @response.body.should.not.be body
72
+ end
73
+
74
+ it 'stores a cache entry' do
75
+ store_simple_entry
76
+ @store.read('/test').should.not.be.empty
77
+ end
78
+
79
+ it 'sets the X-Content-Digest response header before storing' do
80
+ store_simple_entry
81
+ req, res = @store.read('/test').first
82
+ res['X-Content-Digest'].should.be == 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
83
+ end
84
+
85
+ it 'finds a stored entry with #lookup' do
86
+ store_simple_entry
87
+ response = @store.lookup(@request, @entity_store)
88
+ response.should.not.be.nil
89
+ response.should.be.kind_of Rack::Cache::Response
90
+ end
91
+
92
+ it 'restores response headers properly with #lookup' do
93
+ store_simple_entry
94
+ response = @store.lookup(@request, @entity_store)
95
+ response.headers.reject{|k,v| k =~ /^X-/}.
96
+ should.be == @response.headers.merge('Age' => '0', 'Content-Length' => '4')
97
+ end
98
+
99
+ it 'restores response body from entity store with #lookup' do
100
+ store_simple_entry
101
+ response = @store.lookup(@request, @entity_store)
102
+ body = '' ; response.body.each {|p| body << p}
103
+ body.should.be == 'test'
104
+ end
105
+
106
+ # Vary =======================================================================
107
+
108
+ it 'does not return entries that Vary with #lookup' do
109
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
110
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
111
+ res = mock_response(200, {'Vary' => 'Foo Bar'}, ['test'])
112
+ @store.store(req1, res, @entity_store)
113
+
114
+ @store.lookup(req2, @entity_store).should.be.nil
115
+ end
116
+
117
+ it 'stores multiple responses for each Vary combination' do
118
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
119
+ res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
120
+ @store.store(req1, res1, @entity_store)
121
+
122
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
123
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
124
+ @store.store(req2, res2, @entity_store)
125
+
126
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom'})
127
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
128
+ @store.store(req3, res3, @entity_store)
129
+
130
+ slurp(@store.lookup(req3, @entity_store).body).should.be == 'test 3'
131
+ slurp(@store.lookup(req1, @entity_store).body).should.be == 'test 1'
132
+ slurp(@store.lookup(req2, @entity_store).body).should.be == 'test 2'
133
+
134
+ @store.read('/test').length.should.be == 3
135
+ end
136
+
137
+ it 'overwrites non-varying responses with #store' do
138
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
139
+ res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
140
+ @store.store(req1, res1, @entity_store)
141
+ slurp(@store.lookup(req1, @entity_store).body).should.be == 'test 1'
142
+
143
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
144
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
145
+ @store.store(req2, res2, @entity_store)
146
+ slurp(@store.lookup(req2, @entity_store).body).should.be == 'test 2'
147
+
148
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
149
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
150
+ @store.store(req3, res3, @entity_store)
151
+ slurp(@store.lookup(req1, @entity_store).body).should.be == 'test 3'
152
+
153
+ @store.read('/test').length.should.be == 2
154
+ end
155
+
156
+ # Helper Methods =============================================================
157
+
158
+ define_method :mock_request do |uri,opts|
159
+ env = Rack::MockRequest.env_for(uri, opts || {})
160
+ Rack::Cache::Request.new(env)
161
+ end
162
+
163
+ define_method :mock_response do |status,headers,body|
164
+ headers ||= {}
165
+ body = Array(body).compact
166
+ Rack::Cache::Response.new(status, headers, body)
167
+ end
168
+
169
+ define_method :slurp do |body|
170
+ buf = ''
171
+ body.each {|part| buf << part }
172
+ buf
173
+ end
174
+
175
+ end
176
+
177
+
178
+ describe 'Rack::Cache::MetaStore' do
179
+ describe 'Heap' do
180
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
181
+ before do
182
+ @store = Rack::Cache::MetaStore::Heap.new
183
+ @entity_store = Rack::Cache::EntityStore::Heap.new
184
+ end
185
+ end
186
+
187
+ describe 'Disk' do
188
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
189
+ before do
190
+ @temp_dir = create_temp_directory
191
+ @store = Rack::Cache::MetaStore::Disk.new("#{@temp_dir}/meta")
192
+ @entity_store = Rack::Cache::EntityStore::Disk.new("#{@temp_dir}/entity")
193
+ end
194
+ after do
195
+ remove_entry_secure @temp_dir
196
+ end
197
+ end
198
+
199
+ need_memcached 'metastore tests' do
200
+ describe 'MemCache' do
201
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
202
+ before :each do
203
+ @temp_dir = create_temp_directory
204
+ $memcached.flush
205
+ @store = Rack::Cache::MetaStore::MemCache.new($memcached)
206
+ @entity_store = Rack::Cache::EntityStore::Heap.new
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,64 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/options'
3
+
4
+ module Rack::Cache::Options
5
+ option_accessor :foo
6
+ end
7
+
8
+ class MockOptions
9
+ include Rack::Cache::Options
10
+ alias_method :initialize, :initialize_options
11
+ end
12
+
13
+ describe 'Rack::Cache::Options' do
14
+ before { @options = MockOptions.new }
15
+
16
+ describe '#set' do
17
+ it 'sets a Symbol option as rack-cache.symbol' do
18
+ @options.set :bar, 'baz'
19
+ @options.options['rack-cache.bar'].should.be == 'baz'
20
+ end
21
+ it 'sets a String option as string' do
22
+ @options.set 'foo.bar', 'bling'
23
+ @options.options['foo.bar'].should.be == 'bling'
24
+ end
25
+ it 'sets all key/value pairs when given a Hash' do
26
+ @options.set :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling'
27
+ @options.foo.should.be == 'bar'
28
+ @options.options['rack-cache.bar'].should.be == 'baz'
29
+ @options.options['foo.bar'].should.be == 'bling'
30
+ end
31
+ end
32
+
33
+ it 'makes options declared with option_accessor available as attributes' do
34
+ @options.set :foo, 'bar'
35
+ @options.foo.should.be == 'bar'
36
+ end
37
+
38
+ it 'allows setting multiple options via assignment' do
39
+ @options.options = { :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling' }
40
+ @options.foo.should.be == 'bar'
41
+ @options.options['foo.bar'].should.be == 'bling'
42
+ @options.options['rack-cache.bar'].should.be == 'baz'
43
+ end
44
+
45
+ it 'allows the meta store to be configured' do
46
+ @options.should.respond_to :metastore
47
+ @options.should.respond_to :metastore=
48
+ @options.metastore.should.not.be nil
49
+ end
50
+
51
+ it 'allows the entity store to be configured' do
52
+ @options.should.respond_to :entitystore
53
+ @options.should.respond_to :entitystore=
54
+ @options.entitystore.should.not.be nil
55
+ end
56
+
57
+ it 'allows log verbosity to be configured' do
58
+ @options.should.respond_to :verbose
59
+ @options.should.respond_to :verbose=
60
+ @options.should.respond_to :verbose?
61
+ @options.verbose.should.not.be.nil
62
+ end
63
+
64
+ end
data/test/pony.jpg ADDED
Binary file
@@ -0,0 +1,37 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+
3
+ describe 'Rack::Cache::Response' do
4
+
5
+ before(:each) {
6
+ @now = Time.now
7
+ @response = Rack::Cache::Response.new(200, {'Date' => @now.httpdate}, '')
8
+ @one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
9
+ }
10
+
11
+ after(:each) {
12
+ @now, @response, @one_hour_ago = nil
13
+ }
14
+
15
+ it 'responds to cache-related methods' do
16
+ @response.should.respond_to :ttl
17
+ @response.should.respond_to :age
18
+ @response.should.respond_to :date
19
+ end
20
+
21
+ it 'responds to #to_a with a Rack response tuple' do
22
+ @response.should.respond_to :to_a
23
+ @response.to_a.should.be == [200, {'Date' => @now.httpdate}, '']
24
+ end
25
+
26
+ it 'retrieves headers with #[]' do
27
+ @response.headers['X-Foo'] = 'bar'
28
+ @response.should.respond_to :[]
29
+ @response['X-Foo'].should.be == 'bar'
30
+ end
31
+
32
+ it 'sets headers with #[]=' do
33
+ @response.should.respond_to :[]=
34
+ @response['X-Foo'] = 'bar'
35
+ @response.headers['X-Foo'].should.be == 'bar'
36
+ end
37
+ end
@@ -0,0 +1,189 @@
1
+ require 'pp'
2
+ require 'tmpdir'
3
+
4
+ [ STDOUT, STDERR ].each { |io| io.sync = true }
5
+
6
+ begin
7
+ require 'test/spec'
8
+ rescue LoadError => boom
9
+ require 'rubygems' rescue nil
10
+ require 'test/spec'
11
+ end
12
+
13
+ # Set the MEMCACHED environment variable as follows to enable testing
14
+ # of the MemCached meta and entity stores.
15
+ ENV['MEMCACHED'] ||= 'localhost:11215'
16
+ $memcached = nil
17
+
18
+ def have_memcached?(server=ENV['MEMCACHED'])
19
+ return true if $memcached
20
+ require 'memcached'
21
+ $memcached = Memcached.new(server)
22
+ $memcached.set('ping', '')
23
+ true
24
+ rescue LoadError => boom
25
+ $memcached = nil
26
+ false
27
+ rescue => boom
28
+ $memcached = nil
29
+ false
30
+ end
31
+
32
+ def need_memcached(forwhat)
33
+ if have_memcached?
34
+ yield
35
+ else
36
+ STDERR.puts "skipping memcached #{forwhat} (MEMCACHED environment variable not set)"
37
+ end
38
+ end
39
+
40
+ # Setup the load path ..
41
+ $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__)) + '/lib'
42
+ $LOAD_PATH.unshift File.dirname(__FILE__)
43
+
44
+ require 'rack/cache'
45
+
46
+
47
+ # Methods for constructing downstream applications / response
48
+ # generators.
49
+ module CacheContextHelpers
50
+
51
+ # The Rack::Cache::Context instance used for the most recent
52
+ # request.
53
+ attr_reader :cache
54
+
55
+ # An Array of Rack::Cache::Context instances used for each request, in
56
+ # request order.
57
+ attr_reader :caches
58
+
59
+ # The Rack::Response instance result of the most recent request.
60
+ attr_reader :response
61
+
62
+ # An Array of Rack::Response instances for each request, in request order.
63
+ attr_reader :responses
64
+
65
+ # The backend application object.
66
+ attr_reader :app
67
+
68
+ def setup_cache_context
69
+ # holds each Rack::Cache::Context
70
+ @app = nil
71
+
72
+ # each time a request is made, a clone of @cache_template is used
73
+ # and appended to @caches.
74
+ @cache_template = nil
75
+ @cache = nil
76
+ @caches = []
77
+ @errors = StringIO.new
78
+ @cache_config = nil
79
+
80
+ @called = false
81
+ @request = nil
82
+ @response = nil
83
+ @responses = []
84
+
85
+ @storage = Rack::Cache::Storage.new
86
+ end
87
+
88
+ def teardown_cache_context
89
+ @app, @cache_template, @cache, @caches, @called,
90
+ @request, @response, @responses, @cache_config = nil
91
+ end
92
+
93
+ # A basic response with 200 status code and a tiny body.
94
+ def respond_with(status=200, headers={}, body=['Hello World'])
95
+ called = false
96
+ @app =
97
+ lambda do |env|
98
+ called = true
99
+ response = Rack::Response.new(body, status, headers)
100
+ request = Rack::Request.new(env)
101
+ yield request, response if block_given?
102
+ response.finish
103
+ end
104
+ @app.meta_def(:called?) { called }
105
+ @app.meta_def(:reset!) { called = false }
106
+ @app
107
+ end
108
+
109
+ def cache_config(&block)
110
+ @cache_config = block
111
+ end
112
+
113
+ def request(method, uri='/', opts={})
114
+ opts = {
115
+ 'rack.run_once' => true,
116
+ 'rack.errors' => @errors,
117
+ 'rack-cache.storage' => @storage
118
+ }.merge(opts)
119
+
120
+ fail 'response not specified (use respond_with)' if @app.nil?
121
+ @app.reset! if @app.respond_to?(:reset!)
122
+
123
+ @cache_prototype ||= Rack::Cache::Context.new(@app, &@cache_config)
124
+ @cache = @cache_prototype.clone
125
+ @caches << @cache
126
+ @request = Rack::MockRequest.new(@cache)
127
+ yield @cache if block_given?
128
+ @response = @request.send(method, uri, opts)
129
+ @responses << @response
130
+ @response
131
+ end
132
+
133
+ def get(stem, env={}, &b)
134
+ request(:get, stem, env, &b)
135
+ end
136
+
137
+ def post(*args, &b)
138
+ request(:post, *args, &b)
139
+ end
140
+
141
+ end
142
+
143
+
144
+ module TestHelpers
145
+ include FileUtils
146
+ F = File
147
+
148
+ @@temp_dir_count = 0
149
+
150
+ def create_temp_directory
151
+ @@temp_dir_count += 1
152
+ path = F.join(Dir.tmpdir, "rcl-#{$$}-#{@@temp_dir_count}")
153
+ mkdir_p path
154
+ if block_given?
155
+ yield path
156
+ remove_entry_secure path
157
+ end
158
+ path
159
+ end
160
+
161
+ def create_temp_file(root, file, data='')
162
+ path = F.join(root, file)
163
+ mkdir_p F.dirname(path)
164
+ F.open(path, 'w') { |io| io.write(data) }
165
+ end
166
+
167
+ end
168
+
169
+ class Test::Unit::TestCase
170
+ include TestHelpers
171
+ include CacheContextHelpers
172
+ end
173
+
174
+ # Metaid == a few simple metaclass helper
175
+ # (See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html.)
176
+ class Object
177
+ # The hidden singleton lurks behind everyone
178
+ def metaclass; class << self; self; end; end
179
+ def meta_eval(&blk); metaclass.instance_eval(&blk); end
180
+ # Adds methods to a metaclass
181
+ def meta_def name, &blk
182
+ meta_eval { define_method name, &blk }
183
+ end
184
+ # Defines an instance method within a class
185
+ def class_def name, &blk
186
+ class_eval { define_method name, &blk }
187
+ end
188
+ end
189
+