roda 1.1.0 → 1.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +70 -0
  3. data/README.rdoc +261 -302
  4. data/Rakefile +1 -1
  5. data/doc/release_notes/1.2.0.txt +406 -0
  6. data/lib/roda.rb +206 -124
  7. data/lib/roda/plugins/all_verbs.rb +11 -10
  8. data/lib/roda/plugins/assets.rb +5 -5
  9. data/lib/roda/plugins/backtracking_array.rb +12 -5
  10. data/lib/roda/plugins/caching.rb +10 -8
  11. data/lib/roda/plugins/class_level_routing.rb +94 -0
  12. data/lib/roda/plugins/content_for.rb +6 -0
  13. data/lib/roda/plugins/default_headers.rb +4 -11
  14. data/lib/roda/plugins/delay_build.rb +42 -0
  15. data/lib/roda/plugins/delegate.rb +64 -0
  16. data/lib/roda/plugins/drop_body.rb +33 -0
  17. data/lib/roda/plugins/empty_root.rb +48 -0
  18. data/lib/roda/plugins/environments.rb +68 -0
  19. data/lib/roda/plugins/error_email.rb +1 -2
  20. data/lib/roda/plugins/error_handler.rb +1 -1
  21. data/lib/roda/plugins/halt.rb +7 -5
  22. data/lib/roda/plugins/head.rb +4 -2
  23. data/lib/roda/plugins/header_matchers.rb +17 -9
  24. data/lib/roda/plugins/hooks.rb +16 -32
  25. data/lib/roda/plugins/json.rb +4 -10
  26. data/lib/roda/plugins/mailer.rb +233 -0
  27. data/lib/roda/plugins/match_affix.rb +48 -0
  28. data/lib/roda/plugins/multi_route.rb +9 -11
  29. data/lib/roda/plugins/multi_run.rb +81 -0
  30. data/lib/roda/plugins/named_templates.rb +93 -0
  31. data/lib/roda/plugins/not_allowed.rb +43 -48
  32. data/lib/roda/plugins/path.rb +63 -2
  33. data/lib/roda/plugins/render.rb +79 -48
  34. data/lib/roda/plugins/render_each.rb +6 -0
  35. data/lib/roda/plugins/sinatra_helpers.rb +523 -0
  36. data/lib/roda/plugins/slash_path_empty.rb +25 -0
  37. data/lib/roda/plugins/static_path_info.rb +64 -0
  38. data/lib/roda/plugins/streaming.rb +1 -1
  39. data/lib/roda/plugins/view_subdirs.rb +12 -8
  40. data/lib/roda/version.rb +1 -1
  41. data/spec/integration_spec.rb +33 -0
  42. data/spec/plugin/backtracking_array_spec.rb +24 -18
  43. data/spec/plugin/class_level_routing_spec.rb +138 -0
  44. data/spec/plugin/delay_build_spec.rb +23 -0
  45. data/spec/plugin/delegate_spec.rb +20 -0
  46. data/spec/plugin/drop_body_spec.rb +20 -0
  47. data/spec/plugin/empty_root_spec.rb +14 -0
  48. data/spec/plugin/environments_spec.rb +31 -0
  49. data/spec/plugin/h_spec.rb +1 -3
  50. data/spec/plugin/header_matchers_spec.rb +14 -0
  51. data/spec/plugin/hooks_spec.rb +3 -5
  52. data/spec/plugin/mailer_spec.rb +191 -0
  53. data/spec/plugin/match_affix_spec.rb +22 -0
  54. data/spec/plugin/multi_run_spec.rb +31 -0
  55. data/spec/plugin/named_templates_spec.rb +65 -0
  56. data/spec/plugin/path_spec.rb +66 -2
  57. data/spec/plugin/render_spec.rb +46 -1
  58. data/spec/plugin/sinatra_helpers_spec.rb +534 -0
  59. data/spec/plugin/slash_path_empty_spec.rb +22 -0
  60. data/spec/plugin/static_path_info_spec.rb +50 -0
  61. data/spec/request_spec.rb +23 -0
  62. data/spec/response_spec.rb +12 -1
  63. metadata +48 -6
data/Rakefile CHANGED
@@ -17,7 +17,7 @@ end
17
17
 
18
18
  ### RDoc
19
19
 
20
- RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Roda: Routing tree web framework']
20
+ RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Roda: Routing tree web framework toolkit']
21
21
 
22
22
  begin
23
23
  gem 'hanna-nouveau'
