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
data/test/core_test.rb ADDED
@@ -0,0 +1,84 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/core'
3
+
4
+ class MockCore
5
+ include Rack::Cache::Core
6
+ alias_method :initialize, :initialize_core
7
+ public :transition, :trigger, :events
8
+ end
9
+
10
+ describe 'Rack::Cache::Core' do
11
+ before :each do
12
+ @core = MockCore.new
13
+ end
14
+
15
+ it 'has events after instantiation' do
16
+ @core.events.should.respond_to :[]
17
+ end
18
+ it 'defines and triggers event handlers' do
19
+ executed = false
20
+ @core.on(:foo) { executed = true }
21
+ @core.trigger :foo
22
+ executed.should.be true
23
+ end
24
+ it 'executes multiple handlers in LIFO order' do
25
+ x = 'nothing executed'
26
+ @core.on :foo do
27
+ x.should.be == 'bottom executed'
28
+ x = 'top executed'
29
+ end
30
+ @core.on :foo do
31
+ x.should.be == 'nothing executed'
32
+ x = 'bottom executed'
33
+ end
34
+ @core.trigger :foo
35
+ x.should.be == 'top executed'
36
+ end
37
+ it 'records event execution history' do
38
+ @core.on(:foo) {}
39
+ @core.trigger :foo
40
+ @core.should.a.performed :foo
41
+ end
42
+ it 'raises an exception when asked to perform an unknown event' do
43
+ assert_raises NameError do
44
+ @core.trigger :foo
45
+ end
46
+ end
47
+ it 'raises an exception when asked to transition to an unknown event' do
48
+ @core.on(:bling) {}
49
+ @core.on(:foo) { throw(:transition, [:bling]) }
50
+ lambda { @core.transition(from=:foo, to=[:bar, :baz]) }.
51
+ should.raise Rack::Cache::IllegalTransition
52
+ end
53
+ it 'passes transition arguments to handlers' do
54
+ passed = nil
55
+ @core.meta_def(:perform_bar) do |*args|
56
+ passed = args
57
+ 'hi'
58
+ end
59
+ @core.on(:bar) {}
60
+ @core.on(:foo) { throw(:transition, [:bar, 1, 2, 3]) }
61
+ result = @core.transition(from=:foo, to=[:bar])
62
+ passed.should.be == [1,2,3]
63
+ result.should.be == 'hi'
64
+ end
65
+ it 'fully transitions out of handlers when the next event is invoked' do
66
+ x = []
67
+ @core.on(:foo) {
68
+ x << 'in foo, before transitioning to bar'
69
+ throw(:transition, [:bar])
70
+ x << 'in foo, after transitioning to bar'
71
+ }
72
+ @core.on(:bar) { x << 'in bar' }
73
+ @core.trigger(:foo).should.be == [:bar]
74
+ @core.trigger(:bar).should.be.nil
75
+ x.should.be == [
76
+ 'in foo, before transitioning to bar',
77
+ 'in bar'
78
+ ]
79
+ end
80
+ it 'returns the transition event name' do
81
+ @core.on(:foo) { throw(:transition, [:bar]) }
82
+ @core.trigger(:foo).should.be == [:bar]
83
+ end
84
+ end
@@ -0,0 +1,176 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/entitystore'
3
+
4
+ class Object
5
+ def sha_like?
6
+ length == 40 && self =~ /^[0-9a-z]+$/
7
+ end
8
+ end
9
+
10
+ describe_shared 'A Rack::Cache::EntityStore Implementation' do
11
+
12
+ it 'responds to all required messages' do
13
+ %w[read open write exist?].each do |message|
14
+ @store.should.respond_to message
15
+ end
16
+ end
17
+
18
+ it 'stores bodies with #write' do
19
+ key, size = @store.write('My wild love went riding,')
20
+ key.should.not.be.nil
21
+ key.should.be.sha_like
22
+
23
+ data = @store.read(key)
24
+ data.should.be == 'My wild love went riding,'
25
+ end
26
+
27
+ it 'correctly determines whether cached body exists for key with #exist?' do
28
+ key, size = @store.write('She rode to the devil,')
29
+ @store.should.exist key
30
+ @store.should.not.exist '938jasddj83jasdh4438021ksdfjsdfjsdsf'
31
+ end
32
+
33
+ it 'can read data written with #write' do
34
+ key, size = @store.write('And asked him to pay.')
35
+ data = @store.read(key)
36
+ data.should.be == 'And asked him to pay.'
37
+ end
38
+
39
+ it 'gives a 40 character SHA1 hex digest from #write' do
40
+ key, size = @store.write('she rode to the sea;')
41
+ key.should.not.be.nil
42
+ key.length.should.be == 40
43
+ key.should.be =~ /^[0-9a-z]+$/
44
+ key.should.be == '90a4c84d51a277f3dafc34693ca264531b9f51b6'
45
+ end
46
+
47
+ it 'returns the entire body as a String from #read' do
48
+ key, size = @store.write('She gathered together')
49
+ @store.read(key).should.be == 'She gathered together'
50
+ end
51
+
52
+ it 'returns nil from #read when key does not exist' do
53
+ @store.read('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
54
+ end
55
+
56
+ it 'returns a Rack compatible body from #open' do
57
+ key, size = @store.write('Some shells for her hair.')
58
+ body = @store.open(key)
59
+ body.should.respond_to :each
60
+ buf = ''
61
+ body.each { |part| buf << part }
62
+ buf.should.be == 'Some shells for her hair.'
63
+ end
64
+
65
+ it 'returns nil from #open when key does not exist' do
66
+ @store.open('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
67
+ end
68
+
69
+ it 'can store largish bodies with binary data' do
70
+ pony = File.read(File.dirname(__FILE__) + '/pony.jpg')
71
+ key, size = @store.write(pony)
72
+ key.should.be == 'd0f30d8659b4d268c5c64385d9790024c2d78deb'
73
+ data = @store.read(key)
74
+ data.length.should.be == pony.length
75
+ data.hash.should.be == pony.hash
76
+ end
77
+
78
+ end
79
+
80
+ describe 'Rack::Cache::EntityStore' do
81
+
82
+ describe 'Heap' do
83
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
84
+ before { @store = Rack::Cache::EntityStore::Heap.new }
85
+ it 'takes a Hash to ::new' do
86
+ @store = Rack::Cache::EntityStore::Heap.new('foo' => ['bar'])
87
+ @store.read('foo').should.be == 'bar'
88
+ end
89
+ it 'uses its own Hash with no args to ::new' do
90
+ @store.read('foo').should.be.nil
91
+ end
92
+ end
93
+
94
+ describe 'Disk' do
95
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
96
+ before do
97
+ @temp_dir = create_temp_directory
98
+ @store = Rack::Cache::EntityStore::Disk.new(@temp_dir)
99
+ end
100
+ after do
101
+ @store = nil
102
+ remove_entry_secure @temp_dir
103
+ end
104
+ it 'takes a path to ::new and creates the directory' do
105
+ path = @temp_dir + '/foo'
106
+ @store = Rack::Cache::EntityStore::Disk.new(path)
107
+ File.should.be.a.directory path
108
+ end
109
+ it 'spreads data over a 36² hash radius' do
110
+ (<<-PROSE).each { |line| @store.write(line).first.should.be.sha_like }
111
+ My wild love went riding,
112
+ She rode all the day;
113
+ She rode to the devil,
114
+ And asked him to pay.
115
+
116
+ The devil was wiser
117
+ It's time to repent;
118
+ He asked her to give back
119
+ The money she spent
120
+
121
+ My wild love went riding,
122
+ She rode to sea;
123
+ She gathered together
124
+ Some shells for her hair
125
+
126
+ She rode on to Christmas,
127
+ She rode to the farm;
128
+ She rode to Japan
129
+ And re-entered a town
130
+
131
+ My wild love is crazy
132
+ She screams like a bird;
133
+ She moans like a cat
134
+ When she wants to be heard
135
+
136
+ She rode and she rode on
137
+ She rode for a while,
138
+ Then stopped for an evening
139
+ And laid her head down
140
+
141
+ By this time the weather
142
+ Had changed one degree,
143
+ She asked for the people
144
+ To let her go free
145
+
146
+ My wild love went riding,
147
+ She rode for an hour;
148
+ She rode and she rested,
149
+ And then she rode on
150
+ My wild love went riding,
151
+ PROSE
152
+ subdirs = Dir["#{@temp_dir}/*"]
153
+ subdirs.each do |subdir|
154
+ File.basename(subdir).should.be =~ /^[0-9a-z]{2}$/
155
+ files = Dir["#{subdir}/*"]
156
+ files.each do |filename|
157
+ File.basename(filename).should.be =~ /^[0-9a-z]{38}$/
158
+ end
159
+ files.length.should.be > 0
160
+ end
161
+ subdirs.length.should.be == 28
162
+ end
163
+ end
164
+
165
+ need_memcached 'entity store tests' do
166
+ describe 'MemCache' do
167
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
168
+ before do
169
+ @store = Rack::Cache::EntityStore::MemCache.new($memcached)
170
+ end
171
+ after do
172
+ @store = nil
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,71 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/utils/environment_headers'
3
+
4
+ describe 'Rack::Utils::EnvironmentHeaders' do
5
+
6
+ before :each do
7
+ @now = Time.now.httpdate
8
+ @env = {
9
+ 'CONTENT_TYPE' => 'text/plain',
10
+ 'CONTENT_LENGTH' => '0x1A4',
11
+ 'HTTP_X_FOO' => 'BAR',
12
+ 'HTTP_IF_MODIFIED_SINCE' => @now,
13
+ 'rack.run_once' => true
14
+ }
15
+ @h = Rack::Utils::EnvironmentHeaders.new(@env)
16
+ end
17
+
18
+ after(:each) {
19
+ @env, @h = nil, nil
20
+ }
21
+
22
+ it 'retrieves headers with #[]' do
23
+ @h.should.respond_to :[]
24
+ @h['X-Foo'].should.be == 'BAR'
25
+ @h['If-Modified-Since'].should.be == @now
26
+ end
27
+
28
+ it 'sets headers with #[]=' do
29
+ @h.should.respond_to :[]=
30
+ @h['X-Foo'] = 'BAZZLE'
31
+ @h['X-Foo'].should.be == 'BAZZLE'
32
+ end
33
+
34
+ it 'sets values on the underlying environment hash' do
35
+ @h['X-Something-Else'] = 'FOO'
36
+ @env['HTTP_X_SOMETHING_ELSE'].should.be == 'FOO'
37
+ end
38
+
39
+ it 'handles Content-Type special case' do
40
+ @h['Content-Type'].should.be == 'text/plain'
41
+ end
42
+
43
+ it 'handles Content-Length special case' do
44
+ @h['Content-Length'].should.be == '0x1A4'
45
+ end
46
+
47
+ it 'implements #include? with RFC 2616 header name' do
48
+ @h.should.include 'If-Modified-Since'
49
+ end
50
+
51
+ it 'deletes underlying env entries' do
52
+ @h.delete('X-Foo')
53
+ @env.should.not.include? 'HTTP_X_FOO'
54
+ end
55
+
56
+ it 'returns the underlying environment hash with #to_env' do
57
+ @h.to_env.should.be @env
58
+ end
59
+
60
+ it 'iterates over all headers with #each' do
61
+ hash = {}
62
+ @h.each { |name,value| hash[name] = value }
63
+ hash.should.be == {
64
+ 'Content-Type' => 'text/plain',
65
+ 'Content-Length' => '0x1A4',
66
+ 'X-Foo' => 'BAR',
67
+ 'If-Modified-Since' => @now
68
+ }
69
+ end
70
+
71
+ end
@@ -0,0 +1,215 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+
3
+ class MockResponse < Rack::MockResponse
4
+ include Rack::Cache::Headers
5
+ include Rack::Cache::ResponseHeaders
6
+ public :now
7
+ end
8
+
9
+ describe 'Rack::Cache::Headers' do
10
+ before :each do
11
+ @now = Time.now
12
+ @res = MockResponse.new(200, {'Date' => @now.httpdate}, '')
13
+ @one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
14
+ end
15
+ after :each do
16
+ @now, @res, @one_hour_ago = nil
17
+ end
18
+
19
+ describe '#cache_control' do
20
+ it 'handles single name=value pair' do
21
+ @res.headers['Cache-Control'] = 'max-age=600'
22
+ @res.cache_control['max-age'].should.be == '600'
23
+ end
24
+ it 'handles multiple name=value pairs' do
25
+ @res.headers['Cache-Control'] = 'max-age=600, max-stale=300, min-fresh=570'
26
+ @res.cache_control['max-age'].should.be == '600'
27
+ @res.cache_control['max-stale'].should.be == '300'
28
+ @res.cache_control['min-fresh'].should.be == '570'
29
+ end
30
+ it 'handles a single flag value' do
31
+ @res.headers['Cache-Control'] = 'no-cache'
32
+ @res.cache_control.should.include 'no-cache'
33
+ @res.cache_control['no-cache'].should.be true
34
+ end
35
+ it 'handles a bunch of all kinds of stuff' do
36
+ @res.headers['Cache-Control'] = 'max-age=600,must-revalidate,min-fresh=3000,foo=bar,baz'
37
+ @res.cache_control['max-age'].should.be == '600'
38
+ @res.cache_control['must-revalidate'].should.be true
39
+ @res.cache_control['min-fresh'].should.be == '3000'
40
+ @res.cache_control['foo'].should.be == 'bar'
41
+ @res.cache_control['baz'].should.be true
42
+ end
43
+ it 'removes the header when given an empty hash' do
44
+ @res.headers['Cache-Control'] = 'max-age=600, must-revalidate'
45
+ @res.cache_control['max-age'].should.be == '600'
46
+ @res.cache_control = {}
47
+ @res.headers.should.not.include 'Cache-Control'
48
+ end
49
+ end
50
+ end
51
+
52
+ describe 'Rack::Cache::ResponseHeaders' do
53
+ before :each do
54
+ @now = Time.now
55
+ @one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
56
+ @one_hour_later = Time.httpdate((Time.now + (60**2)).httpdate)
57
+ @res = MockResponse.new(200, {'Date' => @now.httpdate}, '')
58
+ end
59
+ after :each do
60
+ @now, @res, @one_hour_ago = nil
61
+ end
62
+
63
+ describe '#validateable?' do
64
+ it 'is true when Last-Modified header present' do
65
+ @res = MockResponse.new(200, { 'Last-Modified' => @one_hour_ago.httpdate }, '')
66
+ @res.extend Rack::Cache::ResponseHeaders
67
+ @res.should.be.validateable
68
+ end
69
+ it 'is true when Etag header present' do
70
+ @res = MockResponse.new(200, { 'Etag' => '"12345"' }, '')
71
+ @res.extend Rack::Cache::ResponseHeaders
72
+ @res.should.be.validateable
73
+ end
74
+ it 'is false when no validator is present' do
75
+ @res = MockResponse.new(200, {}, '')
76
+ @res.extend Rack::Cache::ResponseHeaders
77
+ @res.should.not.be.validateable
78
+ end
79
+ end
80
+
81
+ describe '#date' do
82
+ it 'uses the Date header if present' do
83
+ @res = MockResponse.new(200, { 'Date' => @one_hour_ago.httpdate }, '')
84
+ @res.extend Rack::Cache::ResponseHeaders
85
+ @res.date.should.be == @one_hour_ago
86
+ end
87
+ it 'uses the current time when no Date header present' do
88
+ @res = MockResponse.new(200, {}, '')
89
+ @res.extend Rack::Cache::ResponseHeaders
90
+ @res.date.should.be.close Time.now, 1
91
+ end
92
+ end
93
+
94
+ describe '#expires_at' do
95
+ it 'returns #date + #max_age when Cache-Control/max-age is present' do
96
+ @res.headers['Cache-Control'] = 'max-age=500'
97
+ @res.expires_at.should.be == @res.date + 500
98
+ end
99
+ it 'uses the Expires header when present and no Cache-Control/max-age' do
100
+ @res.headers['Expires'] = @one_hour_ago.httpdate
101
+ @res.expires_at.should.be == @one_hour_ago
102
+ end
103
+ it 'returns nil when no Expires or Cache-Control provided' do
104
+ @res.expires_at.should.be nil
105
+ end
106
+ end
107
+
108
+ describe '#max_age' do
109
+ it 'uses Cache-Control to calculate #max_age when present' do
110
+ @res.headers['Cache-Control'] = 'max-age=600'
111
+ @res.max_age.should.be == 600
112
+ end
113
+ it 'uses Expires for #max_age if no Cache-Control max-age present' do
114
+ @res.headers['Cache-Control'] = 'must-revalidate'
115
+ @res.headers['Expires'] = @one_hour_later.httpdate
116
+ @res.max_age.should.be == 60 ** 2
117
+ end
118
+ it 'gives a #max_age of nil when no freshness information available' do
119
+ @res.max_age.should.be.nil
120
+ end
121
+ end
122
+
123
+ describe '#freshness_information?' do
124
+ it 'is true when Expires header is present' do
125
+ @res.headers['Expires'] = Time.now.httpdate
126
+ @res.freshness_information?.should.be true
127
+ end
128
+ it 'is true when a Cache-Control max-age directive is present' do
129
+ @res.headers['Cache-Control'] = 'max-age=500'
130
+ @res.freshness_information?.should.be true
131
+ end
132
+ it 'is not true otherwise' do
133
+ @res.freshness_information?.should.be false
134
+ end
135
+ end
136
+
137
+ describe '#no_cache?' do
138
+ it 'is true when a Cache-Control no-cache directive is present' do
139
+ @res.headers['Cache-Control'] = 'no-cache'
140
+ @res.no_cache?.should.be true
141
+ end
142
+ it 'is false otherwise' do
143
+ @res.no_cache?.should.be false
144
+ end
145
+ end
146
+
147
+ describe '#stale?' do
148
+ it 'is true when TTL cannot be established' do
149
+ @res.should.be.stale
150
+ end
151
+ it 'is false when the TTL is <= 0' do
152
+ @res.headers['Expires'] = (@res.now + 10).httpdate
153
+ @res.should.not.be.stale
154
+ end
155
+ it 'is true when the TTL is >= 0' do
156
+ @res.headers['Expires'] = (@res.now - 10).httpdate
157
+ @res.should.be.stale
158
+ end
159
+ end
160
+
161
+ describe '#ttl' do
162
+ it 'is nil when no Expires or Cache-Control headers present' do
163
+ @res.ttl.should.be.nil
164
+ end
165
+ it 'uses the Expires header when no max-age is present' do
166
+ @res.headers['Expires'] = (@res.now + (60**2)).httpdate
167
+ @res.ttl.should.be.close(60**2, 1)
168
+ end
169
+ it 'returns negative values when Expires is in part' do
170
+ @res.ttl.should.be.nil
171
+ @res.headers['Expires'] = @one_hour_ago.httpdate
172
+ @res.ttl.should.be < 0
173
+ end
174
+ it 'uses the Cache-Control max-age value when present' do
175
+ @res.headers['Cache-Control'] = 'max-age=60'
176
+ @res.ttl.should.be.close(60, 1)
177
+ end
178
+ end
179
+
180
+ describe '#vary' do
181
+ it 'is nil when no Vary header is present' do
182
+ @res.vary.should.be.nil
183
+ end
184
+ it 'returns the literal value of the Vary header' do
185
+ @res.headers['Vary'] = 'Foo Bar Baz'
186
+ @res.vary.should.be == 'Foo Bar Baz'
187
+ end
188
+ it 'can be checked for existence using the #vary? method' do
189
+ @res.should.respond_to :vary?
190
+ @res.should.not.vary
191
+ @res.headers['Vary'] = '*'
192
+ @res.should.vary
193
+ end
194
+ end
195
+
196
+ describe '#vary_header_names' do
197
+ it 'returns an empty Array when no Vary header is present' do
198
+ @res.vary_header_names.should.be.empty
199
+ end
200
+ it 'parses a single header name value' do
201
+ @res.headers['Vary'] = 'Accept-Language'
202
+ @res.vary_header_names.should.be == ['Accept-Language']
203
+ end
204
+ it 'parses multiple header name values separated by spaces' do
205
+ @res.headers['Vary'] = 'Accept-Language User-Agent X-Foo'
206
+ @res.vary_header_names.should.be ==
207
+ ['Accept-Language', 'User-Agent', 'X-Foo']
208
+ end
209
+ it 'parses multiple header name values separated by commas' do
210
+ @res.headers['Vary'] = 'Accept-Language,User-Agent, X-Foo'
211
+ @res.vary_header_names.should.be ==
212
+ ['Accept-Language', 'User-Agent', 'X-Foo']
213
+ end
214
+ end
215
+ end