roda 0.9.0 → 1.0.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +62 -0
  3. data/README.rdoc +362 -167
  4. data/Rakefile +2 -2
  5. data/doc/release_notes/1.0.0.txt +329 -0
  6. data/lib/roda.rb +553 -180
  7. data/lib/roda/plugins/_erubis_escaping.rb +28 -0
  8. data/lib/roda/plugins/all_verbs.rb +7 -9
  9. data/lib/roda/plugins/backtracking_array.rb +92 -0
  10. data/lib/roda/plugins/content_for.rb +46 -0
  11. data/lib/roda/plugins/csrf.rb +60 -0
  12. data/lib/roda/plugins/flash.rb +53 -7
  13. data/lib/roda/plugins/halt.rb +8 -14
  14. data/lib/roda/plugins/head.rb +56 -0
  15. data/lib/roda/plugins/header_matchers.rb +2 -2
  16. data/lib/roda/plugins/json.rb +84 -0
  17. data/lib/roda/plugins/multi_route.rb +50 -10
  18. data/lib/roda/plugins/not_allowed.rb +140 -0
  19. data/lib/roda/plugins/pass.rb +13 -6
  20. data/lib/roda/plugins/per_thread_caching.rb +70 -0
  21. data/lib/roda/plugins/render.rb +20 -33
  22. data/lib/roda/plugins/render_each.rb +61 -0
  23. data/lib/roda/plugins/symbol_matchers.rb +79 -0
  24. data/lib/roda/plugins/symbol_views.rb +40 -0
  25. data/lib/roda/plugins/view_subdirs.rb +53 -0
  26. data/lib/roda/version.rb +3 -0
  27. data/spec/matchers_spec.rb +61 -5
  28. data/spec/plugin/_erubis_escaping_spec.rb +29 -0
  29. data/spec/plugin/backtracking_array_spec.rb +38 -0
  30. data/spec/plugin/content_for_spec.rb +34 -0
  31. data/spec/plugin/csrf_spec.rb +49 -0
  32. data/spec/plugin/flash_spec.rb +69 -5
  33. data/spec/plugin/head_spec.rb +35 -0
  34. data/spec/plugin/json_spec.rb +50 -0
  35. data/spec/plugin/multi_route_spec.rb +22 -6
  36. data/spec/plugin/not_allowed_spec.rb +55 -0
  37. data/spec/plugin/pass_spec.rb +8 -2
  38. data/spec/plugin/per_thread_caching_spec.rb +28 -0
  39. data/spec/plugin/render_each_spec.rb +30 -0
  40. data/spec/plugin/render_spec.rb +7 -1
  41. data/spec/plugin/symbol_matchers_spec.rb +68 -0
  42. data/spec/plugin/symbol_views_spec.rb +32 -0
  43. data/spec/plugin/view_subdirs_spec.rb +45 -0
  44. data/spec/plugin_spec.rb +11 -1
  45. data/spec/redirect_spec.rb +21 -4
  46. data/spec/request_spec.rb +9 -0
  47. metadata +49 -5
data/Rakefile CHANGED
@@ -3,7 +3,7 @@ require "rake/clean"
3
3
 
4
4
  NAME = 'roda'
5
5
  VERS = lambda do
6
- require File.expand_path("../lib/roda.rb", __FILE__)
6
+ require File.expand_path("../lib/roda/version.rb", __FILE__)
7
7
  Roda::RodaVersion
8
8
  end
9
9
  CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc"]
@@ -34,7 +34,7 @@ rescue LoadError
34
34
  end
35
35
 
36
36
  RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