@@ -0,0 +1,406 @@
1
+ = New Plugins
2
+
3
+ * A static_path_info plugin has been added, which doesn't modify
4
+ SCRIPT_NAME/PATH_INFO during routing, only before dispatching
5
+ the request to another rack application via r.run. This is
6
+ faster and avoids problems caused by changing SCRIPT_NAME/PATH_INFO
7
+ during routing, such as methods that return paths that depend on
8
+ SCRIPT_NAME. This behavior will become Roda's default starting
9
+ in Roda 2, and it is recommended that all Roda apps use it.
10
+
11
+ * A mailer plugin has been added, which allows you to use Roda's
12
+ render plugin to create email bodies, and allows you to use Roda's
13
+ routing tree features to DRY up your mailing code similar to how it
14
+ DRYs up your web code.
15
+
16
+ Here is an example routing tree using the mailer plugin:
17
+
18
+ class Mailer < Roda
19
+ plugin :render
20
+ plugin :mailer
21
+
22
+ route do |r|
23
+ r.on "user/:d" do |user_id|
24
+ # DRY up code by setting shared behavior in higher level
25
+ # branches, instead of duplicating it inside each subtree.
26
+ @user = User[user_id]
27
+ from 'notifications@example.com'
28
+ to @user.email
29
+
30
+ r.mail "open_account" do
31
+ subject 'Welcome to example.com'
32
+ render(:open_account)
33
+ end
34
+
35
+ r.mail "close_account" do
36
+ subject 'Thank you for using example.com'
37
+ render(:close_account)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ With your routing tree setup, you can use the sendmail method to
44
+ send email:
45
+
46
+ Mailer.sendmail("/user/1/open_account")
47
+
48
+ If you want a Mail::Message object returned for further modification
49
+ before sending, you can use mail instead of of sendmail:
50
+
51
+ Mailer.mail("/user/2/close_account").deliver
52
+
53
+ * A delegate plugin has been added, allowing you to easily create
54
+ methods in the route block scope that delegate to the request or
55
+ response. While Roda does not pollute your namespaces by default,
56
+ this allows you to choose to do so yourself if you find it offers
57
+ a nicer API. Example:
58
+
59
+ class App < Roda
60
+ plugin :delegate
61
+ request_delegate :root, :on, :is, :get, :post, :redirect
62
+
63
+ route do |r|
64
+ root do
65
+ redirect "/hello"
66
+ end
67
+
68
+ on "hello" do
69
+ get "world" do
70
+ "Hello world!"
71
+ end
72
+
73
+ is do
74
+ get do
75
+ "Hello!"
76
+ end
77
+
78
+ post do
79
+ puts "Someone said hello!"
80
+ redirect
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ * A class_level_routing plugin has been added, allowing you to define
88
+ your routes at the class level if desired. The routes defined at
89
+ the class level can still use a routing tree for further routing.
90
+ Example:
91
+
92
+ class App < Roda
93
+ plugin :class_level_routing
94
+
95
+ root do
96
+ request.redirect "/hello"
97
+ end
98
+
99
+ get "hello/world" do
100
+ "Hello world!"
101
+ end
102
+
103
+ is "hello" do
104
+ request.get do
105
+ "Hello!"
106
+ end
107
+
108
+ request.post do
109
+ puts "Someone said hello!"
110
+ request.redirect
111
+ end
112
+ end
113
+ end
114
+
115
+ * A named_templates plugin has been added, for creating inline
116
+ templates associated with a given name, that are used by
117
+ the render plugin's render/view method in preference to
118
+ templates stored in the filesystem. This makes it simpler to
119
+ ship single-file Roda applications that use templates. Example:
120
+
121
+ class App < Roda
122
+ plugin :named_templates
123
+
124
+ template :layout do
125
+ "<html><body><%= yield %></body></html>"
126
+ end
127
+ template :index do
128
+ "<p>Hello <%= @user %>!</p>"
129
+ end
130
+
131
+ route do |r|
132
+ @user = 'You'
133
+ render(:index)
134
+ end
135
+ # => "<html><body><p>Hello You!</p></body></html>"
136
+ end
137
+
138
+ * A multi_run plugin has been added, for dispatching to multiple
139
+ rack applications based on the request path prefix. This
140
+ provides a similar API as the multi_route plugin, but allows
141
+ you to separate your applications per routing subtree, as
142
+ opposed to multi_route which uses the same application for
143
+ all routing subtrees.
144
+
145
+ With the multi_run plugin, you call the class level run method
146
+ with the routing prefix and the rack application to use, and
147
+ you call r.multi_run to dispatch to all of the applications
148
+ based on the prefix.
149
+
150
+ class App < Roda
151
+ plugin :multi_run
152
+
153
+ run "foo", Foo
154
+ run "bar", Bar
155
+ run "baz", Baz
156
+
157
+ route do |r|
158
+ r.multi_run
159
+ end
160
+ end
161
+
162
+ In this case, Foo, Bar, and Baz, can be subclasses of App, which
163
+ allows them to share methods that should be shared, but still
164
+ define methods themselves that are not shared by the other
165
+ applications.
166
+
167
+ * A sinatra_helpers plugin has been added, that ports over most
168
+ of the Sinatra::Helpers methods that haven't already been added
169
+ by other plugins. All of the methods are added either to the
170
+ request or response class as appropriate. By default, delegate
171
+ methods are also added to the route block scope, but you can
172
+ turn this off by passing a :delegate=>false option when loading
173
+ the plugin, which avoids polluting the route block namespace.
174
+
175
+ The sinatra_helpers plugin adds the following request methods:
176
+
177
+ * back
178
+ * error
179
+ * not_found
180
+ * uri
181
+ * send_file
182
+
183
+ And the following response methods:
184
+
185
+ * body
186
+ * body=
187
+ * status
188
+ * headers
189
+ * mime_type
190
+ * content_type
191
+ * attachment
192
+ * informational?
193
+ * success?
194
+ * redirect?
195
+ * client_error?
196
+ * not_found?
197
+ * server_error?
198
+
199
+ * A slash_path_empty plugin has been added, which changes Roda
200
+ so that "/" is considered an empty path when doing a
201
+ terminal match via r.is or r.get/r.post with a path.
202
+
203
+ class App < Roda
204
+ plugin :slash_path_empty
205
+
206
+ route do |r|
207
+ r.get "albums" do
208
+ # matches both GET /albums and GET /albums/
209
+ end
210
+ end
211
+ end
212
+
213
+ * An empty_root plugin has been added, which makes r.root match
214
+ the empty string, in addition to /. This can be useful in
215
+ cases where a partial match on the patch has been completed.
216
+
217
+ class App < Roda
218
+ plugin :empty_root
219
+
220
+ route do |r|
221
+ r.on "albums" do
222
+ r.root do
223
+ # matches both GET /albums and GET /albums/
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ * A match_affix plugin has been added, for overriding the default
230
+ prefix/suffix used in match patterns. For example, if you want
231
+ to require that a leading / be specified in your routes. and
232
+ you want to consume any trailing slash:
233
+
234
+ class App < Roda
235
+ plugin :match_affix, "", /(\/|\z)/
236
+
237
+ route do |r|
238
+ r.on "/albums" do |s|
239
+ # GET /albums # s => ""
240
+ # GET /albums/ # s => "/"
241
+ end
242
+ end
243
+ end
244
+
245
+ * An environments plugin has been added, giving some simple
246
+ helpers for executing code in different environments. Example:
247
+
248
+ class App < Roda
249
+ plugin :environments
250
+
251
+ environment # => :development
252
+ development? # => true
253
+ test? # => false
254
+ production? # => false
255
+
256
+ # Set the environment for the application
257
+ self.environment = :test
258
+ test? # => true
259
+
260
+ configure do
261
+ # called, as no environments given
262
+ end
263
+
264
+ configure :development, :production do
265
+ # not called, as no environments match
266
+ end
267
+
268
+ configure :test do
269
+ # called, as environment given matches current environment
270
+ end
271
+ end
272
+
273
+ * A drop_body plugin has been added, which automatically drops the
274
+ body, Content-Type header, and Content-Length header when the
275
+ response status indicates no body (100-102, 204, 205, 304).
276
+
277
+ * A delay_build plugin has been added, which delays building the
278
+ rack application until Roda.app is called, and only rebuilds the
279
+ rack application if build! is called. This removes O(n^2)
280
+ performance in the pathological case of adding a route block
281
+ and then calling Roda.use many times to add middlewares, though
282
+ you have to add a few hundred middlewares for the difference
283
+ to be noticeable.
284
+
285
+ = New Features
286
+
287
+ * r.remaining_path and r.matched_path have been added for returning
288
+ the remaining path that will be used for matching, and for
289
+ returning the path already matched. Currently, these just provide
290
+ the PATH_INFO and SCRIPT_NAME, but starting in Roda 2 PATH_INFO
291
+ and SCRIPT_NAME will not be modified during routing, and you'll
292
+ need to use these methods if you want to find out the remaining
293
+ or already matched paths.
294
+
295
+ * The render plugin now supports a :template option to render/view
296
+ to specify the template to use, instead of requiring a separate
297
+ argument.
298
+
299
+ * The render plugin now supports a :template_class option, allowing
300
+ you to override the default template class that Roda would use.
301
+
302
+ * The render plugin now supports a :template_block option, specifying
303
+ the block to pass when creating a template.
304
+
305
+ * The path class method added by the path plugin now accepts :name,
306
+ :url, :url_only, and :add_script_name options:
307
+
308
+ :name :: Specifies name for method
309
+ :url :: Creates a url method in addition to a path method
310
+ :url_only :: Only creates a url method, not a path method
311
+ :add_script_name :: prefixes the path with SCRIPT_NAME
312
+
313
+ Note that if you plan to use :add_script_name, you should use
314
+ the static_path_info plugin so that the method created does not
315
+ return different results depending on where you are in the
316
+ routing tree.
317
+
318
+ * A :user_agent hash matcher has been added to the header_matchers
319
+ plugin.
320
+
321
+ * An inherit_middleware class accessor has been added. This can
322
+ be set to false if you do not want subclasses to inherit
323
+ middleware from the superclass. This is useful if the
324
+ superclass dispatches to the subclass via r.run, as otherwise
325
+ it would have to run the the same middleware stack twice.
326
+
327
+ * A clear_middleware! class accessor has been added, allowing
328
+ you to clear the current middleware stack.
329
+
330
+ * RodaRequest#default_redirect_status has been added, allowing
331
+ plugins to override the default status used for redirect if
332
+ a status is not given.
333
+
334
+ * Roda{Request,Response}#roda_class has been added, which
335
+ returns the Roda class related to the given request/response.
336
+
337
+ = Other Improvements
338
+
339
+ * The render plugin no longer caches templates by default if
340
+ RACK_ENV is development.
341
+
342
+ * When subclassing a Roda app, unfrozen Array/Hash entries in the
343
+ opts hash are now duped into the subclass, so the subclass
344
+ no longer needs to dup them manually. Note that plugins that
345
+ use nested arrays/hashes in the opts hash still need to dup
346
+ manually inside ClassMethods#inherited. For the plugins where
347
+ it is possible, it is recommended to store plugin options in a
348
+ frozen object in the opts hash, and require loading the plugin
349
+ again to modify the plugin options.
350
+
351
+ * Caching of templates is now fixed when the render/view :opts is
352
+ used to specify template options per-call.
353
+
354
+ * An explicit :default_encoding of nil in the render plugin's
355
+ :opts hash is no longer overwritten with
356
+ Encoding.default_external.
357
+
358
+ * Roda#session now returns the same object as RodaRequest#session.
359
+
360
+ * The view_subdirs, content_for, and render_each plugins now all
361
+ depend on the render plugin.
362
+
363
+ * The not_allowed plugin now depends on the all_verbs plugin.
364
+
365
+ * Local/instance variables are now used in more places instead of
366
+ method calls, improving performance.
367
+
368
+ = Backwards Compatibility
369
+
370
+ * The render plugin's render/view methods no longer pass the given
371
+ hash directly to the underlying template. To pass options to the
372
+ template engine, use a separate hash under the :opts key:
373
+
374
+ render :file, :opts=>{:foo=>'bar'}
375
+
376
+ This is more consistent with the class-level render plugin options,
377
+ which also uses :opts to pass options to the template engine.
378
+
379
+ The :js_opts and :css_opts options to the assets plugin are now
380
+ passed as the :opts hash, so they continue to affect the template
381
+ engine, so they no longer specify general render method options.
382
+
383
+ * Modifying render_opts :layout after loading the render plugin
384
+ now has no effect. You need to use plugin :render, :layout=>'...'
385
+ to set the layout to use now.
386
+
387
+ * Default headers are not set on a response until the response is
388
+ finished. This allows you to check for header presence during
389
+ routing to detect whether the header was specifically set for the
390
+ current request.
391
+
392
+ * RodaRequest.consume_pattern no longer captures anything by default.
393
+ Previously, it did so in order to update SCRIPT_NAME, but that is
394
+ now handled differently. This should only affect external plugins
395
+ that attempt to override RodaRequest#consume.
396
+
397
+ * RodaRequest.def_verb_method has been removed.
398
+
399
+ * The hooks, default_headers, json, and multi_route plugins all store
400
+ their class-level metadata in the opts hash instead of separate
401
+ class instance variables. This should have no affect unless you
402
+ were accessing the class instance variables directly.
403
+
404
+ * The render plugin internals changed significantly, it now passes
405
+ internal data using a hash. This should only affect users that
406
+ were overriding render plugin methods.
@@ -54,6 +54,7 @@ class Roda
54
54
  end
