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.

Files changed (44) hide show
  1. data/CHANGES +43 -0
  2. data/README +18 -9
  3. data/Rakefile +1 -14
  4. data/TODO +13 -14
  5. data/doc/configuration.markdown +7 -153
  6. data/doc/faq.markdown +8 -0
  7. data/doc/index.markdown +7 -9
  8. data/example/sinatra/app.rb +25 -0
  9. data/example/sinatra/views/index.erb +44 -0
  10. data/lib/rack/cache.rb +5 -11
  11. data/lib/rack/cache/cachecontrol.rb +193 -0
  12. data/lib/rack/cache/context.rb +190 -52
  13. data/lib/rack/cache/entitystore.rb +10 -4
  14. data/lib/rack/cache/key.rb +52 -0
  15. data/lib/rack/cache/metastore.rb +52 -16
  16. data/lib/rack/cache/options.rb +60 -39
  17. data/lib/rack/cache/request.rb +11 -15
  18. data/lib/rack/cache/response.rb +221 -30
  19. data/lib/rack/cache/storage.rb +1 -2
  20. data/rack-cache.gemspec +9 -15
  21. data/test/cache_test.rb +9 -6
  22. data/test/cachecontrol_test.rb +139 -0
  23. data/test/context_test.rb +251 -169
  24. data/test/entitystore_test.rb +12 -11
  25. data/test/key_test.rb +50 -0
  26. data/test/metastore_test.rb +57 -14
  27. data/test/options_test.rb +11 -0
  28. data/test/request_test.rb +19 -0
  29. data/test/response_test.rb +164 -23
  30. data/test/spec_setup.rb +7 -0
  31. metadata +12 -20
  32. data/doc/events.dot +0 -27
  33. data/lib/rack/cache/config.rb +0 -65
  34. data/lib/rack/cache/config/busters.rb +0 -16
  35. data/lib/rack/cache/config/default.rb +0 -133
  36. data/lib/rack/cache/config/no-cache.rb +0 -13
  37. data/lib/rack/cache/core.rb +0 -299
  38. data/lib/rack/cache/headers.rb +0 -325
  39. data/lib/rack/utils/environment_headers.rb +0 -78
  40. data/test/config_test.rb +0 -66
  41. data/test/core_test.rb +0 -84
  42. data/test/environment_headers_test.rb +0 -69
  43. data/test/headers_test.rb +0 -298
  44. data/test/logging_test.rb +0 -45
@@ -1,6 +1,7 @@
1
1
  require 'digest/sha1'
2
2
 
3
3
  module Rack::Cache
4
+
4
5
  # Entity stores are used to cache response bodies across requests. All
5
6
  # Implementations are required to calculate a SHA checksum of the data written
6
7
  # which becomes the response body's key.
@@ -13,7 +14,7 @@ module Rack::Cache
13
14
  def slurp(body)
14
15
  digest, size = Digest::SHA1.new, 0
15
16
  body.each do |part|
16
- size += part.length
17
+ size += bytesize(part)
17
18
  digest << part
18
19
  yield part
19
20
  end
@@ -21,7 +22,13 @@ module Rack::Cache
21
22
  [ digest.hexdigest, size ]
22
23
  end
23
24
 
24
- private :slurp
25
+ if ''.respond_to?(:bytesize)
26
+ def bytesize(string); string.bytesize; end
27
+ else
28
+ def bytesize(string); string.size; end
29
+ end
30
+
31
+ private :slurp, :bytesize
25
32
 
26
33
 
27
34
  # Stores entity bodies on the heap using a Hash object.
@@ -90,7 +97,7 @@ module Rack::Cache
90
97
  end
91
98
 
92
99
  def read(key)
93
- File.read(body_path(key))
100
+ File.open(body_path(key), 'rb') { |f| f.read }
94
101
  rescue Errno::ENOENT
95
102
  nil
96
103
  end
@@ -241,7 +248,6 @@ module Rack::Cache
241
248
 
242
249
  MEMCACHE = MemCache
243
250
  MEMCACHED = MemCache
244
-
245
251
  end
246
252
 
247
253
  end
@@ -0,0 +1,52 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack::Cache
4
+ class Key
5
+ include Rack::Utils
6
+
7
+ # Implement .call, since it seems like the "Rack-y" thing to do. Plus, it
8
+ # opens the door for cache key generators to just be blocks.
9
+ def self.call(request)
10
+ new(request).generate
11
+ end
12
+
13
+ def initialize(request)
14
+ @request = request
15
+ end
16
+
17
+ # Generate a normalized cache key for the request.
18
+ def generate
19
+ parts = []
20
+ parts << @request.scheme << "://"
21
+ parts << @request.host
22
+
23
+ if @request.scheme == "https" && @request.port != 443 ||
24
+ @request.scheme == "http" && @request.port != 80
25
+ parts << ":" << @request.port.to_s
26
+ end
27
+
28
+ parts << @request.script_name
29
+ parts << @request.path_info
30
+
31
+ if qs = query_string
32
+ parts << "?"
33
+ parts << qs
34
+ end
35
+
36
+ parts.join
37
+ end
38
+
39
+ private
40
+ # Build a normalized query string by alphabetizing all keys/values
41
+ # and applying consistent escaping.
42
+ def query_string
43
+ return nil if @request.query_string.nil?
44
+
45
+ @request.query_string.split(/[&;] */n).
46
+ map { |p| unescape(p).split('=', 2) }.
47
+ sort.
48
+ map { |k,v| "#{escape(k)}=#{escape(v)}" }.
49
+ join('&')
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,6 @@
1
- require 'rack'
2
1
  require 'fileutils'
