rtomayko-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.
Files changed (44) hide show
  1. data/CHANGES +50 -0
  2. data/COPYING +18 -0
  3. data/README +96 -0
  4. data/Rakefile +144 -0
  5. data/TODO +42 -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/config/busters.rb +16 -0
  15. data/lib/rack/cache/config/default.rb +134 -0
  16. data/lib/rack/cache/config/no-cache.rb +13 -0
  17. data/lib/rack/cache/config.rb +65 -0
  18. data/lib/rack/cache/context.rb +95 -0
  19. data/lib/rack/cache/core.rb +271 -0
  20. data/lib/rack/cache/entitystore.rb +224 -0
  21. data/lib/rack/cache/headers.rb +277 -0
  22. data/lib/rack/cache/metastore.rb +292 -0
  23. data/lib/rack/cache/options.rb +119 -0
  24. data/lib/rack/cache/request.rb +37 -0
  25. data/lib/rack/cache/response.rb +76 -0
  26. data/lib/rack/cache/storage.rb +50 -0
  27. data/lib/rack/cache.rb +51 -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 +505 -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 +222 -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 +122 -0
@@ -0,0 +1,277 @@
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'] || '').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. An
111
+ # object that is cacheable may not necessary be served from cache without
112
+ # first validating the response with the origin.
113
+ #
114
+ # An object that includes no freshness lifetime (Expires, max-age) and that
115
+ # does not include a validator (Last-Modified, Etag) serves no purpose in a
116
+ # cache that only serves fresh or valid objects.
117
+ def cacheable?
118
+ return false unless CACHEABLE_RESPONSE_CODES.include?(status)
119
+ return false if no_store?
120
+ validateable? || fresh?
121
+ end
122
+
123
+ # The response includes specific information about its freshness. True when
124
+ # a +Cache-Control+ header with +max-age+ value is present or when the
125
+ # +Expires+ header is set.
126
+ def freshness_information?
127
+ header?('Expires') || !cache_control['max-age'].nil?
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'].nil?
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
+ # The date, as specified by the Date header. When no Date header is present,
149
+ # set the Date header to Time.now and return.
150
+ def date
151
+ if date = headers['Date']
152
+ Time.httpdate(date)
153
+ else
154
+ headers['Date'] = now.httpdate unless headers.frozen?
155
+ now
156
+ end
157
+ end
158
+
159
+ # The age of the response.
160
+ def age
161
+ [(now - date).to_i, 0].max
162
+ end
163
+
164
+ # The number of seconds after the time specified in the response's Date
165
+ # header when the the response should no longer be considered fresh. First
166
+ # check for a Cache-Control max-age value, and fall back on an expires
167
+ # header; return nil when no maximum age can be established.
168
+ def max_age
169
+ if age = cache_control['max-age']
170
+ age.to_i
171
+ elsif headers['Expires']
172
+ Time.httpdate(headers['Expires']) - date
173
+ end
174
+ end
175
+
176
+ # Sets the number of seconds after which the response should no longer
177
+ # be considered fresh. This sets the Cache-Control max-age value.
178
+ def max_age=(value)
179
+ self.cache_control = cache_control.merge('max-age' => value.to_s)
180
+ end
181
+
182
+ # The Time when the response should be considered stale. With a
183
+ # Cache-Control/max-age value is present, this is calculated by adding the
184
+ # number of seconds specified to the responses #date value. Falls back to
185
+ # the time specified in the Expires header or returns nil if neither is
186
+ # present.
187
+ def expires_at
188
+ if max_age = cache_control['max-age']
189
+ date + max_age.to_i
190
+ elsif time = headers['Expires']
191
+ Time.httpdate(time)
192
+ end
193
+ end
194
+
195
+ # The response's time-to-live in seconds, or nil when no freshness
196
+ # information is present in the response. When the responses #ttl
197
+ # is <= 0, the response may not be served from cache without first
198
+ # revalidating with the origin.
199
+ def ttl
200
+ max_age - age if max_age
201
+ end
202
+
203
+ # Set the response's time-to-live to the specified number of seconds. This
204
+ # adjusts the Cache-Control/max-age value.
205
+ def ttl=(seconds)
206
+ self.max_age = age + seconds
207
+ end
208
+
209
+ # The String value of the Last-Modified header exactly as it appears
210
+ # in the response (i.e., no date parsing / conversion is performed).
211
+ def last_modified
212
+ headers['Last-Modified']
213
+ end
214
+
215
+ # Determine if the response was last modified at the time provided.
216
+ # time_value is the exact string provided in an origin response's
217
+ # Last-Modified header.
218
+ def last_modified_at?(time_value)
219
+ time_value && last_modified == time_value
220
+ end
221
+
222
+ # Determine if response's ETag matches the etag value provided. Return
223
+ # false when either value is nil.
224
+ def etag_matches?(etag)
225
+ etag && self.etag == etag
226
+ end
227
+
228
+ # Headers that MUST NOT be included with 304 Not Modified responses.
229
+ #
230
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
231
+ NOT_MODIFIED_OMIT_HEADERS = %w[
232
+ Allow
233
+ Content-Encoding
234
+ Content-Language
235
+ Content-Length
236
+ Content-Md5
237
+ Content-Type
238
+ Last-Modified
239
+ ].to_set
240
+
241
+ # Modify the response so that it conforms to the rules defined for
242
+ # '304 Not Modified'. This sets the status, removes the body, and
243
+ # discards any headers that MUST NOT be included in 304 responses.
244
+ #
245
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
246
+ def not_modified!
247
+ self.status = 304
248
+ self.body = []
249
+ NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
250
+ nil
251
+ end
252
+
253
+ # The literal value of the Vary header, or nil when no Vary header is
254
+ # present.
255
+ def vary
256
+ headers['Vary']
257
+ end
258
+
259
+ # Does the response include a Vary header?
260
+ def vary?
261
+ ! vary.nil?
262
+ end
263
+
264
+ # An array of header names given in the Vary header or an empty
265
+ # array when no Vary header is present.
266
+ def vary_header_names
267
+ return [] unless vary = headers['Vary']
268
+ vary.split(/[\s,]+/)
269
+ end
270
+
271
+ private
272
+ def now
273
+ @now ||= Time.now
274
+ end
275
+ end
276
+
277
+ end
@@ -0,0 +1,292 @@
1
+ require 'rack'
2
+ require 'fileutils'
3
+ require 'digest/sha1'
4
+
5
+ module Rack::Cache
6
+
7
+ # The MetaStore is responsible for storing meta information about a
8
+ # request/response pair keyed by the request's URL.
9
+ #
10
+ # The meta store keeps a list of request/response pairs for each canonical
11
+ # request URL. A request/response pair is a two element Array of the form:
12
+ # [request, response]
13
+ #
14
+ # The +request+ element is a Hash of Rack environment keys. Only protocol
15
+ # keys (i.e., those that start with "HTTP_") are stored. The +response+
16
+ # element is a Hash of cached HTTP response headers for the paired request.
17
+ #
18
+ # The MetaStore class is abstract and should not be instanstiated
19
+ # directly. Concrete subclasses should implement the protected #read,
20
+ # #write, and #purge methods. Care has been taken to keep these low-level
21
+ # methods dumb and straight-forward to implement.
22
+ class MetaStore
23
+
24
+ # Locate a cached response for the request provided. Returns a
25
+ # Rack::Cache::Response object if the cache hits or nil if no cache entry
26
+ # was found.
27
+ def lookup(request, entity_store)
28
+ entries = read(request.fullpath)
29
+
30
+ # bail out if we have nothing cached
31
+ return nil if entries.empty?
32
+
33
+ # find a cached entry that matches the request.
34
+ env = request.env
35
+ match = entries.detect{ |req,res| requests_match?(res['Vary'], env, req)}
36
+ if match
37
+ # TODO what if body doesn't exist in entity store?
38
+ # reconstruct response object
39
+ req, res = match
40
+ status = res['X-Status']
41
+ body = entity_store.open(res['X-Content-Digest'])
42
+ response = Rack::Cache::Response.new(status.to_i, res, body)
43
+ response.activate!
44
+
45
+ # Return the cached response
46
+ response
47
+ end
48
+ end
49
+
50
+ # Write a cache entry to the store under the given key. Existing
51
+ # entries are read and any that match the response are removed.
52
+ # This method calls #write with the new list of cache entries.
53
+ #--
54
+ # TODO canonicalize URL key
55
+ def store(request, response, entity_store)
56
+ key = request.fullpath
57
+ stored_env = persist_request(request)
58
+
59
+ # write the response body to the entity store if this is the
60
+ # original response.
61
+ response['X-Status'] = response.status.to_s
62
+ if response['X-Content-Digest'].nil?
63
+ digest, size = entity_store.write(response.body)
64
+ response['X-Content-Digest'] = digest
65
+ response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
66
+ response.body = entity_store.open(digest)
67
+ response.activate!
68
+ end
69
+
70
+ # read existing cache entries, remove non-varying, and add this one to
71
+ # the list
72
+ vary = response.vary
73
+ entries =
74
+ read(key).reject do |env,res|
75
+ (vary == res['Vary']) &&
76
+ requests_match?(vary, env, stored_env)
77
+ end
78
+ entries.unshift [stored_env, response.headers.dup]
79
+ write key, entries
80
+ end
81
+
82
+ private
83
+ # Extract the environment Hash from +request+ while making any
84
+ # necessary modifications in preparation for persistence. The Hash
85
+ # returned must be marshalable.
86
+ def persist_request(request)
87
+ env = request.env.dup
88
+ env.reject! { |key,val| key =~ /[^0-9A-Z_]/ }
89
+ env
90
+ end
91
+
92
+ # Determine whether the two environment hashes are non-varying based on
93
+ # the vary response header value provided.
94
+ def requests_match?(vary, env1, env2)
95
+ return true if vary.nil? || vary == ''
96
+ vary.split(/[\s,]+/).all? do |header|
97
+ key = "HTTP_#{header.upcase.tr('-', '_')}"
98
+ env1[key] == env2[key]
99
+ end
100
+ end
101
+
102
+ protected
103
+ # Locate all cached request/response pairs that match the specified
104
+ # URL key. The result must be an Array of all cached request/response
105
+ # pairs. An empty Array must be returned if nothing is cached for
106
+ # the specified key.
107
+ def read(key)
108
+ raise NotImplemented
109
+ end
110
+
111
+ # Store an Array of request/response pairs for the given key. Concrete
112
+ # implementations should not attempt to filter or concatenate the
113
+ # list in any way.
114
+ def write(key, negotiations)
115
+ raise NotImplemented
116
+ end
117
+
118
+ # Remove all cached entries at the key specified. No error is raised
119
+ # when the key does not exist.
120
+ def purge(key)
121
+ raise NotImplemented
122
+ end
123
+
124
+ private
125
+
126
+ # Generate a SHA1 hex digest for the specified string. This is a
127
+ # simple utility method for meta store implementations.
128
+ def hexdigest(data)
129
+ Digest::SHA1.hexdigest(data)
130
+ end
131
+
132
+ public
133
+
134
+ # Concrete MetaStore implementation that uses a simple Hash to store
135
+ # request/response pairs on the heap.
136
+ class Heap < MetaStore
137
+ def initialize(hash={})
138
+ @hash = hash
139
+ end
140
+
141
+ def read(key)
142
+ @hash.fetch(key, []).collect do |req,res|
143
+ [req.dup, res.dup]
144
+ end
145
+ end
146
+
147
+ def write(key, entries)
148
+ @hash[key] = entries
149
+ end
150
+
151
+ def purge(key)
152
+ @hash.delete(key)
153
+ nil
154
+ end
155
+
156
+ def to_hash
157
+ @hash
158
+ end
159
+
160
+ def self.resolve(uri)
161
+ new
162
+ end
163
+ end
164
+
165
+ HEAP = Heap
166
+ MEM = HEAP
167
+
168
+ # Concrete MetaStore implementation that stores request/response
169
+ # pairs on disk.
170
+ class Disk < MetaStore
171
+ attr_reader :root
172
+
173
+ def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
174
+ @root = File.expand_path(root)
175
+ FileUtils.mkdir_p(root, :mode => 0755)
176
+ end
177
+
178
+ def read(key)
179
+ path = key_path(key)
180
+ File.open(path, 'rb') { |io| Marshal.load(io) }
181
+ rescue Errno::ENOENT
182
+ []
183
+ end
184
+
185
+ def write(key, entries)
186
+ path = key_path(key)
187
+ File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
188
+ rescue Errno::ENOENT
189
+ Dir.mkdir(File.dirname(path), 0755)
190
+ retry
191
+ end
192
+
193
+ def purge(key)
194
+ path = key_path(key)
195
+ File.unlink(path)
196
+ nil
197
+ rescue Errno::ENOENT
198
+ nil
199
+ end
200
+
201
+ private
202
+ def key_path(key)
203
+ File.join(root, spread(hexdigest(key)))
204
+ end
205
+
206
+ def spread(sha, n=2)
207
+ sha = sha.dup
208
+ sha[n,0] = '/'
209
+ sha
210
+ end
211
+
212
+ public
213
+ def self.resolve(uri)
214
+ path = File.expand_path(uri.opaque || uri.path)
215
+ new path
216
+ end
217
+
218
+ end
219
+
220
+ DISK = Disk
221
+ FILE = Disk
222
+
223
+ # Stores request/response pairs in memcached. Keys are not stored
224
+ # directly since memcached has a 250-byte limit on key names. Instead,
225
+ # the SHA1 hexdigest of the key is used.
226
+ class MemCache < MetaStore
227
+
228
+ # The Memcached instance used to communicated with the memcached
229
+ # daemon.
230
+ attr_reader :cache
231
+
232
+ def initialize(server="localhost:11211", options={})
233
+ @cache =
234
+ if server.respond_to?(:stats)
235
+ server
236
+ else
237
+ require 'memcached'
238
+ Memcached.new(server, options)
239
+ end
240
+ end
241
+
242
+ def read(key)
243
+ key = hexdigest(key)
244
+ cache.get(key)
245
+ rescue Memcached::NotFound
246
+ []
247
+ end
248
+
249
+ def write(key, entries)
250
+ key = hexdigest(key)
251
+ cache.set(key, entries)
252
+ end
253
+
254
+ def purge(key)
255
+ key = hexdigest(key)
256
+ cache.delete(key)
257
+ nil
258
+ rescue Memcached::NotFound
259
+ nil
260
+ end
261
+
262
+ extend Rack::Utils
263
+
264
+ # Create MemCache store for the given URI. The URI must specify
265
+ # a host and may specify a port, namespace, and options:
266
+ #
267
+ # memcached://example.com:11211/namespace?opt1=val1&opt2=val2
268
+ #
269
+ # Query parameter names and values are documented with the memcached
270
+ # library: http://tinyurl.com/4upqnd
271
+ def self.resolve(uri)
272
+ server = "#{uri.host}:#{uri.port || '11211'}"
273
+ options = parse_query(uri.query)
274
+ options.keys.each do |key|
275
+ value =
276
+ case value = options.delete(key)
277
+ when 'true' ; true
278
+ when 'false' ; false
279
+ else value.to_sym
280
+ end
281
+ options[k.to_sym] = value
282
+ end
283
+ options[:namespace] = uri.path.sub(/^\//, '')
284
+ new server, options
285
+ end
286
+ end
287
+
288
+ MEMCACHE = MemCache
289
+ MEMCACHED = MemCache
290
+ end
291
+
292
+ end
@@ -0,0 +1,119 @@
1
+ require 'rack'
2
+ require 'rack/cache/storage'
3
+
4
+ module Rack::Cache
5
+ # Configuration options and utility methods for option access. Rack::Cache
6
+ # uses the Rack Environment to store option values. All options documented
7
+ # below are stored in the Rack Environment as "rack-cache.<option>", where
8
+ # <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
+ 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) }
22
+ end
23
+ end
24
+
25
+ # Enable verbose trace logging. This option is currently enabled by
26
+ # default but is likely to be disabled in a future release.
27
+ option_accessor :verbose
28
+
29
+ # The storage resolver. Defaults to the Rack::Cache.storage singleton instance
30
+ # of Rack::Cache::Storage. This object is responsible for resolving metastore
31
+ # and entitystore URIs to an implementation instances.
32
+ option_accessor :storage
33
+
34
+ # A URI specifying the meta-store implementation that should be used to store
35
+ # request/response meta information. The following URIs schemes are
36
+ # supported:
37
+ #
38
+ # * heap:/
39
+ # * file:/absolute/path or file:relative/path
40
+ # * memcached://localhost:11211[/namespace]
41
+ #
42
+ # If no meta store is specified the 'heap:/' store is assumed. This
43
+ # implementation has significant draw-backs so explicit configuration is
44
+ # recommended.
45
+ option_accessor :metastore
46
+
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.
50
+ #
51
+ # If no entity store is specified the 'heap:/' store is assumed. This
52
+ # implementation has significant draw-backs so explicit configuration is
53
+ # recommended.
54
+ option_accessor :entitystore
55
+
56
+ # The number of seconds that a cache entry should be considered
57
+ # "fresh" when no explicit freshness information is provided in
58
+ # a response. Explicit Cache-Control or Expires headers
59
+ # override this value.
60
+ #
61
+ # Default: 0
62
+ option_accessor :default_ttl
63
+
64
+ # The underlying options Hash. During initialization (or outside of a
65
+ # request), this is a default values Hash. During a request, this is the
66
+ # Rack environment Hash. The default values Hash is merged in underneath
67
+ # the Rack environment before each request is processed.
68
+ def options
69
+ @env || @default_options
70
+ end
71
+
72
+ # Set multiple options.
73
+ def options=(hash={})
74
+ hash.each { |key,value| write_option(key, value) }
75
+ end
76
+
77
+ # Set an option. When +option+ is a Symbol, it is set in the Rack
78
+ # Environment as "rack-cache.option". When +option+ is a String, it
79
+ # exactly as specified. The +option+ argument may also be a Hash in
80
+ # which case each key/value pair is merged into the environment as if
81
+ # the #set method were called on each.
82
+ def set(option, value=self)
83
+ if value == self
84
+ self.options = option.to_hash
85
+ else
86
+ write_option option, value
87
+ end
88
+ end
89
+
90
+ private
91
+ def read_option(key)
92
+ options[option_name(key)]
93
+ end
94
+
95
+ def write_option(key, value)
96
+ options[option_name(key)] = value
97
+ end
98
+
99
+ def option_name(key)
100
+ case key
101
+ when Symbol ; "rack-cache.#{key}"
102
+ when String ; key
103
+ else raise ArgumentError
104
+ end
105
+ end
106
+
107
+ private
108
+ def initialize_options(options={})
109
+ @default_options = {
110
+ 'rack-cache.verbose' => true,
111
+ 'rack-cache.storage' => Rack::Cache::Storage.instance,
112
+ 'rack-cache.metastore' => 'heap:/',
113
+ 'rack-cache.entitystore' => 'heap:/',
114
+ 'rack-cache.default_ttl' => 0
115
+ }
116
+ self.options = options
117
+ end
118
+ end
119
+ end