55
55
 
56
56
  @app = nil
57
+ @inherit_middleware = true
57
58
  @middleware = []
58
59
  @opts = {}
59
60
  @route_block = nil
@@ -89,13 +90,16 @@ class Roda
89
90
  # Methods are put into a plugin so future plugins can easily override
90
91
  # them and call super to get the default behavior.
91
92
  module Base
92
- SESSION_KEY = 'rack.session'.freeze
93
-
94
93
  # Class methods for the Roda class.
95
94
  module ClassMethods
96
95
  # The rack application that this class uses.
97
96
  attr_reader :app
98
97
 
98
+ # Whether middleware from the current class should be inherited by subclasses.
99
+ # True by default, should be set to false when using a design where the parent
100
+ # class accepts requests and uses run to dispatch the request to a subclass.
101
+ attr_accessor :inherit_middleware
102
+
99
103
  # The settings/options hash for the current class.
100
104
  attr_reader :opts
101
105
 
@@ -110,6 +114,12 @@ class Roda
110
114
  app.call(env)
111
115
  end
112
116
 
117
+ # Clear the middleware stack
118
+ def clear_middleware!
119
+ @middleware.clear
120
+ build_rack_app
121
+ end
122
+
113
123
  # Create a match_#{key} method in the request class using the given
114
124
  # block, so that using a hash key in a request match method will
