rtomayko-rack-cache 0.3.0 → 0.3.9

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.
@@ -1,17 +1,17 @@
1
- require 'rack'
1
+ require 'rack/cache/key'
2
2
  require 'rack/cache/storage'
3
3
 
4
4
  module Rack::Cache
5
+
5
6
  # Configuration options and utility methods for option access. Rack::Cache
6
7
  # uses the Rack Environment to store option values. All options documented
7
8
  # below are stored in the Rack Environment as "rack-cache.<option>", where
8
9
  # <option> is the option name.
9
10
  #
10
- # The #set method can be used within an event or a top-level configuration
11
- # block to configure a option values. When #set is called at the top-level,
12
- # the value applies to all requests; when called from within an event, the
13
- # values applies only to the request being processed.
14
-
11
+ # The #set method can be used to configure a option values. When #set is
12
+ # called outside of request scope, the value applies to all requests; when
13
+ # called from within a request context, applies only to the request being
14
+ # processed.
15
15
  module Options
16
16
  class << self
17
17
  private
@@ -44,6 +44,19 @@ module Rack::Cache
44
44
  # recommended.
45
45
  option_accessor :metastore
46
46
 
47
+ # A custom cache key generator, which can be anything that responds to :call.
48
+ # By default, this is the Rack::Cache::Key class, but you can implement your
49
+ # own generator. A cache key generator gets passed a request and generates the
50
+ # appropriate cache key.
51
+ #
52
+ # In addition to setting the generator to an object, you can just pass a block
53
+ # instead, which will act as the cache key generator:
54
+ #
55
+ # set :cache_key do |request|
56
+ # request.fullpath.replace(/\//, '-')
57
+ # end
58
+ option_accessor :cache_key
59
+
47
60
  # A URI specifying the entity-store implement that should be used to store
48
61
  # response bodies. See the metastore option for information on supported URI
49
62
  # schemes.
@@ -88,8 +101,10 @@ module Rack::Cache
88
101
  # exactly as specified. The +option+ argument may also be a Hash in
89
102
  # which case each key/value pair is merged into the environment as if
90
103
  # the #set method were called on each.
91
- def set(option, value=self)
92
- if value == self
104
+ def set(option, value=self, &block)
105
+ if block_given?
106
+ write_option option, block
107
+ elsif value == self
93
108
  self.options = option.to_hash
94
109
  else
95
110
  write_option option, value
@@ -116,11 +131,12 @@ module Rack::Cache
116
131
  private
117
132
  def initialize_options(options={})
118
133
  @default_options = {
119
- 'rack-cache.verbose' => true,
120
- 'rack-cache.storage' => Rack::Cache::Storage.instance,
121
- 'rack-cache.metastore' => 'heap:/',
122
- 'rack-cache.entitystore' => 'heap:/',
123
- 'rack-cache.default_ttl' => 0,
134
+ 'rack-cache.cache_key' => Key,
135
+ 'rack-cache.verbose' => true,
136
+ 'rack-cache.storage' => Rack::Cache::Storage.instance,
137
+ 'rack-cache.metastore' => 'heap:/',
138
+ 'rack-cache.entitystore' => 'heap:/',
139
+ 'rack-cache.default_ttl' => 0,
124
140
  'rack-cache.private_headers' => ['Authorization', 'Cookie']
125
141
  }
126
142
  self.options = options
@@ -1,19 +1,15 @@
1
1
  require 'rack/request'
2
- require 'rack/cache/headers'
3
- require 'rack/utils/environment_headers'
2
+ require 'rack/cache/cachecontrol'
4
3
 
5
4
  module Rack::Cache
5
+
6
6
  # Provides access to the HTTP request. The +request+ and +original_request+
7
7
  # objects exposed by the Core caching engine are instances of this class.
8
8
  #
9
9
  # Request objects respond to a variety of convenience methods, including
10
10
  # everything defined by Rack::Request as well as the Headers and
11
11
  # RequestHeaders modules.
12
-
13
12
  class Request < Rack::Request
14
- include Rack::Cache::Headers
15
- include Rack::Cache::RequestHeaders
16
-
17
13
  # The HTTP request method. This is the standard implementation of this
18
14
  # method but is respecified here due to libraries that attempt to modify
19
15
  # the behavior to respect POST tunnel method specifiers. We always want
@@ -22,16 +18,16 @@ module Rack::Cache
22
18
  @env['REQUEST_METHOD']
23
19
  end
24
20
 
25
- # Determine if the request's method matches any of the values
26
- # provided:
27
- # if request.request_method?('GET', 'POST')
28
- # ...
29
- # end
30
- def request_method?(*methods)
31
- method = request_method
32
- methods.any? { |test| test.to_s.upcase == method }
21
+ # A CacheControl instance based on the request's Cache-Control header.
22
+ def cache_control
23
+ @cache_control ||= CacheControl.new(env['HTTP_CACHE_CONTROL'])
33
24
  end
