rtomayko-rack-cache 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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