3
2
  require 'digest/sha1'
3
+ require 'rack/utils'
4
4
 
5
5
  module Rack::Cache
6
6
 
@@ -25,7 +25,8 @@ module Rack::Cache
25
25
  # Rack::Cache::Response object if the cache hits or nil if no cache entry
26
26
  # was found.
27
27
  def lookup(request, entity_store)
28
- entries = read(request.fullpath)
28
+ key = cache_key(request)
29
+ entries = read(key)
29
30
 
30
31
  # bail out if we have nothing cached
31
32
  return nil if entries.empty?
@@ -37,9 +38,7 @@ module Rack::Cache
37
38
 
38
39
  req, res = match
39
40
  if body = entity_store.open(res['X-Content-Digest'])
40
- response = Rack::Cache::Response.new(res['X-Status'].to_i, res, body)
41
- response.activate!
42
- response
41
+ restore_response(res, body)
43
42
  else
44
43
  # TODO the metastore referenced an entity that doesn't exist in
45
44
  # the entitystore. we definitely want to return nil but we should
@@ -50,21 +49,17 @@ module Rack::Cache
50
49
  # Write a cache entry to the store under the given key. Existing
51
50
  # entries are read and any that match the response are removed.
52
51
  # This method calls #write with the new list of cache entries.
53
- #--
54
- # TODO canonicalize URL key
55
52
  def store(request, response, entity_store)
56
- key = request.fullpath
53
+ key = cache_key(request)
57
54
  stored_env = persist_request(request)
58
55
 
59
56
  # write the response body to the entity store if this is the
60
57
  # original response.
61
- response['X-Status'] = response.status.to_s
62
- if response['X-Content-Digest'].nil?
58
+ if response.headers['X-Content-Digest'].nil?
63
59
  digest, size = entity_store.write(response.body)
64
- response['X-Content-Digest'] = digest
65
- response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
60
+ response.headers['X-Content-Digest'] = digest
61
+ response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
66
62
  response.body = entity_store.open(digest)
67
- response.activate!
68
63
  end
69
64
 
70
65
  # read existing cache entries, remove non-varying, and add this one to
@@ -75,11 +70,41 @@ module Rack::Cache
75
70
  (vary == res['Vary']) &&
76
71
  requests_match?(vary, env, stored_env)
77
72
  end
78
- entries.unshift [stored_env, {}.update(response.headers)]
73
+
74
+ headers = persist_response(response)
75
+ headers.delete 'Age'
76
+
77
+ entries.unshift [stored_env, headers]
79
78
  write key, entries
79
+ key
80
+ end
81
+
82
+ # Generate a cache key for the request.
83
+ def cache_key(request)
84
+ keygen = request.env['rack-cache.cache_key'] || Key
85
+ keygen.call(request)
86
+ end
87
+
88
+ # Invalidate all cache entries that match the request.
89
+ def invalidate(request, entity_store)
90
+ modified = false
91
+ key = cache_key(request)
92
+ entries =
93
+ read(key).map do |req, res|
94
+ response = restore_response(res)
95
+ if response.fresh?
96
+ response.expire!
97
+ modified = true
98
+ [req, persist_response(response)]
99
+ else
100
+ [req, res]
101
+ end
102
+ end
103
+ write key, entries if modified
80
104
  end
81
105
 
82
106
  private
107
+
83
108
  # Extract the environment Hash from +request+ while making any
84
109
  # necessary modifications in preparation for persistence. The Hash
85
110
  # returned must be marshalable.
@@ -89,6 +114,19 @@ module Rack::Cache
89
114
  env
90
115
  end
91
116
 
117
+ # Converts a stored response hash into a Response object. The caller
118
+ # is responsible for loading and passing the body if needed.
119
+ def restore_response(hash, body=nil)
120
+ status = hash.delete('X-Status').to_i
121
+ Rack::Cache::Response.new(status, hash, body)
122
+ end
123
+
124
+ def persist_response(response)
125
+ hash = response.headers.to_hash
126
+ hash['X-Status'] = response.status.to_s
127
+ hash
128
+ end
129
+
92
130
  # Determine whether the two environment hashes are non-varying based on
93
131
  # the vary response header value provided.
94
132
  def requests_match?(vary, env1, env2)
@@ -122,7 +160,6 @@ module Rack::Cache
122
160
  end
123
161
 
124
162
  private
125
-
126
163
  # Generate a SHA1 hex digest for the specified string. This is a
127
164
  # simple utility method for meta store implementations.
128
165
  def hexdigest(data)
@@ -130,7 +167,6 @@ module Rack::Cache
130
167
  end
131
168
 
132
169
  public