34
25
 
35
- alias_method :method?, :request_method?
26
+ # True when the Cache-Control/no-cache directive is present or the
27
+ # Pragma header is set to no-cache.
28
+ def no_cache?
29
+ cache_control['no-cache'] ||
30
+ env['HTTP_PRAGMA'] == 'no-cache'
31
+ end
36
32
  end
37
33
  end
@@ -1,7 +1,11 @@
1
+ require 'time'
1
2
  require 'set'
2
- require 'rack/cache/headers'
3
+ require 'rack/response'
4
+ require 'rack/utils'
5
+ require 'rack/cache/cachecontrol'
3
6
 
4
7
  module Rack::Cache
8
+
5
9
  # Provides access to the response generated by the downstream application. The
6
10
  # +response+, +original_response+, and +entry+ objects exposed by the Core
7
11
  # caching engine are instances of this class.
@@ -14,21 +18,14 @@ module Rack::Cache
14
18
  # not perform many of the same initialization and finalization tasks. For
15
19
  # example, the body is not slurped during initialization and there are no
16
20
  # facilities for generating response output.
17
-
18
21
  class Response
19
22
  include Rack::Response::Helpers
20
- include Rack::Cache::Headers
21
- include Rack::Cache::ResponseHeaders
22
23
 
23
- # The response's status code (integer).
24
- attr_accessor :status
24
+ # Rack response tuple accessors.
25
+ attr_accessor :status, :headers, :body
25
26
 
26
- # The response body. See the Rack spec for information on the behavior
27
- # required by this object.
28
- attr_accessor :body
29
-
30
- # The response headers.
31
- attr_reader :headers
27
+ # The time when the Response object was instantiated.
28
+ attr_reader :now
32
29
 
33
30
  # Create a Response instance given the response status code, header hash,
34
31
  # and body.
@@ -37,7 +34,7 @@ module Rack::Cache
37
34
  @headers = Rack::Utils::HeaderHash.new(headers)
38
35
  @body = body
39
36
  @now = Time.now
40
- @headers['Date'] ||= now.httpdate
37
+ @headers['Date'] ||= @now.httpdate
41
38
  end
42
39
 
43
40
  def initialize_copy(other)
@@ -45,32 +42,226 @@ module Rack::Cache
45
42
  @headers = other.headers.dup
46
43
  end
47
44
 
48
- # Return the value of the named response header.
49
- def [](header_name)
50
- headers[header_name]
45
+ # Return the status, headers, and body in a three-tuple.
46
+ def to_a
47
+ [status, headers.to_hash, body]
51
48
  end
52
49
 
53
- # Set a response header value.
54
- def []=(header_name, header_value)
55
- headers[header_name] = header_value
50
+ # Status codes of responses that MAY be stored by a cache or used in reply
51
+ # to a subsequent request.
52
+ #
53
+ # http://tools.ietf.org/html/rfc2616#section-13.4
54
+ CACHEABLE_RESPONSE_CODES = [
55
+ 200, # OK
56
+ 203, # Non-Authoritative Information
57
+ 300, # Multiple Choices
58
+ 301, # Moved Permanently
59
+ 302, # Found
60
+ 404, # Not Found
61
+ 410 # Gone
62
+ ].to_set
63
+
64
+ # A Hash of name=value pairs that correspond to the Cache-Control header.
65
+ # Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
66
+ # of true. This method always returns a Hash, empty if no Cache-Control
67
+ # header is present.
68
+ def cache_control
69
+ @cache_control ||= CacheControl.new(headers['Cache-Control'])
56
70
  end
57
71
 
58
- # Called immediately after an object is loaded from the cache.
59
- def activate!
60
- headers['Age'] = age.to_i.to_s
72
+ # Set the Cache-Control header to the values specified by the Hash. See
73
+ # the #cache_control method for information on expected Hash structure.
74
+ def cache_control=(value)
75
+ if value.respond_to? :to_hash
76
+ cache_control.clear
77
+ cache_control.merge!(value)
78
+ value = cache_control.to_s
79
+ end
80
+
81
+ if value.nil? || value.empty?
82
+ headers.delete('Cache-Control')
83
+ else
84
+ headers['Cache-Control'] = value
85
+ end
61
86
  end
62
87
 
63
- # Return the status, headers, and body in a three-tuple.
64
- def to_a
65
- [status, headers.to_hash, body]
88
+ # Determine if the response is "fresh". Fresh responses may be served from
89
+ # cache without any interaction with the origin. A response is considered
90
+ # fresh when it includes a Cache-Control/max-age indicator or Expiration
91
+ # header and the calculated age is less than the freshness lifetime.
92
+ def fresh?
93
+ ttl && ttl > 0
66
94
  end
