roda 1.1.0 → 1.2.0

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