133
-
134
170
  # Concrete MetaStore implementation that uses a simple Hash to store
135
171
  # request/response pairs on the heap.
136
172
  class Heap < MetaStore
@@ -1,26 +1,28 @@
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
- # 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
-
15
10
  module Options
16
- class << self
17
- private
18
- def option_accessor(key)
19
- define_method(key) { || read_option(key) }
20
- define_method("#{key}=") { |value| write_option(key, value) }
21
- define_method("#{key}?") { || !! read_option(key) }
11
+ def self.option_accessor(key)
12
+ name = option_name(key)
13
+ define_method(key) { || options[name] }
14
+ define_method("#{key}=") { |value| options[name] = value }
15
+ define_method("#{key}?") { || !! options[name] }
16
+ end
17
+
18
+ def option_name(key)
19
+ case key
20
+ when Symbol ; "rack-cache.#{key}"
21
+ when String ; key
22
+ else raise ArgumentError
22
23
  end
23
24
  end
25
+ module_function :option_name
24
26
 
25
27
  # Enable verbose trace logging. This option is currently enabled by
26
28
  # default but is likely to be disabled in a future release.
@@ -44,9 +46,22 @@ module Rack::Cache
44
46
  # recommended.
45
47
  option_accessor :metastore
46
48
 
47
- # A URI specifying the entity-store implement that should be used to store
48
- # response bodies. See the metastore option for information on supported URI
49
- # schemes.
49
+ # A custom cache key generator, which can be anything that responds to :call.
50
+ # By default, this is the Rack::Cache::Key class, but you can implement your
51
+ # own generator. A cache key generator gets passed a request and generates the
52
+ # appropriate cache key.
53
+ #
54
+ # In addition to setting the generator to an object, you can just pass a block
55
+ # instead, which will act as the cache key generator:
56
+ #
57
+ # set :cache_key do |request|
58
+ # request.fullpath.replace(/\//, '-')
59
+ # end
60
+ option_accessor :cache_key
61
+
62
+ # A URI specifying the entity-store implementation that should be used to
63
+ # store response bodies. See the metastore option for information on
64
+ # supported URI schemes.
50
65
  #
51
66
  # If no entity store is specified the 'heap:/' store is assumed. This
52
67
  # implementation has significant draw-backs so explicit configuration is
@@ -70,6 +85,16 @@ module Rack::Cache
70
85
  # Default: ['Authorization', 'Cookie']
71
86
  option_accessor :private_headers
72
87
 
88
+ # Specifies whether the client can force a cache reload by including a
89
+ # Cache-Control "no-cache" directive in the request. This is enabled by
90
+ # default for compliance with RFC 2616.
91
+ option_accessor :allow_reload
92
+
93
+ # Specifies whether the client can force a cache revalidate by including
94
+ # a Cache-Control "max-age=0" directive in the request. This is enabled by
95
+ # default for compliance with RFC 2616.
96
+ option_accessor :allow_revalidate
97
+
73
98
  # The underlying options Hash. During initialization (or outside of a
74
99
  # request), this is a default values Hash. During a request, this is the
75
100
  # Rack environment Hash. The default values Hash is merged in underneath
@@ -88,8 +113,10 @@ module Rack::Cache
88
113
  # exactly as specified. The +option+ argument may also be a Hash in
89
114
  # which case each key/value pair is merged into the environment as if
90
115
  # the #set method were called on each.
91
- def set(option, value=self)
92
- if value == self
116
+ def set(option, value=self, &block)
117
+ if block_given?
118
+ write_option option, block
119
+ elsif value == self
93
120
  self.options = option.to_hash
94
121
  else
95
122
  write_option option, value
@@ -97,6 +124,21 @@ module Rack::Cache
97
124
  end
98
125
 
99
126
  private
127
+ def initialize_options(options={})
128
+ @default_options = {
129
+ 'rack-cache.cache_key' => Key,
130
+ 'rack-cache.verbose' => true,
131
+ 'rack-cache.storage' => Rack::Cache::Storage.instance,
132
+ 'rack-cache.metastore' => 'heap:/',
133
+ 'rack-cache.entitystore' => 'heap:/',
134
+ 'rack-cache.default_ttl' => 0,
135
+ 'rack-cache.private_headers' => ['Authorization', 'Cookie'],
136
+ 'rack-cache.allow_reload' => true,
137
+ 'rack-cache.allow_revalidate' => true
138
+ }
139
+ self.options = options
140
+ end
141
+
100
142
  def read_option(key)
101
143
  options[option_name(key)]
102
144
  end
@@ -104,26 +146,5 @@ module Rack::Cache
104
146
  def write_option(key, value)
105
147
  options[option_name(key)] = value
106
148
  end
107
-
108
- def option_name(key)
109
- case key
110
- when Symbol ; "rack-cache.#{key}"
111
- when String ; key
112
- else raise ArgumentError
113
- end
114
- end
115
-
116
- private
117
- def initialize_options(options={})
118
- @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,
124
- 'rack-cache.private_headers' => ['Authorization', 'Cookie']
125
- }
126
- self.options = options
127
- end
128
149
  end
129
150
  end
@@ -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