67
95
 
68
- # Freezes
69
- def freeze
70
- @headers.freeze
71
- super
96
+ # Determine if the response is worth caching under any circumstance. Responses
97
+ # marked "private" with an explicit Cache-Control directive are considered
98
+ # uncacheable
99
+ #
100
+ # Responses with neither a freshness lifetime (Expires, max-age) nor cache
101
+ # validator (Last-Modified, ETag) are considered uncacheable.
102
+ def cacheable?
103
+ return false unless CACHEABLE_RESPONSE_CODES.include?(status)
104
+ return false if cache_control.no_store? || cache_control.private?
105
+ validateable? || fresh?
72
106
  end
73
107
 
74
- end
108
+ # Determine if the response includes headers that can be used to validate
109
+ # the response with the origin using a conditional GET request.
110
+ def validateable?
111
+ headers.key?('Last-Modified') || headers.key?('ETag')
112
+ end
113
+
114
+ # Mark the response "private", making it ineligible for serving other
115
+ # clients.
116
+ def private=(value)
117
+ value = value ? true : nil
118
+ self.cache_control = cache_control.
119
+ merge('public' => !value, 'private' => value)
120
+ end
121
+
122
+ # Indicates that the cache must not serve a stale response in any
123
+ # circumstance without first revalidating with the origin. When present,
124
+ # the TTL of the response should not be overriden to be greater than the
125
+ # value provided by the origin.
126
+ def must_revalidate?
127
+ cache_control.must_revalidate || cache_control.proxy_revalidate
128
+ end
129
+
130
+ # Mark the response stale by setting the Age header to be equal to the
131
+ # maximum age of the response.
132
+ def expire!
133
+ headers['Age'] = max_age.to_s if fresh?
134
+ end
135
+
136
+ # The date, as specified by the Date header. When no Date header is present,
137
+ # set the Date header to Time.now and return.
138
+ def date
139
+ if date = headers['Date']
140
+ Time.httpdate(date)
141
+ else
142
+ headers['Date'] = now.httpdate unless headers.frozen?
143
+ now
144
+ end
145
+ end
146
+
147
+ # The age of the response.
148
+ def age
149
+ (headers['Age'] || [(now - date).to_i, 0].max).to_i
150
+ end
151
+
152
+ # The number of seconds after the time specified in the response's Date
153
+ # header when the the response should no longer be considered fresh. First
154
+ # check for a s-maxage directive, then a max-age directive, and then fall
155
+ # back on an expires header; return nil when no maximum age can be
156
+ # established.
157
+ def max_age
158
+ cache_control.shared_max_age ||
159
+ cache_control.max_age ||
160
+ (expires && (expires - date))
161
+ end
162
+
163
+ # The value of the Expires header as a Time object.
164
+ def expires
165
+ headers['Expires'] && Time.httpdate(headers['Expires'])
166
+ end
75
167
 