115
125
  # call the block. The block should return nil or false to not
@@ -134,8 +144,14 @@ class Roda
134
144
  # and setup the request and response subclasses.
135
145
  def inherited(subclass)
136
146
  super
137
- subclass.instance_variable_set(:@middleware, @middleware.dup)
147
+ subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware)
148
+ subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : [])
138
149
  subclass.instance_variable_set(:@opts, opts.dup)
150
+ subclass.opts.to_a.each do |k,v|
151
+ if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen?
152
+ subclass.opts[k] = v.dup
153
+ end
154
+ end
139
155
  subclass.instance_variable_set(:@route_block, @route_block)
140
156
  subclass.send(:build_rack_app)
141
157
 
@@ -155,36 +171,36 @@ class Roda
155
171
  #
156
172
  # Roda.plugin PluginModule
157
173
  # Roda.plugin :csrf
158
- def plugin(mixin, *args, &block)
159
- if mixin.is_a?(Symbol)
160
- mixin = RodaPlugins.load_plugin(mixin)
174
+ def plugin(plugin, *args, &block)
175
+ if plugin.is_a?(Symbol)
176
+ plugin = RodaPlugins.load_plugin(plugin)
161
177
  end
162
178
 
163
- if mixin.respond_to?(:load_dependencies)
164
- mixin.load_dependencies(self, *args, &block)
179
+ if plugin.respond_to?(:load_dependencies)
180
+ plugin.load_dependencies(self, *args, &block)
165
181
  end
