rack-cache 0.3.0 → 0.4
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.
- data/CHANGES +43 -0
- data/README +18 -9
- data/Rakefile +1 -14
- data/TODO +13 -14
- data/doc/configuration.markdown +7 -153
- data/doc/faq.markdown +8 -0
- data/doc/index.markdown +7 -9
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +5 -11
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +190 -52
- data/lib/rack/cache/entitystore.rb +10 -4
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +52 -16
- data/lib/rack/cache/options.rb +60 -39
- data/lib/rack/cache/request.rb +11 -15
- data/lib/rack/cache/response.rb +221 -30
- data/lib/rack/cache/storage.rb +1 -2
- data/rack-cache.gemspec +9 -15
- data/test/cache_test.rb +9 -6
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +251 -169
- data/test/entitystore_test.rb +12 -11
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +57 -14
- data/test/options_test.rb +11 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +164 -23
- data/test/spec_setup.rb +7 -0
- metadata +12 -20
- data/doc/events.dot +0 -27
- data/lib/rack/cache/config.rb +0 -65
- data/lib/rack/cache/config/busters.rb +0 -16
- data/lib/rack/cache/config/default.rb +0 -133
- data/lib/rack/cache/config/no-cache.rb +0 -13
- data/lib/rack/cache/core.rb +0 -299
- data/lib/rack/cache/headers.rb +0 -325
- data/lib/rack/utils/environment_headers.rb +0 -78
- data/test/config_test.rb +0 -66
- data/test/core_test.rb +0 -84
- data/test/environment_headers_test.rb +0 -69
- data/test/headers_test.rb +0 -298
- data/test/logging_test.rb +0 -45
data/lib/rack/cache/headers.rb
DELETED
@@ -1,325 +0,0 @@
|
|
1
|
-
require 'set'
|
2
|
-
require 'rack/utils/environment_headers'
|
3
|
-
|
4
|
-
module Rack::Cache
|
5
|
-
# Generic HTTP header helper methods. Provides access to headers that can be
|
6
|
-
# included in requests and responses. This can be mixed into any object that
|
7
|
-
# responds to #headers by returning a Hash.
|
8
|
-
|
9
|
-
module Headers
|
10
|
-
# Determine if any of the header names exist:
|
11
|
-
# if header?('Authorization', 'Cookie')
|
12
|
-
# ...
|
13
|
-
# end
|
14
|
-
def header?(*names)
|
15
|
-
names.any? { |name| headers.include?(name) }
|
16
|
-
end
|
17
|
-
|
18
|
-
# A Hash of name=value pairs that correspond to the Cache-Control header.
|
19
|
-
# Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
|
20
|
-
# of true. This method always returns a Hash, empty if no Cache-Control
|
21
|
-
# header is present.
|
22
|
-
def cache_control
|
23
|
-
@cache_control ||=
|
24
|
-
headers['Cache-Control'].to_s.split(/\s*[,;]\s*/).inject({}) {|hash,token|
|
25
|
-
name, value = token.split(/\s*=\s*/, 2)
|
26
|
-
hash[name.downcase] = (value || true) unless name.empty?
|
27
|
-
hash
|
28
|
-
}.freeze
|
29
|
-
end
|
30
|
-
|
31
|
-
# Set the Cache-Control header to the values specified by the Hash. See
|
32
|
-
# the #cache_control method for information on expected Hash structure.
|
33
|
-
def cache_control=(hash)
|
34
|
-
value =
|
35
|
-
hash.collect { |key,value|
|
36
|
-
next nil unless value
|
37
|
-
next key if value == true
|
38
|
-
"#{key}=#{value}"
|
39
|
-
}.compact.join(', ')
|
40
|
-
if value.empty?
|
41
|
-
headers.delete('Cache-Control')
|
42
|
-
@cache_control = {}
|
43
|
-
else
|
44
|
-
headers['Cache-Control'] = value
|
45
|
-
@cache_control = hash.dup.freeze
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# The literal value of the ETag HTTP header or nil if no ETag is specified.
|
50
|
-
def etag
|
51
|
-
headers['Etag']
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# HTTP request header helpers. When included in Rack::Cache::Request, headers
|
56
|
-
# may be accessed by their standard RFC 2616 names using the #headers Hash.
|
57
|
-
module RequestHeaders
|
58
|
-
include Rack::Cache::Headers
|
59
|
-
|
60
|
-
# A Hash-like object providing access to HTTP request headers.
|
61
|
-
def headers
|
62
|
-
@headers ||= Rack::Utils::EnvironmentHeaders.new(env)
|
63
|
-
end
|
64
|
-
|
65
|
-
# The literal value of the If-Modified-Since request header or nil when
|
66
|
-
# no If-Modified-Since header is present.
|
67
|
-
def if_modified_since
|
68
|
-
headers['If-Modified-Since']
|
69
|
-
end
|
70
|
-
|
71
|
-
# The literal value of the If-None-Match request header or nil when
|
72
|
-
# no If-None-Match header is present.
|
73
|
-
def if_none_match
|
74
|
-
headers['If-None-Match']
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# HTTP response header helper methods.
|
79
|
-
module ResponseHeaders
|
80
|
-
include Rack::Cache::Headers
|
81
|
-
|
82
|
-
# Status codes of responses that MAY be stored by a cache or used in reply
|
83
|
-
# to a subsequent request.
|
84
|
-
#
|
85
|
-
# http://tools.ietf.org/html/rfc2616#section-13.4
|
86
|
-
CACHEABLE_RESPONSE_CODES = [
|
87
|
-
200, # OK
|
88
|
-
203, # Non-Authoritative Information
|
89
|
-
300, # Multiple Choices
|
90
|
-
301, # Moved Permanently
|
91
|
-
302, # Found
|
92
|
-
404, # Not Found
|
93
|
-
410 # Gone
|
94
|
-
].to_set
|
95
|
-
|
96
|
-
# Determine if the response is "fresh". Fresh responses may be served from
|
97
|
-
# cache without any interaction with the origin. A response is considered
|
98
|
-
# fresh when it includes a Cache-Control/max-age indicator or Expiration
|
99
|
-
# header and the calculated age is less than the freshness lifetime.
|
100
|
-
def fresh?
|
101
|
-
ttl && ttl > 0
|
102
|
-
end
|
103
|
-
|
104
|
-
# Determine if the response is "stale". Stale responses must be validated
|
105
|
-
# with the origin before use. This is the inverse of #fresh?.
|
106
|
-
def stale?
|
107
|
-
!fresh?
|
108
|
-
end
|
109
|
-
|
110
|
-
# Determine if the response is worth caching under any circumstance. Responses
|
111
|
-
# marked "private" with an explicit Cache-Control directive are considered
|
112
|
-
# uncacheable
|
113
|
-
#
|
114
|
-
# Responses with neither a freshness lifetime (Expires, max-age) nor cache
|
115
|
-
# validator (Last-Modified, Etag) are considered uncacheable.
|
116
|
-
def cacheable?
|
117
|
-
return false unless CACHEABLE_RESPONSE_CODES.include?(status)
|
118
|
-
return false if no_store? || private?
|
119
|
-
validateable? || fresh?
|
120
|
-
end
|
121
|
-
|
122
|
-
# The response includes specific information about its freshness. True when
|
123
|
-
# a +Cache-Control+ header with +max-age+ value is present or when the
|
124
|
-
# +Expires+ header is set.
|
125
|
-
def freshness_information?
|
126
|
-
header?('Expires') ||
|
127
|
-
!!(cache_control['s-maxage'] || cache_control['max-age'])
|
128
|
-
end
|
129
|
-
|
130
|
-
# Determine if the response includes headers that can be used to validate
|
131
|
-
# the response with the origin using a conditional GET request.
|
132
|
-
def validateable?
|
133
|
-
header?('Last-Modified') || header?('Etag')
|
134
|
-
end
|
135
|
-
|
136
|
-
# Indicates that the response should not be served from cache without first
|
137
|
-
# revalidating with the origin. Note that this does not necessary imply that
|
138
|
-
# a caching agent ought not store the response in its cache.
|
139
|
-
def no_cache?
|
140
|
-
cache_control['no-cache']
|
141
|
-
end
|
142
|
-
|
143
|
-
# Indicates that the response should not be stored under any circumstances.
|
144
|
-
def no_store?
|
145
|
-
cache_control['no-store']
|
146
|
-
end
|
147
|
-
|
148
|
-
# True when the response has been explicitly marked "public".
|
149
|
-
def public?
|
150
|
-
cache_control['public']
|
151
|
-
end
|
152
|
-
|
153
|
-
# Mark the response "public", making it eligible for other clients. Note
|
154
|
-
# that responses are considered "public" by default unless the request
|
155
|
-
# includes private headers (Authorization, Cookie).
|
156
|
-
def public=(value)
|
157
|
-
value = value ? true : nil
|
158
|
-
self.cache_control = cache_control.
|
159
|
-
merge('public' => value, 'private' => !value)
|
160
|
-
end
|
161
|
-
|
162
|
-
# True when the response has been marked "private" explicitly.
|
163
|
-
def private?
|
164
|
-
cache_control['private']
|
165
|
-
end
|
166
|
-
|
167
|
-
# Mark the response "private", making it ineligible for serving other
|
168
|
-
# clients.
|
169
|
-
def private=(value)
|
170
|
-
value = value ? true : nil
|
171
|
-
self.cache_control = cache_control.
|
172
|
-
merge('public' => !value, 'private' => value)
|
173
|
-
end
|
174
|
-
|
175
|
-
# Indicates that the cache must not serve a stale response in any
|
176
|
-
# circumstance without first revalidating with the origin. When present,
|
177
|
-
# the TTL of the response should not be overriden to be greater than the
|
178
|
-
# value provided by the origin.
|
179
|
-
def must_revalidate?
|
180
|
-
cache_control['must-revalidate'] ||
|
181
|
-
cache_control['proxy-revalidate']
|
182
|
-
end
|
183
|
-
|
184
|
-
# The date, as specified by the Date header. When no Date header is present,
|
185
|
-
# set the Date header to Time.now and return.
|
186
|
-
def date
|
187
|
-
if date = headers['Date']
|
188
|
-
Time.httpdate(date)
|
189
|
-
else
|
190
|
-
headers['Date'] = now.httpdate unless headers.frozen?
|
191
|
-
now
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
# The age of the response.
|
196
|
-
def age
|
197
|
-
[(now - date).to_i, 0].max
|
198
|
-
end
|
199
|
-
|
200
|
-
# The number of seconds after the time specified in the response's Date
|
201
|
-
# header when the the response should no longer be considered fresh. First
|
202
|
-
# check for a s-maxage directive, then a max-age directive, and then fall
|
203
|
-
# back on an expires header; return nil when no maximum age can be
|
204
|
-
# established.
|
205
|
-
def max_age
|
206
|
-
if age = (cache_control['s-maxage'] || cache_control['max-age'])
|
207
|
-
age.to_i
|
208
|
-
elsif headers['Expires']
|
209
|
-
Time.httpdate(headers['Expires']) - date
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
# The number of seconds after which the response should no longer
|
214
|
-
# be considered fresh. Sets the Cache-Control max-age directive.
|
215
|
-
def max_age=(value)
|
216
|
-
self.cache_control = cache_control.merge('max-age' => value.to_s)
|
217
|
-
end
|
218
|
-
|
219
|
-
# Like #max_age= but sets the s-maxage directive, which applies only
|
220
|
-
# to shared caches.
|
221
|
-
def shared_max_age=(value)
|
222
|
-
self.cache_control = cache_control.merge('s-maxage' => value.to_s)
|
223
|
-
end
|
224
|
-
|
225
|
-
# The Time when the response should be considered stale. With a
|
226
|
-
# Cache-Control/max-age value is present, this is calculated by adding the
|
227
|
-
# number of seconds specified to the responses #date value. Falls back to
|
228
|
-
# the time specified in the Expires header or returns nil if neither is
|
229
|
-
# present.
|
230
|
-
def expires_at
|
231
|
-
if max_age = (cache_control['s-maxage'] || cache_control['max-age'])
|
232
|
-
date + max_age.to_i
|
233
|
-
elsif time = headers['Expires']
|
234
|
-
Time.httpdate(time)
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
# The response's time-to-live in seconds, or nil when no freshness
|
239
|
-
# information is present in the response. When the responses #ttl
|
240
|
-
# is <= 0, the response may not be served from cache without first
|
241
|
-
# revalidating with the origin.
|
242
|
-
def ttl
|
243
|
-
max_age - age if max_age
|
244
|
-
end
|
245
|
-
|
246
|
-
# Set the response's time-to-live for shared caches to the specified number
|
247
|
-
# of seconds. This adjusts the Cache-Control/s-maxage directive.
|
248
|
-
def ttl=(seconds)
|
249
|
-
self.shared_max_age = age + seconds
|
250
|
-
end
|
251
|
-
|
252
|
-
# Set the response's time-to-live for private/client caches. This adjusts
|
253
|
-
# the Cache-Control/max-age directive.
|
254
|
-
def client_ttl=(seconds)
|
255
|
-
self.max_age = age + seconds
|
256
|
-
end
|
257
|
-
|
258
|
-
# The String value of the Last-Modified header exactly as it appears
|
259
|
-
# in the response (i.e., no date parsing / conversion is performed).
|
260
|
-
def last_modified
|
261
|
-
headers['Last-Modified']
|
262
|
-
end
|
263
|
-
|
264
|
-
# Determine if the response was last modified at the time provided.
|
265
|
-
# time_value is the exact string provided in an origin response's
|
266
|
-
# Last-Modified header.
|
267
|
-
def last_modified_at?(time_value)
|
268
|
-
time_value && last_modified == time_value
|
269
|
-
end
|
270
|
-
|
271
|
-
# Determine if response's ETag matches the etag value provided. Return
|
272
|
-
# false when either value is nil.
|
273
|
-
def etag_matches?(etag)
|
274
|
-
etag && self.etag == etag
|
275
|
-
end
|
276
|
-
|
277
|
-
# Headers that MUST NOT be included with 304 Not Modified responses.
|
278
|
-
#
|
279
|
-
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
280
|
-
NOT_MODIFIED_OMIT_HEADERS = %w[
|
281
|
-
Allow
|
282
|
-
Content-Encoding
|
283
|
-
Content-Language
|
284
|
-
Content-Length
|
285
|
-
Content-Md5
|
286
|
-
Content-Type
|
287
|
-
Last-Modified
|
288
|
-
].to_set
|
289
|
-
|
290
|
-
# Modify the response so that it conforms to the rules defined for
|
291
|
-
# '304 Not Modified'. This sets the status, removes the body, and
|
292
|
-
# discards any headers that MUST NOT be included in 304 responses.
|
293
|
-
#
|
294
|
-
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
295
|
-
def not_modified!
|
296
|
-
self.status = 304
|
297
|
-
self.body = []
|
298
|
-
NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
|
299
|
-
nil
|
300
|
-
end
|
301
|
-
|
302
|
-
# The literal value of the Vary header, or nil when no header is present.
|
303
|
-
def vary
|
304
|
-
headers['Vary']
|
305
|
-
end
|
306
|
-
|
307
|
-
# Does the response include a Vary header?
|
308
|
-
def vary?
|
309
|
-
! vary.nil?
|
310
|
-
end
|
311
|
-
|
312
|
-
# An array of header names given in the Vary header or an empty
|
313
|
-
# array when no Vary header is present.
|
314
|
-
def vary_header_names
|
315
|
-
return [] unless vary = headers['Vary']
|
316
|
-
vary.split(/[\s,]+/)
|
317
|
-
end
|
318
|
-
|
319
|
-
private
|
320
|
-
def now
|
321
|
-
@now ||= Time.now
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
require 'rack/utils'
|
2
|
-
|
3
|
-
module Rack::Utils #:nodoc:
|
4
|
-
# A facade over a Rack Environment Hash that gives access to headers
|
5
|
-
# using their normal RFC 2616 names.
|
6
|
-
|
7
|
-
class EnvironmentHeaders
|
8
|
-
include Enumerable
|
9
|
-
|
10
|
-
# Create the facade over the given Rack Environment Hash.
|
11
|
-
def initialize(env)
|
12
|
-
@env = env
|
13
|
-
end
|
14
|
-
|
15
|
-
# Return the value of the specified header. The +header_name+ should
|
16
|
-
# be as specified by RFC 2616 (e.g., "Content-Type", "Accept", etc.)
|
17
|
-
def [](header_name)
|
18
|
-
@env[env_name(header_name)]
|
19
|
-
end
|
20
|
-
|
21
|
-
# Set the value of the specified header. The +header_name+ should
|
22
|
-
# be as specified by RFC 2616 (e.g., "Content-Type", "Accept", etc.)
|
23
|
-
def []=(header_name, value)
|
24
|
-
@env[env_name(header_name)] = value
|
25
|
-
end
|
26
|
-
|
27
|
-
# Determine if the underlying Rack Environment includes a header
|
28
|
-
# of the given name.
|
29
|
-
def include?(header_name)
|
30
|
-
@env.include?(env_name(header_name))
|
31
|
-
end
|
32
|
-
|
33
|
-
# Iterate over all headers yielding a (name, value) tuple to the
|
34
|
-
# block. Rack Environment keys that do not map to an header are not
|
35
|
-
# included.
|
36
|
-
def each
|
37
|
-
@env.each do |key,value|
|
38
|
-
next unless key =~ /^(HTTP_|CONTENT_)/
|
39
|
-
yield header_name(key), value
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# Delete the entry in the underlying Rack Environment that corresponds
|
44
|
-
# to the given RFC 2616 header name.
|
45
|
-
def delete(header_name)
|
46
|
-
@env.delete(env_name(header_name))
|
47
|
-
end
|
48
|
-
|
49
|
-
# Return the underlying Rack Environment Hash.
|
50
|
-
def to_env
|
51
|
-
@env
|
52
|
-
end
|
53
|
-
|
54
|
-
alias_method :to_hash, :to_env
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
# Return the Rack Environment key for the given RFC 2616 header name.
|
59
|
-
def env_name(header_name)
|
60
|
-
case header_name = header_name.upcase
|
61
|
-
when 'CONTENT-TYPE' then 'CONTENT_TYPE'
|
62
|
-
when 'CONTENT-LENGTH' then 'CONTENT_LENGTH'
|
63
|
-
else "HTTP_#{header_name.tr('-', '_')}"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Return the RFC 2616 header name for the given Rack Environment key.
|
68
|
-
def header_name(env_name)
|
69
|
-
env_name.
|
70
|
-
sub(/^HTTP_/, '').
|
71
|
-
downcase.
|
72
|
-
capitalize.
|
73
|
-
gsub(/_(.)/) { '-' + $1.upcase }
|
74
|
-
end
|
75
|
-
|
76
|
-
end
|
77
|
-
|
78
|
-
end
|
data/test/config_test.rb
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
-
require 'rack/cache/config'
|
3
|
-
|
4
|
-
class MockConfig
|
5
|
-
include Rack::Cache::Config
|
6
|
-
def configured!
|
7
|
-
@configured = true
|
8
|
-
end
|
9
|
-
def configured?
|
10
|
-
@configured
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
describe 'Rack::Cache::Config' do
|
15
|
-
before :each do
|
16
|
-
@config = MockConfig.new
|
17
|
-
@tempdir = create_temp_directory
|
18
|
-
$:.unshift @tempdir
|
19
|
-
end
|
20
|
-
after :each do
|
21
|
-
@config = nil
|
22
|
-
$:.shift if $:.first == @tempdir
|
23
|
-
remove_entry_secure @tempdir
|
24
|
-
end
|
25
|
-
|
26
|
-
def make_temp_file(filename, data='configured!')
|
27
|
-
create_temp_file @tempdir, filename, data
|
28
|
-
end
|
29
|
-
|
30
|
-
it 'loads config files from the load path when file is relative' do
|
31
|
-
make_temp_file 'foo/bar.rb'
|
32
|
-
@config.import 'foo/bar.rb'
|
33
|
-
@config.should.be.configured
|
34
|
-
end
|
35
|
-
it 'assumes a .rb file extension when no file extension exists' do
|
36
|
-
make_temp_file 'foo/bar.rb'
|
37
|
-
@config.import 'foo/bar'
|
38
|
-
@config.should.be.configured
|
39
|
-
end
|
40
|
-
it 'does not assume a .rb file extension when other file extension exists' do
|
41
|
-
make_temp_file 'foo/bar.conf'
|
42
|
-
@config.import 'foo/bar.conf'
|
43
|
-
@config.should.be.configured
|
44
|
-
end
|
45
|
-
it 'should locate files with absolute path names' do
|
46
|
-
make_temp_file 'foo/bar.rb'
|
47
|
-
@config.import File.join(@tempdir, 'foo/bar.rb')
|
48
|
-
@config.should.be.configured
|
49
|
-
end
|
50
|
-
it 'raises a LoadError when the file cannot be found' do
|
51
|
-
assert_raises(LoadError) {
|
52
|
-
@config.import('this/file/is/very-likely/not/to/exist.rb')
|
53
|
-
}
|
54
|
-
end
|
55
|
-
it 'executes within the context of the object instance' do
|
56
|
-
make_temp_file 'foo/bar.rb',
|
57
|
-
'self.should.be.kind_of Rack::Cache::Config ; configured!'
|
58
|
-
@config.import 'foo/bar'
|
59
|
-
@config.should.be.configured
|
60
|
-
end
|
61
|
-
it 'does not import files more than once' do
|
62
|
-
make_temp_file 'foo/bar.rb', "import 'foo/bar'"
|
63
|
-
@config.import('foo/bar').should.be true
|
64
|
-
@config.import('foo/bar').should.be false
|
65
|
-
end
|
66
|
-
end
|