168
+ # The number of seconds after which the response should no longer
169
+ # be considered fresh. Sets the Cache-Control max-age directive.
170
+ def max_age=(value)
171
+ self.cache_control = cache_control.merge('max-age' => value.to_s)
172
+ end
173
+
174
+ # Like #max_age= but sets the s-maxage directive, which applies only
175
+ # to shared caches.
176
+ def shared_max_age=(value)
177
+ self.cache_control = cache_control.merge('s-maxage' => value.to_s)
178
+ end
179
+
180
+ # The response's time-to-live in seconds, or nil when no freshness
181
+ # information is present in the response. When the responses #ttl
182
+ # is <= 0, the response may not be served from cache without first
183
+ # revalidating with the origin.
184
+ def ttl
185
+ max_age - age if max_age
186
+ end
187
+
188
+ # Set the response's time-to-live for shared caches to the specified number
189
+ # of seconds. This adjusts the Cache-Control/s-maxage directive.
190
+ def ttl=(seconds)
191
+ self.shared_max_age = age + seconds
192
+ end
193
+
194
+ # Set the response's time-to-live for private/client caches. This adjusts
195
+ # the Cache-Control/max-age directive.
196
+ def client_ttl=(seconds)
197
+ self.max_age = age + seconds
198
+ end
199
+
200
+ # The String value of the Last-Modified header exactly as it appears
201
+ # in the response (i.e., no date parsing / conversion is performed).
202
+ def last_modified
203
+ headers['Last-Modified']
204
+ end
205
+
206
+ # The literal value of ETag HTTP header or nil if no ETag is specified.
207
+ def etag
208
+ headers['ETag']
209
+ end
210
+
211
+ # Determine if the response was last modified at the time provided.
212
+ # time_value is the exact string provided in an origin response's
213
+ # Last-Modified header.
214
+ def last_modified_at?(time_value)
215
+ time_value && last_modified == time_value
216
+ end
217
+
218
+ # Determine if response's ETag matches the etag value provided. Return
219
+ # false when either value is nil.
220
+ def etag_matches?(etag)
221
+ etag && self.etag == etag
222
+ end
223
+
224
+ # Headers that MUST NOT be included with 304 Not Modified responses.
225
+ #
226
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
227
+ NOT_MODIFIED_OMIT_HEADERS = %w[
228
+ Allow
229
+ Content-Encoding
230
+ Content-Language
231
+ Content-Length
232
+ Content-MD5
233
+ Content-Type
234
+ Last-Modified
235
+ ].to_set
236
+
237
+ # Modify the response so that it conforms to the rules defined for
238
+ # '304 Not Modified'. This sets the status, removes the body, and
239
+ # discards any headers that MUST NOT be included in 304 responses.
240
+ #
241
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
242
+ def not_modified!
243
+ self.status = 304
244
+ self.body = []
245
+ NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
246
+ nil
247
+ end
248
+
249
+ # The literal value of the Vary header, or nil when no header is present.
250
+ def vary
251
+ headers['Vary']
252
+ end
253
+
254
+ # Does the response include a Vary header?
255
+ def vary?
256
+ ! vary.nil?
257
+ end
258
+
259
+ # An array of header names given in the Vary header or an empty
260
+ # array when no Vary header is present.
261
+ def vary_header_names
262
+ return [] unless vary = headers['Vary']
263
+ vary.split(/[\s,]+/)
264
+ end
265
+
266
+ end
76
267
  end
@@ -3,11 +3,11 @@ require 'rack/cache/metastore'
3
3
  require 'rack/cache/entitystore'
4
4
 
5
5
  module Rack::Cache
6
+
6
7
  # Maintains a collection of MetaStore and EntityStore instances keyed by
7
8
  # URI. A single instance of this class can be used across a single process
8
9
  # to ensure that only a single instance of a backing store is created per
9
10
  # unique storage URI.
10
-
11
11
  class Storage
12
12
  def initialize
13
13
  @metastores = {}
@@ -22,7 +22,6 @@ module Rack::Cache
22
22
  @entitystores[uri.to_s] ||= create_store(EntityStore, uri)
23
23
  end
24
24
 
25
- # Clear store instances.
26
25
  def clear
27
26
  @metastores.clear
28
27
  @entitystores.clear
data/rack-cache.gemspec CHANGED
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
4
 
5
5
  s.name = 'rack-cache'
6
- s.version = '0.3.0'
7
- s.date = '2008-12-28'
6
+ s.version = '0.3.9'
7
+ s.date = '2009-03-07'
8
8
 
9
9
  s.description = "HTTP Caching for Rack"
10
10
  s.summary = "HTTP Caching for Rack"
@@ -28,33 +28,28 @@ Gem::Specification.new do |s|
28
28
  doc/rack-cache.css
29
29
  doc/server.ru
30
30
  doc/storage.markdown
31
+ example/sinatra/app.rb
32
+ example/sinatra/views/index.erb
31
33
  lib/rack/cache.rb
32
- lib/rack/cache/config.rb
33
- lib/rack/cache/config/busters.rb
34
- lib/rack/cache/config/default.rb
35
- lib/rack/cache/config/no-cache.rb
34
+ lib/rack/cache/cachecontrol.rb
36
35
  lib/rack/cache/context.rb
37
- lib/rack/cache/core.rb
38
36
  lib/rack/cache/entitystore.rb
39
- lib/rack/cache/headers.rb
37
+ lib/rack/cache/key.rb
40
38
  lib/rack/cache/metastore.rb
41
39
  lib/rack/cache/options.rb
42
40
  lib/rack/cache/request.rb
43
41
  lib/rack/cache/response.rb
44
42
  lib/rack/cache/storage.rb
45
- lib/rack/utils/environment_headers.rb
46
43
  rack-cache.gemspec
47
44
  test/cache_test.rb
48
- test/config_test.rb
45
+ test/cachecontrol_test.rb
49
46
  test/context_test.rb
50
- test/core_test.rb
51
47
  test/entitystore_test.rb
52
- test/environment_headers_test.rb
53
- test/headers_test.rb
54
- test/logging_test.rb
48
+ test/key_test.rb
55
49
  test/metastore_test.rb
56
50
  test/options_test.rb
57
51
  test/pony.jpg
52
+ test/request_test.rb
58
53
  test/response_test.rb
59
54
  test/spec_setup.rb
60
55
  test/storage_test.rb