rtomayko-rack-cache 0.3.0 → 0.3.9
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +41 -0
- data/README +0 -1
- data/TODO +14 -10
- data/doc/configuration.markdown +7 -153
- data/doc/index.markdown +1 -3
- 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 +188 -51
- 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 +29 -13
- 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 -14
- data/test/cache_test.rb +4 -1
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +198 -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 +6 -0
- metadata +13 -19
- 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
|