166
182
 
167
- if defined?(mixin::InstanceMethods)
168
- include mixin::InstanceMethods
183
+ if defined?(plugin::InstanceMethods)
184
+ include(plugin::InstanceMethods)
169
185
  end
170
- if defined?(mixin::ClassMethods)
171
- extend mixin::ClassMethods
186
+ if defined?(plugin::ClassMethods)
187
+ extend(plugin::ClassMethods)
172
188
  end
173
- if defined?(mixin::RequestMethods)
174
- self::RodaRequest.send(:include, mixin::RequestMethods)
189
+ if defined?(plugin::RequestMethods)
190
+ self::RodaRequest.send(:include, plugin::RequestMethods)
175
191
  end
176
- if defined?(mixin::RequestClassMethods)
177
- self::RodaRequest.extend mixin::RequestClassMethods
192
+ if defined?(plugin::RequestClassMethods)
193
+ self::RodaRequest.extend(plugin::RequestClassMethods)
178
194
  end
179
- if defined?(mixin::ResponseMethods)
180
- self::RodaResponse.send(:include, mixin::ResponseMethods)
195
+ if defined?(plugin::ResponseMethods)
196
+ self::RodaResponse.send(:include, plugin::ResponseMethods)
181
197
  end
182
- if defined?(mixin::ResponseClassMethods)
183
- self::RodaResponse.extend mixin::ResponseClassMethods
198
+ if defined?(plugin::ResponseClassMethods)
199
+ self::RodaResponse.extend(plugin::ResponseClassMethods)
184
200
  end
185
201
 
186
- if mixin.respond_to?(:configure)
187
- mixin.configure(self, *args, &block)
202
+ if plugin.respond_to?(:configure)
203
+ plugin.configure(self, *args, &block)
188
204
  end
189
205
  end
190
206
 
@@ -315,7 +331,7 @@ class Roda
315
331
  #
316
332
  # env['REQUEST_METHOD'] # => 'GET'
317
333
  def env
318
- request.env
334
+ @_request.env
319
335
  end
320
336
 
321
337
  # The class-level options hash. This should probably not be
@@ -340,10 +356,12 @@ class Roda
340
356
  @_response
341
357
  end
342
358
 
343
- # The session for the current request. Raises a RodaError if
344
- # a session handler has not been loaded.
359
+ # The session hash for the current request. Raises RodaError
360
+ # if no session existsExample:
361
+ #
362
+ # session # => {}
345
363
  def session
346
- env[SESSION_KEY] || raise(RodaError, "You're missing a session handler. You can get started by adding use Rack::Session::Cookie")
364
+ @_request.session
347
365
  end
348
366
 
349
367
  private
@@ -352,8 +370,9 @@ class Roda
352
370
  # behavior after the request and response have been setup.
353
371
  def _route(&block)
354
372
  catch(:halt) do
355
- request.block_result(instance_exec(@_request, &block))
356
- response.finish
373
+ r = @_request
374
+ r.block_result(instance_exec(r, &block))
375
+ @_response.finish
357
376
  end
358
377
  end
359
378
  end
@@ -379,17 +398,6 @@ class Roda
379
398
  pattern
380
399
  end
381
400
 
382
- # Define a verb method in the given that will yield to the match block
383
- # if the request method matches and there are either no arguments or
384
- # there is a successful terminal match on the arguments.
385
- def def_verb_method(mod, verb)
386
- mod.class_eval(<<-END, __FILE__, __LINE__+1)
387
- def #{verb}(*args, &block)
388
- _verb(args, &block) if #{verb == :get ? :is_get : verb}?
389
- end
390
- END
391
- end
392
-
393
401
  # Since RodaRequest is anonymously subclassed when Roda is subclassed,
394
402
  # and then assigned to a constant of the Roda subclass, make inspect
395
403
  # reflect the likely name for the class.
@@ -403,7 +411,7 @@ class Roda
403
411
  # pattern requires the path starts with a string and does not match partial
404
412
  # segments.
405
413
  def consume_pattern(pattern)