37
- RDOC_FILES = %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb"
37
+ RDOC_FILES = %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb" + Dir["doc/*.rdoc"] + Dir['doc/release_notes/*.txt']
38
38
 
39
39
  rdoc_task_class.new do |rdoc|
40
40
  rdoc.rdoc_dir = "rdoc"
@@ -0,0 +1,329 @@
1
+ = New Plugins
2
+
3
+ * A csrf plugin has been added for CSRF prevention, using
4
+ Rack::Csrf. It also adds helper methods for views such as
5
+ csrf_tag.
6
+
7
+ * A symbol_matchers plugin has been added, for customizing
8
+ the regexps used per symbol. This also affects the use
9
+ of embedded colons in strings. This supports the following
10
+ symbol regexps by default:
11
+
12
+ :d :: (\d+), a decimal segment
13
+ :format :: (?:\.(\w+))?, an optional format/extension
14
+ :opt :: (?:\/([^\/]+))?, an optional segment
15
+ :optd :: (?:\/(\d+))?, an optional decimal segment
16
+ :rest :: (.*), all remaining characters, if any
17
+ :w :: (\w+), a alphanumeric segment
18
+
19
+ This allows you to write code such as:
20
+
21
+ plugin :symbol_matchers
22
+
23
+ route do |r|
24
+ r.is "track/:d" do
25
+ end
26
+ end
27
+
28
+ And have it only match routes such as /track/123, not
29
+ /track/abc.
30
+
31
+ Note that :opt, :optd, and :format are only going to make sense
32
+ when used as embedded colons in strings, due to how segment matching
33
+ works.
34
+
35
+ You can add your own symbol matchers using the symbol_matcher
36
+ class method:
37
+
38
+ plugin :symbol_matchers
39
+ symbol_matcher :slug, /([\w-]+)/
40
+
41
+ route do |r|
42
+ r.on :slug do
43
+ end
44
+ end
45
+
46
+ * A symbol_views plugin has been added, which allows match blocks to
47
+ return symbols, which are interpreted as template names:
48
+
49
+ plugin :symbol_views
50
+
51
+ route do |r|
52
+ :template_name # same as view :template_name
53
+ end
54
+
55
+ * A json plugin has been added, which allows match blocks to return
56
+ arrays or hashes, and uses a JSON version of them as the response
57
+ body:
58
+
59
+ plugin :json
60
+
61
+ route do |r|
62
+ {'a'=>[1,2,3]} # response: {"a":[1,2,3]}
63
+ end
64
+
65
+ This also sets the Content-Type of the response to application/json.
66
+
67
+ To convert additional object types to JSON, you can modify
68
+ json_response_classes:
69
+
70
+ plugin :json
71
+ json_response_classes << Sequel::Model
72
+
73
+ * A view_subdirs plugin has been added for setting a default
74
+ subdirectory to use for views:
75
+
76
+ Roda.route do |r|
77
+ r.on "admin" do
78
+ set_view_subdir "admin"
79
+
80
+ r.is do
81
+ view "index" # uses admin/index view
82
+ end
83
+ end
84
+ end
85
+
86
+ * A render_each plugin has been added, for rendering the same
87
+ template for multiple objects, and returning the concatenation
88
+ of all of the output:
89
+
90
+ <%= render_each([1,2,3], 'number') %>
91
+
92
+ This renders the number template 3 times. Each time the template
93
+ is rendered, a local variable named number will be present with
94
+ the current entry in the enumerable. You can control the name of
95
+ the local variable using the :local option:
96
+
97
+ <%= render_each([1,2,3], 'number', :local=>:n) %>
98
+
99
+ * A content_for plugin has been added, for storing content in one
100
+ template and retrieving that content in a different template (such
101
+ as the layout). To set content, you call content_for with a block:
102
+
103
+ <% content_for :foo do %>
104
+ content for foo
105
+ <% end %>
106
+
107
+ To retrieve content, you call content_for without a block:
108
+
109
+ <%= content_for :foo %>
110
+
111
+ This plugin probably only works when using erb templates.
112
+
113
+ * A not_allowed plugin has been added, for automatically returning 405
114
+ Method Not Allowed responses when a route is handled for a different
115
+ request method than the one used. For this routing tree:
116
+
117
+ plugin :not_allowed
118
+
119
+ route do |r|
120
+ r.get "foo" do
121
+ end
122
+ end
123
+
124
+ If you submit a POST /foo request, it will return a 405 error
125
+ instead of a 404 error.
126
+
127
+ This also handles cases when multiple methods are supported for
128
+ a single path, so for this routing tree:
129
+
130
+ route do |r|
131
+ r.is "foo" do
132
+ r.get do
133
+ end
134
+ r.post do
135
+ end
136
+ end
137
+ end
138
+
139
+ If you submit a DELETE /foo request, it will return a 405 error
140
+ instead of a 404 error.
141
+
142
+ * A head plugin has been added, automatically handling HEAD requests
143
+ the same as GET requests, except returning an empty body. So for
144
+ this routing tree:
145
+
146
+ plugin :head
147
+
148
+ route do |r|
149
+ r.get "foo" do
150
+ end
151
+ end
152
+
153
+ A request for HEAD /foo will return a 200 result instead of a 404
154
+ error.
155
+
156
+ * A backtracking_array plugin has been added, which makes matching
157
+ backtrack to the next entry in an array if a later matcher fails.
158
+ For example, the following code does not match /foo/bar by
159
+ default in Roda:
160
+
161
+ r.is ['foo', 'foo/bar'] do
162
+ end
163
+
164
+ This is because the 'foo' entry in the array matches, so the
165
+ array matches. However, after the array is matched, the terminal
166
+ matcher added by r.is fails to match. That causes the routing
167
+ method not to match the request, so the match block is not called.
168
+
169
+ With the backtracking_array plugin, failures of later matchers after
170
+ an array matcher backtrack so the next entry in the array is tried.
171
+
172
+ * A per_thread_caching plugin has been added, allowing you to change
173
+ from a thread-safe shared cache to a per-thread cache, which may
174
+ be faster on alternative ruby implementations, at the cost of
175
+ additional memory usage.
176
+
177
+ = New Features
178
+
179
+ * The hash_matcher class method has been added to make it easier to
180
+ define custom hash matchers:
181
+
182
+ hash_matcher(:foo) do |v|
183
+ self['foo'] == v
184
+ end
185
+
186
+ route do |r|
187
+ r.on :foo=>'bar' do
188
+ # matches when param foo has value bar
189
+ end
190
+ end
191
+
192
+ * An r.root routing method has been added for handling GET
193
+ requests where the current path is /. This is basically
194
+ a faster and simpler version of r.get "", except it does
195
+ not consume the / from the path.
196
+
197
+ * The r.halt method now works without an argument, in which
198
+ case it uses the current response.
199
+
200
+ * The r.redirect method now works without an argument for non-GET
201
+ requests, redirecting to the current path.
202
+
203
+ * An :all hash matcher has been added, which takes an array and
204
+ matches only if all of the elements match. This is mainly
205
+ designed for usage inside an array matcher, so:
206
+
207
+ r.on ["foo", {:all=>["bar", :id]}] do
208
+ end
209
+
210
+ will match either /foo or /bar/123, but not /bar.
211
+
212
+ * The render plugin's view method now accepts a :content option,
213
+ in which case it uses the content directly without running it
214
+ through the template engine. This is useful if you have
215
+ arbitrary content you want rendered inside the layout.
216
+
217
+ * The render plugin now accepts an :escape option, in which case
218
+ it will automatically set the default :engine_class for erb
219
+ templates to an Erubis::EscapedEruby subclass. This changes the
220
+ behavior of erb templates such that:
221
+
222
+ <%= '<escaped>' %> # &lt;escaped&gt;
223
+ <%== '<not escaped>' %> # <not escaped>
224
+
225
+ This makes it easier to protect against XSS attacks in your
226
+ templates, as long as you only use <%== %> for content that has
227
+ already been escaped.
228
+
229
+ Note that similar behavior is available in Erubis by default,
230
+ using the :opts=>{:escape_html=>true} render option, but that
231
+ doesn't handle postfix conditionals in <%= %> tags.
232
+
233
+ * The multi_route plugin now has an r.multi_route method, which
234
+ will attempt to dispatch to one of the named routes based on
235
+ first segment in the path. So this routing tree:
236
+
237
+ plugin :multi_route
238
+
239
+ route "a" do |r|
240
+ r.is "c" do
241
+ "e"
242
+ end
243
+ end
244
+ route "b" do |r|
245
+ r.is "d" do
246
+ "f"
247
+ end
248
+ end
249
+
250
+ route do |r|
251
+ r.multi_route
252
+ end
253
+
254
+ will return "e" for /a/c and "f" for /b/d.
255
+
256
+ * Plugins can now override request and response class methods
257
+ using RequestClassMethods and ResponseClassMethods modules.
258
+
259
+ = Optimizations
260
+
261
+ * String, hash, and symbol matchers are now much faster by caching
262
+ the underlying regexp.
263
+
264
+ * String, hash, and symbol matchers are now faster by using a
265
+ regexp positive lookahead assertion instead of an additional
266
+ capture.
267
+
268
+ * Terminal matching in the r.is, r.get, and r.post routing methods
269
+ is now faster, as it does not use a hash matcher internally.
270
+
271
+ * The routing methods are now faster by reducing the number of
272
+ Array objects created.
273
+
274
+ * Calling routing methods without arguments is now faster.
275
+
276
+ * The r.get method is now faster by reducing the number of string
277
+ allocations.
278
+
279
+ * Many request methods are faster by reducing the number of
280
+ method calls used.
281
+
282
+ * Template caching no longer uses a mutex on MRI, since one is
283
+ not needed for thread safety there.
284
+
285
+ = Other Improvements
286
+
287
+ * The flash plugin now implements its own flash hash instead of
288
+ using sinatra-flash. It is now slightly faster and handles nil
289
+ keys in #keep and #discard.
290
+
291
+ * Roda's version is now stored in roda/version.rb so that it can be
292
+ required without requiring Roda itself.
293
+
294
+ = Backwards Compatibility
295
+
296
+ * The multi_route plugin's route instance method has been changed
297
+ to a request method. So the new usage is:
298
+
299
+ plugin :multi_route
300
+
301
+ route "a" do |r|
302
+ end
303
+
304
+ route do |r|
305
+ r.route "a" # instead of: route "a"
306
+ end
307
+
308
+ * The session key used for the flash hash in the flash plugin is
309
+ now :_flash, not :flash.
310
+
311
+ * The :extension matcher now longer forces a terminal match, use
312
+ one of the routing methods that forces a terminal match if you
313
+ want that behavior.
314
+
315
+ * The :term hash matcher has been removed.
316
+
317
+ * The r.consume private method now takes the exact regexp to use
318
+ to search the current path, it no longer enforces a preceeding
319
+ slash and that the match end on a segment boundary.
320
+
321
+ * Dynamically constructing match patterns is now a potential
322
+ memory leak due to them being cached. So you shouldn't do
323
+ things like:
324
+
325
+ r.on r['param'] do
326
+ end
327
+
328
+ * Many private routing methods were changed or removed, if you were
329
+ using them, you'll probably need to update your code.
@@ -1,52 +1,54 @@
1
1
  require "rack"
2
2
  require "thread"
3
+ require "roda/version"
3
4
 
4
5
  # The main class for Roda. Roda is built completely out of plugins, with the
5
6
  # default plugin being Roda::RodaPlugins::Base, so this class is mostly empty
6
7
  # except for some constants.
7
8
  class Roda
8
- # Roda's version, always specified by a string in \d+\.\d+\.\d+ format.
9
- RodaVersion = '0.9.0'.freeze
10
-
11
9
  # Error class raised by Roda
12
10
  class RodaError < StandardError; end
13
11
 
14
- # Base class used for Roda requests. The instance methods for this
15
- # class are added by Roda::RodaPlugins::Base::RequestMethods, so this
16
- # only contains the class methods.
17
- class RodaRequest < ::Rack::Request;
18
- @roda_class = ::Roda
12
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE != 'ruby'
13
+ # A thread safe cache class, offering only #[] and #[]= methods,
14
+ # each protected by a mutex. Used on non-MRI where Hash is not
15
+ # thread safe.
16
+ class RodaCache
17
+ # Create a new thread safe cache.
18
+ def initialize
19
+ @mutex = Mutex.new
20
+ @hash = {}
21
+ end
19
22
 
20
- class << self
21
- # Reference to the Roda class related to this request class.
22
- attr_accessor :roda_class
23
+ # Make getting value from underlying hash thread safe.
24
+ def [](key)
25
+ @mutex.synchronize{@hash[key]}
26
+ end
23
27
 
24
- # Since RodaRequest is anonymously subclassed when Roda is subclassed,
25
- # and then assigned to a constant of the Roda subclass, make inspect
26
- # reflect the likely name for the class.
27
- def inspect
28
- "#{roda_class.inspect}::RodaRequest"
28
+ # Make setting value in underlying hash thread safe.
29
+ def []=(key, value)
30
+ @mutex.synchronize{@hash[key] = value}
29
31
  end
30
32
  end
33
+ else
34
+ # Hashes are already thread-safe in MRI, due to the GVL, so they
35
+ # can safely be used as a cache.
36
+ RodaCache = Hash
37
+ end
38
+
39
+ # Base class used for Roda requests. The instance methods for this
40
+ # class are added by Roda::RodaPlugins::Base::RequestMethods, the
41
+ # class methods are added by Roda::RodaPlugins::Base::RequestClassMethods.
42
+ class RodaRequest < ::Rack::Request;
43
+ @roda_class = ::Roda
44
+ @match_pattern_cache = ::Roda::RodaCache.new
31
45
  end
32
46
 
33
47
  # Base class used for Roda responses. The instance methods for this
34
- # class are added by Roda::RodaPlugins::Base::ResponseMethods, so this
35
- # only contains the class methods.
48
+ # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class
49
+ # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
36
50
  class RodaResponse < ::Rack::Response;
37
51
  @roda_class = ::Roda
38
-
39
- class << self
40
- # Reference to the Roda class related to this response class.
41
- attr_accessor :roda_class
42
-
43
- # Since RodaResponse is anonymously subclassed when Roda is subclassed,
44
- # and then assigned to a constant of the Roda subclass, make inspect
45
- # reflect the likely name for the class.
46
- def inspect
47
- "#{roda_class.inspect}::RodaResponse"
48
- end
49
- end
50
52
  end
51
53
 
52
54
  @builder = ::Rack::Builder.new
@@ -56,11 +58,8 @@ class Roda
56
58
  # Module in which all Roda plugins should be stored. Also contains logic for
57
59
  # registering and loading plugins.
58
60
  module RodaPlugins
59
- # Mutex protecting the plugins hash
60
- @mutex = ::Mutex.new
61
-
62
61
  # Stores registered plugins
63
- @plugins = {}
62
+ @plugins = RodaCache.new
64
63
 
65
64
  # If the registered plugin already exists, use it. Otherwise,
66
65
  # require it and return it. This raises a LoadError if such a
@@ -68,17 +67,19 @@ class Roda
68
67
  # not register itself correctly.
69
68
  def self.load_plugin(name)
70
69
  h = @plugins
71
- unless plugin = @mutex.synchronize{h[name]}
70
+ unless plugin = h[name]
72
71
  require "roda/plugins/#{name}"
73
- raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = @mutex.synchronize{h[name]}
72
+ raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = h[name]
74
73
  end
75
74
  plugin
76
75
  end
77
76
 
78
77
  # Register the given plugin with Roda, so that it can be loaded using #plugin
79
- # with a symbol. Should be used by plugin files.
78
+ # with a symbol. Should be used by plugin files. Example:
79
+ #
80
+ # Roda::RodaPlugins.register_plugin(:plugin_name, PluginModule)
80
81
  def self.register_plugin(name, mod)
81
- @mutex.synchronize{@plugins[name] = mod}
82
+ @plugins[name] = mod
82
83
  end
83
84
 
84
85
  # The base plugin for Roda, implementing all default functionality.
@@ -101,6 +102,26 @@ class Roda
101
102
  app.call(env)
102
103
  end
103
104
 
105
+ # Create a match_#{key} method in the request class using the given
106
+ # block, so that using a hash key in a request match method will
107
+ # call the block. The block should return nil or false to not
108
+ # match, and anything else to match.
109
+ #
110
+ # class App < Roda
111
+ # hash_matcher(:foo) do |v|
112
+ # self['foo'] == v
113
+ # end
114
+ #
115
+ # route do
116
+ # r.on :foo=>'bar' do
117
+ # # matches when param foo has value bar
118
+ # end
119
+ # end
120
+ # end
121
+ def hash_matcher(key, &block)
122
+ request_module{define_method(:"match_#{key}", &block)}
123
+ end
124
+
104
125
  # When inheriting Roda, setup a new rack app builder, copy the
105
126
  # default middleware and opts into the subclass, and set the
106
127
  # request and response classes in the subclasses to be subclasses
@@ -115,6 +136,7 @@ class Roda
115
136
 
116
137
  request_class = Class.new(self::RodaRequest)
117
138
  request_class.roda_class = subclass
139
+ request_class.match_pattern_cache = thread_safe_cache
118
140
  subclass.const_set(:RodaRequest, request_class)
119
141
 
120
142
  response_class = Class.new(self::RodaResponse)
@@ -125,6 +147,9 @@ class Roda
125
147
  # Load a new plugin into the current class. A plugin can be a module
126
148
  # which is used directly, or a symbol represented a registered plugin
127
149
  # which will be required and then used.
150
+ #
151
+ # Roda.plugin PluginModule
152
+ # Roda.plugin :csrf
128
153
  def plugin(mixin, *args, &block)
129
154
  if mixin.is_a?(Symbol)
130
155
  mixin = RodaPlugins.load_plugin(mixin)
@@ -143,9 +168,15 @@ class Roda
143
168
  if defined?(mixin::RequestMethods)
144
169
  self::RodaRequest.send(:include, mixin::RequestMethods)
145
170
  end
171
+ if defined?(mixin::RequestClassMethods)
172
+ self::RodaRequest.extend mixin::RequestClassMethods
173
+ end
146
174
  if defined?(mixin::ResponseMethods)
147
175
  self::RodaResponse.send(:include, mixin::ResponseMethods)
148
176
  end
177
+ if defined?(mixin::ResponseClassMethods)
178
+ self::RodaResponse.extend mixin::ResponseClassMethods
179
+ end
149
180
 
150
181
  if mixin.respond_to?(:configure)
151
182
  mixin.configure(self, *args, &block)
@@ -154,28 +185,71 @@ class Roda
154
185
 
155
186
  # Include the given module in the request class. If a block
156
187
  # is provided instead of a module, create a module using the
157
- # the block.
188
+ # the block. Example:
189
+ #
190
+ # Roda.request_module SomeModule
191
+ #
192
+ # Roda.request_module do
193
+ # def description
194
+ # "#{request_method} #{path_info}"
195
+ # end
196
+ # end
197
+ #
198
+ # Roda.route do |r|
199
+ # r.description
200
+ # end
158
201
  def request_module(mod = nil, &block)
159
202
  module_include(:request, mod, &block)
160
203
  end
161
204
 
162
205
  # Include the given module in the response class. If a block
163
206
  # is provided instead of a module, create a module using the
164
- # the block.
207
+ # the block. Example:
208
+ #
209
+ # Roda.response_module SomeModule
210
+ #
211
+ # Roda.response_module do
212
+ # def error!
213
+ # self.status = 500
214
+ # end
215
+ # end
216
+ #
217
+ # Roda.route do |r|
218
+ # response.error!
219
+ # end
165
220
  def response_module(mod = nil, &block)
166
221
  module_include(:response, mod, &block)
167
222
  end
168
223
 
169
- # Setup route definitions for the current class, and build the
170
- # rack application using the stored middleware.
224
+ # Setup routing tree for the current Roda application, and build the
225
+ # underlying rack application using the stored middleware. Requires
226
+ # a block, which is yielded the request. By convention, the block
227
+ # argument should be named +r+. Example:
228
+ #
229
+ # Roda.route do |r|
230
+ # r.root do
231
+ # "Root"
232
+ # end
233
+ # end
234
+ #
235
+ # This should only be called once per class, and if called multiple
236
+ # times will overwrite the previous routing.
171
237
  def route(&block)
172
238
  @middleware.each{|a, b| @builder.use(*a, &b)}
173
239
  @builder.run lambda{|env| new.call(env, &block)}
174
240
  @app = @builder.to_app
175
241
  end
176
242
 
243
+ # A new thread safe cache instance. This is a method so it can be
244
+ # easily overridden for alternative implementations.
245
+ def thread_safe_cache
246
+ RodaCache.new
247
+ end
248
+
177
249
  # Add a middleware to use for the rack application. Must be
178
- # called before calling #route.
250
+ # called before calling #route to have an effect. Example:
251
+ #
252
+ # Roda.use Rack::Session::Cookie, :secret=>ENV['secret']
179
253
  def use(*args, &block)
180
254
  @middleware << [args, block]
181
255
  end
@@ -214,25 +288,34 @@ class Roda
214
288
 
215
289
  # Create a request and response of the appopriate
216
290
  # class, the instance_exec the route block with
217
- # the request, handling any halts.
291
+ # the request, handling any halts. This is not usually
292
+ # called directly.
218
293
  def call(env, &block)
219
294
  @_request = self.class::RodaRequest.new(self, env)
220
295
  @_response = self.class::RodaResponse.new
221
296
  _route(&block)
222
297
  end
223
298
 
224
- # The environment for the current request.
299
+ # The environment hash for the current request. Example:
300
+ #
301
+ # env['REQUEST_METHOD'] # => 'GET'
225
302
  def env
226
303
  request.env
227
304
  end
228
305
 
229
306
  # The class-level options hash. This should probably not be
230
- # modified at the instance level.
307
+ # modified at the instance level. Example:
308
+ #
309
+ # Roda.plugin :render
310
+ # Roda.route do |r|
311
+ # opts[:render_opts].inspect
312
+ # end
231
313
  def opts
232
314
  self.class.opts
233
315
  end
234
316
 
235
317
  # The instance of the request class related to this request.
318
+ # This is the same object yielded by Roda.route.
236
319
  def request
237
320
  @_request
238
321
  end
@@ -254,12 +337,61 @@ class Roda
254
337
  # behavior after the request and response have been setup.
255
338
  def _route(&block)
256
339
  catch(:halt) do
257
- request.handle_on_result(instance_exec(@_request, &block))
340
+ request.block_result(instance_exec(@_request, &block))
258
341
  response.finish
259
342
  end
260
343
  end
261
344
  end
262
345
 
346
+ # Class methods for RodaRequest
347
+ module RequestClassMethods
348
+ # Reference to the Roda class related to this request class.
349
+ attr_accessor :roda_class
350
+
351
+ # The cache to use for match patterns for this request class.
352
+ attr_accessor :match_pattern_cache
353
+
354
+ # Return the cached pattern for the given object. If the object is
355
+ # not already cached, yield to get the basic pattern, and convert the
356
+ # basic pattern to a pattern that does not partial segments.
357
+ def cached_matcher(obj)
358
+ cache = @match_pattern_cache
359
+
360
+ unless pattern = cache[obj]
361
+ pattern = cache[obj] = consume_pattern(yield)
362
+ end
363
+
364
+ pattern
365
+ end
366
+
367
+ # Define a verb method in the given that will yield to the match block
368
+ # if the request method matches and there are either no arguments or
369
+ # there is a successful terminal match on the arguments.
370
+ def def_verb_method(mod, verb)
371
+ mod.class_eval(<<-END, __FILE__, __LINE__+1)
372
+ def #{verb}(*args, &block)
373
+ _verb(args, &block) if #{verb == :get ? :is_get : verb}?
374
+ end
375
+ END
376
+ end
377
+
378
+ # Since RodaRequest is anonymously subclassed when Roda is subclassed,
379
+ # and then assigned to a constant of the Roda subclass, make inspect
380
+ # reflect the likely name for the class.
381
+ def inspect
382
+ "#{roda_class.inspect}::RodaRequest"
383
+ end
384
+
385
+ private
386
+
387
+ # The pattern to use for consuming, based on the given argument. The returned
388
+ # pattern requires the path starts with a string and does not match partial
389
+ # segments.
390
+ def consume_pattern(pattern)
391
+ /\A(\/(?:#{pattern}))(?=\/|\z)/
392
+ end
393
+ end
394
+
263
395
  # Instance methods for RodaRequest, mostly related to handling routing
264
396
  # for the request.
265
397
  module RequestMethods
@@ -267,8 +399,16 @@ class Roda
267
399
  SCRIPT_NAME = "SCRIPT_NAME".freeze
268
400
  REQUEST_METHOD = "REQUEST_METHOD".freeze
269
401
  EMPTY_STRING = "".freeze
270
- TERM = {:term=>true}.freeze
402
+ SLASH = "/".freeze
271
403
  SEGMENT = "([^\\/]+)".freeze
404
+ TERM_INSPECT = "TERM".freeze
405
+ GET_REQUEST_METHOD = 'GET'.freeze
406
+
407
+ TERM = Object.new
408
+ def TERM.inspect
409
+ TERM_INSPECT
410
+ end
411
+ TERM.freeze
272
412
 
273
413
  # The current captures for the request. This gets modified as routing
274
414
  # occurs.
@@ -286,102 +426,315 @@ class Roda
286
426
  end
287
427
 
288
428
  # As request routing modifies SCRIPT_NAME and PATH_INFO, this exists
289
- # as a helper method to get the full request of the path info.
429
+ # as a helper method to get the full path of the request.
430
+ #
431
+ # r.env['SCRIPT_NAME'] = '/foo'
432
+ # r.env['PATH_INFO'] = '/bar'
433
+ # r.full_path_info
434
+ # # => '/foo/bar'
290
435
  def full_path_info
291
- "#{env[SCRIPT_NAME]}#{env[PATH_INFO]}"
292
- end
293
-
294
- # If this is not a GET method, returns immediately. Otherwise, calls
295
- # #is if there are any arguments, or #on if there are no arguments.
296
- def get(*args, &block)
297
- is_or_on(*args, &block) if get?
436
+ "#{@env[SCRIPT_NAME]}#{@env[PATH_INFO]}"
298
437
  end
299
438
 
300
439
  # Immediately stop execution of the route block and return the given
301
- # rack response array of status, headers, and body.
302
- def halt(response)
303
- _halt(response)
304
- end
305
-
306
- # Handle #on block return values. By default, if a string is given
440
+ # rack response array of status, headers, and body. If no argument
441
+ # is given, uses the current response.
442
+ #
443
+ # r.halt [200, {'Content-Type'=>'text/html'}, ['Hello World!']]
444
+ #
445
+ # response.status = 200
446
+ # response['Content-Type'] = 'text/html'
447
+ # response.write 'Hello World!'
448
+ # r.halt
449
+ def halt(res=response.finish)
450
+ throw :halt, res
451
+ end
452
+
453
+ # Optimized method for whether this request is a +GET+ request.
454
+ # Similar to the default Rack::Request get? method, but can be
455
+ # overridden without changing rack's behavior.
456
+ def is_get?
457
+ @env[REQUEST_METHOD] == GET_REQUEST_METHOD
458
+ end
459
+
460
+ # Handle match block return values. By default, if a string is given
307
461
  # and the response is empty, use the string as the response body.
308
- def handle_on_result(result)
462
+ def block_result(result)
309
463
  res = response
310
- if result.is_a?(String) && res.empty?
311
- res.write(result)
464
+ if res.empty? && (body = block_result_body(result))
465
+ res.write(body)
312
466
  end
313
467
  end
314
468
 
315
469
  # Show information about current request, including request class,
316
470
  # request method and full path.
471
+ #
472
+ # r.inspect
473
+ # # => '#<Roda::RodaRequest GET /foo/bar>'
317
474
  def inspect
318
- "#<#{self.class.inspect} #{env[REQUEST_METHOD]} #{full_path_info}>"
319
- end
320
-
321
- # Adds TERM as the final argument and passes to #on, ensuring that
322
- # there is only a match if #on has fully matched the path.
475
+ "#<#{self.class.inspect} #{@env[REQUEST_METHOD]} #{full_path_info}>"
476
+ end
477
+
478
+ # Does a terminal match on the current path, matching only if the arguments
479
+ # have fully matched the path. If it matches, the match block is
480
+ # executed, and when the match block returns, the rack response is
481
+ # returned.
482
+ #
483
+ # r.path_info
484
+ # # => "/foo/bar"
485
+ #
486
+ # r.is 'foo' do
487
+ # # does not match, as path isn't fully matched (/bar remaining)
488
+ # end
489
+ #
490
+ # r.is 'foo/bar' do
491
+ # # matches as path is empty after matching
492
+ # end
493
+ #
494
+ # If no arguments are given, matches if the path is already fully matched.
495
+ #
496
+ # r.on 'foo/bar' do
497
+ # r.is do
498
+ # # matches as path is already empty
499
+ # end
500
+ # end
501
+ #
502
+ # Note that this matches only if the path after matching the arguments
503
+ # is empty, not if it still contains a trailing slash:
504
+ #
505
+ # r.path_info
506
+ # # => "/foo/bar/"
507
+ #
508
+ # r.is 'foo/bar' do
509
+ # # does not match, as path isn't fully matched (/ remaining)
510
+ # end
511
+ #
512
+ # r.is 'foo/bar/' do
513
+ # # matches as path is empty after matching
514
+ # end
515
+ #
516
+ # r.on 'foo/bar' do
517
+ # r.is "" do
518
+ # # matches as path is empty after matching
519
+ # end
520
+ # end
323
521
  def is(*args, &block)
324
- args << TERM
325
- on(*args, &block)
522
+ if args.empty?
523
+ if @env[PATH_INFO] == EMPTY_STRING
524
+ always(&block)
525
+ end
526
+ else
527
+ args << TERM
528
+ if_match(args, &block)
529
+ end
326
530
  end
327
531
 
328
- # Attempts to match on all of the arguments. If all of the
329
- # arguments match, control is yielded to the block, and after
330
- # the block returns, the rack response will be returned.
331
- # If any of the arguments fails, ensures the request state is
332
- # returned to that before matches were attempted.
532
+ # Does a match on the path, matching only if the arguments
533
+ # have matched the path. Because this doesn't fully match the
534
+ # path, this is usually used to setup branches of the routing tree,
535
+ # not for final handling of the request.
536
+ #
537
+ # r.path_info
538
+ # # => "/foo/bar"
539
+ #
540
+ # r.on 'foo' do
541
+ # # matches, path is /bar after matching
542
+ # end
543
+ #
544
+ # r.on 'bar' do
545
+ # # does not match
546
+ # end
547
+ #
548
+ # Like other routing methods, If it matches, the match block is
549
+ # executed, and when the match block returns, the rack response is
550
+ # returned. However, in general you will call another routing method
551
+ # inside the match block that fully matches the path and does the
552
+ # final handling for the request:
553
+ #
554
+ # r.on 'foo' do
555
+ # r.is 'bar' do
556
+ # # handle /foo/bar request
557
+ # end
558
+ # end
333
559
  def on(*args, &block)
334
- try do
335
- # We stop evaluation of this entire matcher unless
336
- # each and every `arg` defined for this matcher evaluates
337
- # to a non-false value.
338
- #
339
- # Short circuit examples:
340
- # on true, false do
341
- #
342
- # # PATH_INFO=/user
343
- # on true, "signup"
344
- return unless args.all?{|arg| match(arg)}
345
-
346
- # The captures we yield here were generated and assembled
347
- # by evaluating each of the `arg`s above. Most of these
348
- # are carried out by #consume.
349
- handle_on_result(yield(*captures))
350
-
351
- _halt response.finish
560
+ if args.empty?
561
+ always(&block)
562
+ else
563
+ if_match(args, &block)
352
564
  end
353
565
  end
354
566
 
355
- # If this is not a POST method, returns immediately. Otherwise, calls
356
- # #is if there are any arguments, or #on if there are no arguments.
357
- def post(*args, &block)
358
- is_or_on(*args, &block) if post?
359
- end
360
-
361
- # The response related to the current request.
567
+ # The response related to the current request. See ResponseMethods for
568
+ # instance methods for the response, but in general the most common usage
569
+ # is to override the response status and headers:
570
+ #
571
+ # response.status = 200
572
+ # response['Header-Name'] = 'Header value'
362
573
  def response
363
574
  scope.response
364
575
  end
365
576
 
366
- # Immediately redirect to the given path.
367
- def redirect(path, status=302)
577
+ # Immediately redirect to the path using the status code. This ends
578
+ # the processing of the request:
579
+ #
580
+ # r.redirect '/page1', 301 if r['param'] == 'value1'
581
+ # r.redirect '/page2' # uses 302 status code
582
+ # response.status = 404 # not reached
583
+ #
584
+ # If you do not provide a path, by default it will redirect to the same
585
+ # path if the request is not a +GET+ request. This is designed to make
586
+ # it easy to use where a +POST+ request to a URL changes state, +GET+
587
+ # returns the current state, and you want to show the current state
588
+ # after changing:
589
+ #
590
+ # r.is "foo" do
591
+ # r.get do
592
+ # # show state
593
+ # end
594
+ #
595
+ # r.post do
596
+ # # change state
597
+ # r.redirect
598
+ # end
599
+ # end
600
+ def redirect(path=default_redirect_path, status=302)
368
601
  response.redirect(path, status)
369
- _halt response.finish
602
+ throw :halt, response.finish
603
+ end
604
+
605
+ # Routing matches that only matches +GET+ requests where the current
606
+ # path is +/+. If it matches, the match block is executed, and when
607
+ # the match block returns, the rack response is returned.
608
+ #
609
+ # [r.request_method, r.path_info]
610
+ # # => ['GET', '/']
611
+ #
612
+ # r.root do
613
+ # # matches
614
+ # end
615
+ #
616
+ # This is usuable inside other match blocks:
617
+ #
618
+ # [r.request_method, r.path_info]
619
+ # # => ['GET', '/foo/']
620
+ #
621
+ # r.on 'foo' do
622
+ # r.root do
623
+ # # matches
624
+ # end
625
+ # end
626
+ #
627
+ # Note that this does not match non-+GET+ requests:
628
+ #
629
+ # [r.request_method, r.path_info]
630
+ # # => ['POST', '/']
631
+ #
632
+ # r.root do
633
+ # # does not match
634
+ # end
635
+ #
636
+ # Use <tt>r.post ""</tt> for +POST+ requests where the current path
637
+ # is +/+.
638
+ #
639
+ # Nor does it match empty paths:
640
+ #
641
+ # [r.request_method, r.path_info]
642
+ # # => ['GET', '/foo']
643
+ #
644
+ # r.on 'foo' do
645
+ # r.root do
646
+ # # does not match
647
+ # end
648
+ # end
649
+ #
650
+ # Use <tt>r.get true</tt> to handle +GET+ requests where the current
651
+ # path is empty.
652
+ def root(&block)
653
+ if @env[PATH_INFO] == SLASH && is_get?
654
+ always(&block)
655
+ end
370
656
  end
371
657
 
372
- # Call the given rack app with the environment and immediately return
373
- # the response as the response for this request.
658
+ # Call the given rack app with the environment and return the response
659
+ # from the rack app as the response for this request. This ends
660
+ # the processing of the request:
661
+ #
662
+ # r.run(proc{[403, {}, []]}) unless r['letmein'] == '1'
663
+ # r.run(proc{[404, {}, []]})
664
+ # response.status = 404 # not reached
374
665
  def run(app)
375
- _halt app.call(env)
666
+ throw :halt, app.call(@env)
376
667
  end
377
668
 
378
669
  private
379
670
 
380
- # Internal halt method, used so that halt can be overridden to handle
381
- # non-rack response arrays, but internal code that always generates
382
- # rack response arrays can use this for performance.
383
- def _halt(response)
384
- throw :halt, response
671
+ # Match any of the elements in the given array. Return at the
672
+ # first match without evaluating future matches. Returns false
673
+ # if no elements in the array match.
674
+ def _match_array(matcher)
675
+ matcher.any? do |m|
676
+ if matched = match(m)
677
+ if m.is_a?(String)
678
+ captures.push(m)
679
+ end
680
+ end
681
+
682
+ matched
683
+ end
684
+ end
685
+
686
+ # Match the given regexp exactly if it matches a full segment.
687
+ def _match_regexp(re)
688
+ consume(self.class.cached_matcher(re){re})
689
+ end
690
+
691
+ # Match the given hash if all hash matchers match.
692
+ def _match_hash(hash)
693
+ hash.all?{|k,v| send("match_#{k}", v)}
694
+ end
695
+
696
+ # Match the given string to the request path. Regexp escapes the
697
+ # string so that regexp metacharacters are not matched, and recognizes
698
+ # colon tokens for placeholders.
699
+ def _match_string(str)
700
+ consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}})
701
+ end
702
+
703
+ # Match the given symbol if any segment matches.
704
+ def _match_symbol(sym)
705
+ consume(self.class.cached_matcher(sym){_match_symbol_regexp(sym)})
706
+ end
707
+
708
+ # The regular expression to use for matching symbols. By default, any non-empty
709
+ # segment matches.
710
+ def _match_symbol_regexp(s)
711
+ SEGMENT
712
+ end
713
+
714
+ # Backbone of the verb method support, using a terminal match if
715
+ # args is not empty, or a regular match if it is empty.
716
+ def _verb(args, &block)
717
+ if args.empty?
718
+ always(&block)
719
+ else
720
+ args << TERM
721
+ if_match(args, &block)
722
+ end
723
+ end
724
+
725
+ # Yield to the match block and return rack response after the block returns.
726
+ def always
727
+ block_result(yield)
728
+ throw :halt, response.finish
729
+ end
730
+
731
+ # The body to use for the response if the response does not return
732
+ # a body. By default, a String is returned directly, and nil is
733
+ # returned otherwise.
734
+ def block_result_body(result)
735
+ if result.is_a?(String)
736
+ result
737
+ end
385
738
  end
386
739
 
387
740
  # Attempts to match the pattern to the current path. If there is no
@@ -389,43 +742,67 @@ class Roda
389
742
  # SCRIPT_NAME to include the matched path, removes the matched
390
743
  # path from PATH_INFO, and updates captures with any regex captures.
391
744
  def consume(pattern)
392
- matchdata = env[PATH_INFO].match(/\A(\/(?:#{pattern}))(\/|\z)/)
393
-
394
- return false unless matchdata
745
+ env = @env
746
+ return unless matchdata = env[PATH_INFO].match(pattern)
395
747
 
396
748
  vars = matchdata.captures
397
749
 
398
750
  # Don't mutate SCRIPT_NAME, breaks try
399
751
  env[SCRIPT_NAME] += vars.shift
400
- env[PATH_INFO] = "#{vars.pop}#{matchdata.post_match}"
752
+ env[PATH_INFO] = matchdata.post_match
401
753
 
402
754
  captures.concat(vars)
403
755
  end
404
756
 
405
- # Backbone of the verb method support, calling #is if there are any
406
- # arguments, or #on if there are none.
407
- def is_or_on(*args, &block)
408
- if args.empty?
409
- on(*args, &block)
410
- else
411
- is(*args, &block)
412
- end
413
- end
757
+ # The default path to use for redirects when a path is not given.
758
+ # For non-GET requests, redirects to the current path, which will
759
+ # trigger a GET request. This is to make the common case where
760
+ # a POST request will redirect to a GET request at the same location
761
+ # will work fine.
762
+ #
763
+ # If the current request is a GET request, raise an error, as otherwise
764
+ # it is easy to create an infinite redirect.
765
+ def default_redirect_path
766
+ raise RodaError, "must provide path argument to redirect for get requests" if is_get?
767
+ full_path_info
768
+ end
769
+
770
+ # If all of the arguments match, yields to the match block and
771
+ # returns the rack response when the block returns. If any of
772
+ # the match arguments doesn't match, does nothing.
773
+ def if_match(args)
774
+ env = @env
775
+ script = env[SCRIPT_NAME]
776
+ path = env[PATH_INFO]
777
+
778
+ # For every block, we make sure to reset captures so that
779
+ # nesting matchers won't mess with each other's captures.
780
+ captures.clear
414
781
 
782
+ return unless match_all(args)
783
+ block_result(yield(*captures))
784
+ throw :halt, response.finish
785
+ ensure
786
+ env[SCRIPT_NAME] = script
787
+ env[PATH_INFO] = path
788
+ end
789
+
415
790
  # Attempt to match the argument to the given request, handling
416
791
  # common ruby types.
417
792
  def match(matcher)
418
793
  case matcher
419
794
  when String
420
- match_string(matcher)
795
+ _match_string(matcher)
421
796
  when Regexp
422
- consume(matcher)
797
+ _match_regexp(matcher)
423
798
  when Symbol
424
- consume(SEGMENT)
799
+ _match_symbol(matcher)
800
+ when TERM
801
+ @env[PATH_INFO] == EMPTY_STRING
425
802
  when Hash
426
- matcher.all?{|k,v| send("match_#{k}", v)}
803
+ _match_hash(matcher)
427
804
  when Array
428
- match_array(matcher)
805
+ _match_array(matcher)
429
806
  when Proc
430
807
  matcher.call
431
808
  else
@@ -433,25 +810,15 @@ class Roda
433
810
  end
434
811
  end
435
812
 
436
- # Match any of the elements in the given array. Return at the
437
- # first match without evaluating future matches. Returns false
438
- # if no elements in the array match.
439
- def match_array(matcher)
440
- matcher.any? do |m|
441
- if matched = match(m)
442
- if m.is_a?(String)
443
- captures.push(m)
444
- end
445
- end
446
-
447
- matched
448
- end
813
+ # Match only if all of the arguments in the given array match.
814
+ def match_all(args)
815
+ args.all?{|arg| match(arg)}
449
816
  end
450
817
 
451
818
  # Match files with the given extension. Requires that the
452
819
  # request path end with the extension.
453
820
  def match_extension(ext)
454
- consume("([^\\/]+?)\.#{ext}\\z")
821
+ consume(self.class.cached_matcher([:extension, ext]){/([^\\\/]+)\.#{ext}/})
455
822
  end
456
823
 
457
824
  # Match by request method. This can be an array if you want
@@ -460,7 +827,7 @@ class Roda
460
827
  if type.is_a?(Array)
461
828
  type.any?{|t| match_method(t)}
462
829
  else
463
- type.to_s.upcase == env[REQUEST_METHOD]
830
+ type.to_s.upcase == @env[REQUEST_METHOD]
464
831
  end
465
832
  end
466
833
 
@@ -479,37 +846,18 @@ class Roda
479
846
  captures << v
480
847
  end
481
848
  end
849
+ end
482
850
 
483
- # Match the given string to the request path. Regexp escapes the
484
- # string so that regexp metacharacters are not matched, and recognizes
485
- # colon tokens for placeholders.
486
- def match_string(str)
487
- str = Regexp.escape(str)
488
- str.gsub!(/:\w+/, SEGMENT)
489
- consume(str)
490
- end
491
-
492
- # Only match if the request path is empty, which usually indicates it
493
- # has already been fully matched.
494
- def match_term(term)
495
- !(term ^ (env[PATH_INFO] == EMPTY_STRING))
496
- end
497
-
498
- # Yield to the given block, clearing any captures before
499
- # yielding and restoring the SCRIPT_NAME and PATH_INFO on exit.
500
- def try
501
- script = env[SCRIPT_NAME]
502
- path = env[PATH_INFO]
503
-
504
- # For every block, we make sure to reset captures so that
505
- # nesting matchers won't mess with each other's captures.
506
- captures.clear
507
-
508
- yield
851
+ # Class methods for RodaResponse
852
+ module ResponseClassMethods
853
+ # Reference to the Roda class related to this response class.
854
+ attr_accessor :roda_class
509
855
 
510
- ensure
511
- env[SCRIPT_NAME] = script
512
- env[PATH_INFO] = path
856
+ # Since RodaResponse is anonymously subclassed when Roda is subclassed,
857
+ # and then assigned to a constant of the Roda subclass, make inspect
858
+ # reflect the likely name for the class.
859
+ def inspect
860
+ "#{roda_class.inspect}::RodaResponse"
513
861
  end
514
862
  end
515
863
 
@@ -535,12 +883,16 @@ class Roda
535
883
  @length = 0
536
884
  end
537
885
 
538
- # Return the response header with the given key.
886
+ # Return the response header with the given key. Example:
887
+ #
888
+ # response['Content-Type'] # => 'text/html'
539
889
  def [](key)
540
890
  @headers[key]
541
891
  end
542
892
 
543
893
  # Set the response header with the given key to the given value.
894
+ #
895
+ # response['Content-Type'] = 'application/json'
544
896
  def []=(key, value)
545
897
  @headers[key] = value
546
898
  end
@@ -558,19 +910,29 @@ class Roda
558
910
  # Modify the headers to include a Set-Cookie value that
559
911
  # deletes the cookie. A value hash can be provided to
560
912
  # override the default one used to delete the cookie.
913
+ # Example:
914
+ #
915
+ # response.delete_cookie('foo')
916
+ # response.delete_cookie('foo', :domain=>'example.org')
561
917
  def delete_cookie(key, value = {})
562
918
  ::Rack::Utils.delete_cookie_header!(@headers, key, value)
563
919
  end
564
920
 
565
921
  # Whether the response body has been written to yet. Note
566
922
  # that writing an empty string to the response body marks
567
- # the response as not empty.
923
+ # the response as not empty. Example:
924
+ #
925
+ # response.empty? # => true
926
+ # response.write('a')
927
+ # response.empty? # => false
568
928
  def empty?
569
929
  @body.empty?
570
930
  end
571
931
 
572
932
  # Return the rack response array of status, headers, and body
573
- # for the current response.
933
+ # for the current response. Example:
934
+ #
935
+ # response.finish # => [200, {'Content-Type'=>'text/html'}, []]
574
936
  def finish
575
937
  b = @body
576
938
  s = (@status ||= b.empty? ? 404 : 200)
@@ -578,19 +940,28 @@ class Roda
578
940
  end
579
941
 
580
942
  # Set the Location header to the given path, and the status
581
- # to the given status.
943
+ # to the given status. Example:
944
+ #
945
+ # response.redirect('foo', 301)
946
+ # response.redirect('bar')
582
947
  def redirect(path, status = 302)
583
948
  @headers[LOCATION] = path
584
949
  @status = status
585
950
  end
586
951
 
587
952
  # Set the cookie with the given key in the headers.
953
+ #
954
+ # response.set_cookie('foo', 'bar')
955
+ # response.set_cookie('foo', :value=>'bar', :domain=>'example.org')
588
956
  def set_cookie(key, value)
589
957
  ::Rack::Utils.set_cookie_header!(@headers, key, value)
590
958
  end
591
959
 
592
960
  # Write to the response body. Updates Content-Length header
593
- # with the size of the string written. Returns nil.
961
+ # with the size of the string written. Returns nil. Example:
962
+ #
963
+ # response.write('foo')
964
+ # response['Content-Length'] # =>'3'
594
965
  def write(str)
595
966
  s = str.to_s
596
967
 
@@ -605,4 +976,6 @@ class Roda
605
976
 
606
977
  extend RodaPlugins::Base::ClassMethods
607
978
  plugin RodaPlugins::Base
979
+ RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :get)
980
+ RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :post)
608
981
  end