406
- /\A(\/(?:#{pattern}))(?=\/|\z)/
414
+ /\A\/(?:#{pattern})(?=\/|\z)/
407
415
  end
408
416
  end
409
417
 
@@ -418,6 +426,7 @@ class Roda
418
426
  SEGMENT = "([^\\/]+)".freeze
419
427
  TERM_INSPECT = "TERM".freeze
420
428
  GET_REQUEST_METHOD = 'GET'.freeze
429
+ SESSION_KEY = 'rack.session'.freeze
421
430
 
422
431
  TERM = Object.new
423
432
  def TERM.inspect
@@ -440,15 +449,20 @@ class Roda
440
449
  super(env)
441
450
  end
442
451
 
443
- # As request routing modifies SCRIPT_NAME and PATH_INFO, this exists
444
- # as a helper method to get the full path of the request.
445
- #
446
- # r.env['SCRIPT_NAME'] = '/foo'
447
- # r.env['PATH_INFO'] = '/bar'
448
- # r.full_path_info
449
- # # => '/foo/bar'
450
- def full_path_info
451
- "#{@env[SCRIPT_NAME]}#{@env[PATH_INFO]}"
452
+ # Handle match block return values. By default, if a string is given
453
+ # and the response is empty, use the string as the response body.
454
+ def block_result(result)
455
+ res = response
456
+ if res.empty? && (body = block_result_body(result))
457
+ res.write(body)
458
+ end
459
+ end
460
+
461
+ # Match GET requests. If no arguments are provided, matches all GET
462
+ # requests, otherwise, matches only GET requests where the arguments
463
+ # given fully consume the path.
464
+ def get(*args, &block)
465
+ _verb(args, &block) if is_get?
452
466
  end
453
467
 
454
468
  # Immediately stop execution of the route block and return the given
@@ -465,29 +479,13 @@ class Roda
465
479
  throw :halt, res
466
480
  end
467
481
 
468
- # Optimized method for whether this request is a +GET+ request.
469
- # Similar to the default Rack::Request get? method, but can be
470
- # overridden without changing rack's behavior.
471
- def is_get?
472
- @env[REQUEST_METHOD] == GET_REQUEST_METHOD
473
- end
474
-
475
- # Handle match block return values. By default, if a string is given
476
- # and the response is empty, use the string as the response body.
477
- def block_result(result)
478
- res = response
479
- if res.empty? && (body = block_result_body(result))
480
- res.write(body)
481
- end
482
- end
483
-
484
482
  # Show information about current request, including request class,
485
483
  # request method and full path.
486
484
  #
487
485
  # r.inspect
488
486
  # # => '#<Roda::RodaRequest GET /foo/bar>'
489
487
  def inspect
490
- "#<#{self.class.inspect} #{@env[REQUEST_METHOD]} #{full_path_info}>"
488
+ "#<#{self.class.inspect} #{@env[REQUEST_METHOD]} #{path}>"
491
489
  end
492
490
 
493
491
  # Does a terminal match on the current path, matching only if the arguments
@@ -535,7 +533,7 @@ class Roda
535
533
  # end
536
534
  def is(*args, &block)
537
535
  if args.empty?
538
- if @env[PATH_INFO] == EMPTY_STRING
536
+ if empty_path?
539
537
  always(&block)
540
538
  end
541
539
  else
@@ -544,6 +542,13 @@ class Roda
544
542
  end
545
543
  end
546
544
 
545
+ # Optimized method for whether this request is a +GET+ request.
546
+ # Similar to the default Rack::Request get? method, but can be
547
+ # overridden without changing rack's behavior.
548
+ def is_get?
549
+ @env[REQUEST_METHOD] == GET_REQUEST_METHOD
550
+ end
551
+
547
552
  # Does a match on the path, matching only if the arguments
548
553
  # have matched the path. Because this doesn't fully match the
549
554
  # path, this is usually used to setup branches of the routing tree,
@@ -579,14 +584,34 @@ class Roda
579
584
  end
580
585
  end
581
586
 
582
- # The response related to the current request. See ResponseMethods for
583
- # instance methods for the response, but in general the most common usage
584
- # is to override the response status and headers:
587
+ # The already matched part of the path, including the original SCRIPT_NAME.
588
+ def matched_path
589
+ @env[SCRIPT_NAME]
590
+ end
591
+
592
+ # This an an optimized version of Rack::Request#path.
585
593
  #
586
- # response.status = 200
587
- # response['Header-Name'] = 'Header value'
588
- def response
589
- scope.response
594
+ # r.env['SCRIPT_NAME'] = '/foo'
595
+ # r.env['PATH_INFO'] = '/bar'
596
+ # r.path
597
+ # # => '/foo/bar'
598
+ def path
599
+ e = @env
600
+ "#{e[SCRIPT_NAME]}#{e[PATH_INFO]}"
601
+ end
602
+ alias full_path_info path
603
+
604
+ # The current path to match requests against. This is the same as PATH_INFO
605
+ # in the environment, which gets updated as the request is being routed.
606
+ def remaining_path
607
+ @env[PATH_INFO]
608
+ end
609
+
610
+ # Match POST requests. If no arguments are provided, matches all POST
611
+ # requests, otherwise, matches only POST requests where the arguments
612
+ # given fully consume the path.
613
+ def post(*args, &block)
614
+ _verb(args, &block) if post?
590
615
  end
591
616
 
592
617
  # Immediately redirect to the path using the status code. This ends
@@ -612,11 +637,26 @@ class Roda
612
637
  # r.redirect
613
638
  # end
614
639
  # end
615
- def redirect(path=default_redirect_path, status=302)
640
+ def redirect(path=default_redirect_path, status=default_redirect_status)
616
641
  response.redirect(path, status)
617
642
  throw :halt, response.finish
618
643
  end
619
644
 
645
+ # The response related to the current request. See ResponseMethods for
646
+ # instance methods for the response, but in general the most common usage
647
+ # is to override the response status and headers:
648
+ #
649
+ # response.status = 200
650
+ # response['Header-Name'] = 'Header value'
651
+ def response
652
+ scope.response
653
+ end
654
+
655
+ # Return the Roda class related to this request.
656
+ def roda_class
657
+ self.class.roda_class
658
+ end
659
+
620
660
  # Routing matches that only matches +GET+ requests where the current
621
661
  # path is +/+. If it matches, the match block is executed, and when
622
662
  # the match block returns, the rack response is returned.
@@ -665,7 +705,7 @@ class Roda
665
705
  # Use <tt>r.get true</tt> to handle +GET+ requests where the current
666
706
  # path is empty.
667
707
  def root(&block)
668
- if @env[PATH_INFO] == SLASH && is_get?
708
+ if remaining_path == SLASH && is_get?
669
709
  always(&block)
670
710
  end
671
711
  end
@@ -681,6 +721,12 @@ class Roda
681
721
  throw :halt, app.call(@env)
682
722
  end
683
723
 
724
+ # The session for the current request. Raises a RodaError if
725
+ # a session handler has not been loaded.
726
+ def session
727
+ @env[SESSION_KEY] || raise(RodaError, "You're missing a session handler. You can get started by adding use Rack::Session::Cookie")
728
+ end
729
+
684
730
  private
685
731
 
686
732
  # Match any of the elements in the given array. Return at the
@@ -690,7 +736,7 @@ class Roda
690
736
  matcher.any? do |m|
691
737
  if matched = match(m)
692
738
  if m.is_a?(String)
693
- captures.push(m)
739
+ @captures.push(m)
694
740
  end
695
741
  end
696
742
 
@@ -698,16 +744,16 @@ class Roda
698
744
  end
699
745
  end
700
746
 
701
- # Match the given regexp exactly if it matches a full segment.
702
- def _match_regexp(re)
703
- consume(self.class.cached_matcher(re){re})
704
- end
705
-
706
747
  # Match the given hash if all hash matchers match.
707
748
  def _match_hash(hash)
708
749
  hash.all?{|k,v| send("match_#{k}", v)}
709
750
  end
710
751
 
752
+ # Match the given regexp exactly if it matches a full segment.
753
+ def _match_regexp(re)
754
+ consume(self.class.cached_matcher(re){re})
755
+ end
756
+
711
757
  # Match the given string to the request path. Regexp escapes the
712
758
  # string so that regexp metacharacters are not matched, and recognizes
713
759
  # colon tokens for placeholders.
@@ -757,16 +803,10 @@ class Roda
757
803
  # SCRIPT_NAME to include the matched path, removes the matched
758
804
  # path from PATH_INFO, and updates captures with any regex captures.
759
805
  def consume(pattern)
760
- env = @env
761
- return unless matchdata = env[PATH_INFO].match(pattern)
762
-
763
- vars = matchdata.captures
764
-
765
- # Don't mutate SCRIPT_NAME, breaks try
766
- env[SCRIPT_NAME] += vars.shift
767
- env[PATH_INFO] = matchdata.post_match
768
-
769
- captures.concat(vars)
806
+ if matchdata = remaining_path.match(pattern)
807
+ update_remaining_path(matchdata.post_match)
808
+ @captures.concat(matchdata.captures)
809
+ end
770
810
  end
771
811
 
772
812
  # The default path to use for redirects when a path is not given.
@@ -779,29 +819,47 @@ class Roda
779
819
  # it is easy to create an infinite redirect.
780
820
  def default_redirect_path
781
821
  raise RodaError, "must provide path argument to redirect for get requests" if is_get?
782
- full_path_info
822
+ path
823
+ end
824
+
825
+ # The default status to use for redirects if a status is not provided,
826
+ # 302 by default.
827
+ def default_redirect_status
828
+ 302
829
+ end
830
+
831
+ # Whether the current path is considered empty.
832
+ def empty_path?
833
+ remaining_path == EMPTY_STRING
783
834
  end
784
835
 
785
836
  # If all of the arguments match, yields to the match block and
786
837
  # returns the rack response when the block returns. If any of
787
838
  # the match arguments doesn't match, does nothing.
788
839
  def if_match(args)
840
+ keep_remaining_path do
841
+ # For every block, we make sure to reset captures so that
842
+ # nesting matchers won't mess with each other's captures.
843
+ @captures.clear
844
+
845
+ return unless match_all(args)
846
+ block_result(yield(*captures))
847
+ throw :halt, response.finish
848
+ end
849
+ end
850
+
851
+ # Yield to the block, restoring SCRIPT_NAME and PATH_INFO to
852
+ # their initial values before returning from the block.
853
+ def keep_remaining_path
789
854
  env = @env
790
- script = env[SCRIPT_NAME]
791
- path = env[PATH_INFO]
792
-
793
- # For every block, we make sure to reset captures so that
794
- # nesting matchers won't mess with each other's captures.
795
- captures.clear
796
-
797
- return unless match_all(args)
798
- block_result(yield(*captures))
799
- throw :halt, response.finish
855
+ script = env[sn = SCRIPT_NAME]
856
+ path = env[pi = PATH_INFO]
857
+ yield
800
858
  ensure
801
- env[SCRIPT_NAME] = script
802
- env[PATH_INFO] = path
859
+ env[sn] = script
860
+ env[pi] = path
803
861
  end
804
-
862
+
805
863
  # Attempt to match the argument to the given request, handling
806
864
  # common ruby types.
807
865
  def match(matcher)
@@ -813,7 +871,7 @@ class Roda
813
871
  when Symbol
814
872
  _match_symbol(matcher)
815
873
  when TERM
816
- @env[PATH_INFO] == EMPTY_STRING
874
+ empty_path?
817
875
  when Hash
818
876
  _match_hash(matcher)
819
877
  when Array
@@ -850,7 +908,7 @@ class Roda
850
908
  # Adds any match to the captures.
851
909
  def match_param(key)
852
910
  if v = self[key]
853
- captures << v
911
+ @captures << v
854
912
  end
855
913
  end
856
914
 
@@ -858,9 +916,18 @@ class Roda
858
916
  # Adds any match to the captures.
859
917
  def match_param!(key)
860
918
  if (v = self[key]) && !v.empty?
861
- captures << v
919
+ @captures << v
862
920
  end
863
921
  end
922
+
923
+ # Update PATH_INFO and SCRIPT_NAME based on the matchend and remaining variables.
924
+ def update_remaining_path(remaining)
925
+ e = @env
926
+
927
+ # Don't mutate SCRIPT_NAME, breaks try
928
+ e[SCRIPT_NAME] += e[pi = PATH_INFO].chomp(remaining)
929
+ e[pi] = remaining
930
+ end
864
931
  end
865
932
 
866
933
  # Class methods for RodaResponse
@@ -879,21 +946,20 @@ class Roda
879
946
  # Instance methods for RodaResponse
880
947
  module ResponseMethods
881
948
  CONTENT_LENGTH = "Content-Length".freeze
882
- CONTENT_TYPE = "Content-Type".freeze
883
- DEFAULT_CONTENT_TYPE = "text/html".freeze
949
+ DEFAULT_HEADERS = {"Content-Type" => "text/html".freeze}.freeze
884
950
  LOCATION = "Location".freeze
885
951
 
952
+ # The hash of response headers for the current response.
953
+ attr_reader :headers
954
+
886
955
  # The status code to use for the response. If none is given, will use 200
887
956
  # code for non-empty responses and a 404 code for empty responses.
888
957
  attr_accessor :status
889
958
 
890
- # The hash of response headers for the current response.
891
- attr_reader :headers
892
-
893
959
  # Set the default headers when creating a response.
894
960
  def initialize
895
961
  @status = nil
896
- @headers = default_headers
962
+ @headers = {}
897
963
  @body = []
898
964
  @length = 0
899
965
  end
@@ -912,14 +978,9 @@ class Roda
912
978
  @headers[key] = value
913
979
  end
914
980
 
915
- # Show response class, status code, response headers, and response body
916
- def inspect
917
- "#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>"
918
- end
919
-
920
981
  # The default headers to use for responses.
921
982
  def default_headers
922
- {CONTENT_TYPE => DEFAULT_CONTENT_TYPE}
983
+ DEFAULT_HEADERS
923
984
  end
924
985
 
925
986
  # Modify the headers to include a Set-Cookie value that
@@ -959,8 +1020,9 @@ class Roda
959
1020
  def finish
960
1021
  b = @body
961
1022
  s = (@status ||= b.empty? ? 404 : 200)
1023
+ set_default_headers
962
1024
  h = @headers
963
- h[CONTENT_LENGTH] = @length.to_s
1025
+ h[CONTENT_LENGTH] ||= @length.to_s
964
1026
  [s, h, b]
965
1027
  end
966
1028
 
@@ -969,9 +1031,15 @@ class Roda
969
1031
  # and doesn't add the Content-Length header or use the existing
970
1032
  # body.
971
1033
  def finish_with_body(body)
1034
+ set_default_headers
972
1035
  [@status || 200, @headers, body]
973
1036
  end
974
1037
 
1038
+ # Show response class, status code, response headers, and response body
1039
+ def inspect
1040
+ "#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>"
1041
+ end
1042
+
975
1043
  # Set the Location header to the given path, and the status
976
1044
  # to the given status. Example:
977
1045
  #
@@ -982,6 +1050,11 @@ class Roda
982
1050
  @status = status
983
1051
  end
984
1052
 
1053
+ # Return the Roda class related to this response.
1054
+ def roda_class
1055
+ self.class.roda_class
1056
+ end
1057
+
985
1058
  # Set the cookie with the given key in the headers.
986
1059
  #
987
1060
  # response.set_cookie('foo', 'bar')
@@ -999,12 +1072,21 @@ class Roda
999
1072
  @body << s
1000
1073
  nil
1001
1074
  end
1075
+
1076
+ private
1077
+
1078
+ # For each default header, if a header has not already been set for the
1079
+ # response, set the header in the response.
1080
+ def set_default_headers
1081
+ h = @headers
1082
+ default_headers.each do |k,v|
1083
+ h[k] ||= v
1084
+ end
1085
+ end
1002
1086
  end
1003
1087
  end
1004
1088
  end
1005
1089
 
1006
1090
  extend RodaPlugins::Base::ClassMethods
1007
1091
  plugin RodaPlugins::Base
1008
- RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :get)
1009
- RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :post)
1010
1092
  end