roda 1.3.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +24 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -4
  5. data/doc/release_notes/2.0.0.txt +75 -0
  6. data/lib/roda/plugins/assets.rb +2 -2
  7. data/lib/roda/plugins/backtracking_array.rb +2 -11
  8. data/lib/roda/plugins/caching.rb +4 -2
  9. data/lib/roda/plugins/chunked.rb +4 -9
  10. data/lib/roda/plugins/class_level_routing.rb +1 -3
  11. data/lib/roda/plugins/default_headers.rb +1 -2
  12. data/lib/roda/plugins/error_email.rb +4 -14
  13. data/lib/roda/plugins/error_handler.rb +4 -4
  14. data/lib/roda/plugins/flash.rb +1 -3
  15. data/lib/roda/plugins/halt.rb +24 -5
  16. data/lib/roda/plugins/header_matchers.rb +2 -7
  17. data/lib/roda/plugins/hooks.rb +1 -3
  18. data/lib/roda/plugins/json.rb +4 -2
  19. data/lib/roda/plugins/mailer.rb +8 -7
  20. data/lib/roda/plugins/middleware.rb +21 -9
  21. data/lib/roda/plugins/not_found.rb +3 -3
  22. data/lib/roda/plugins/padrino_render.rb +60 -0
  23. data/lib/roda/plugins/param_matchers.rb +3 -3
  24. data/lib/roda/plugins/path.rb +2 -1
  25. data/lib/roda/plugins/render.rb +55 -37
  26. data/lib/roda/plugins/render_each.rb +4 -2
  27. data/lib/roda/plugins/static_path_info.rb +2 -63
  28. data/lib/roda/plugins/streaming.rb +4 -2
  29. data/lib/roda/version.rb +2 -2
  30. data/lib/roda.rb +71 -172
  31. data/spec/matchers_spec.rb +31 -82
  32. data/spec/plugin/assets_spec.rb +6 -6
  33. data/spec/plugin/error_handler_spec.rb +23 -0
  34. data/spec/plugin/halt_spec.rb +39 -0
  35. data/spec/plugin/middleware_spec.rb +7 -0
  36. data/spec/plugin/padrino_render_spec.rb +57 -0
  37. data/spec/plugin/render_each_spec.rb +1 -1
  38. data/spec/plugin/render_spec.rb +59 -5
  39. data/spec/request_spec.rb +0 -12
  40. data/spec/response_spec.rb +0 -24
  41. data/spec/views/_test.erb +1 -0
  42. metadata +7 -4
  43. data/lib/roda/plugins/delete_nil_headers.rb +0 -34
  44. data/spec/module_spec.rb +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1ec447de1d1c9201190d3e05cddc9ca3aca957e4
4
- data.tar.gz: fc6e061b744a3ed47259369907bb82e222d12515
3
+ metadata.gz: 035bdb54b566e77c48d1332fdd7ebf396a849193
4
+ data.tar.gz: 682a8a39b4dc9e25d1395102890f8650f750e12d
5
5
  SHA512:
6
- metadata.gz: a5d0a587873870952a81b8df6a0828c76d68e2de2ff6d388f1ccbf82da5b45815722460ba51a540d1cf74f11fa7911b45d6e7928515901095eb08a8d76754ade
7
- data.tar.gz: 33f7d3837fdf5b846169597868531493e00cf1089b2d68771e2b9ea1345603adb3462ca8283735ab393721ddb461467a0813cb9b05d13cdda869d8d62aaf143f
6
+ metadata.gz: c219eb5e94a58c04d90256d87cbe5a86c1dc8c972666cbf61d97a34c5f3b4bc8d6b7de56fdee7aa65ef01ba70d7f4052d479c80211f49201dc94705e17e69308
7
+ data.tar.gz: ba8d204672492c04de406eb87d276c59e92af57047c4cc6bb479e7fc9efb1785fd314df682e496bc905327450571497f77581fef04a29346159de747172f07bc
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ = 2.0.0 (2015-02-13)
2
+
3
+ * Allow Roda app to be used as a regular rack app even when using the middleware plugin (jeremyevans)
4
+
5
+ * Make render plugin :layout option always be true or false (jeremyevans)
6
+
7
+ * Make :layout=>true view option use the default layout (jeremyevans)
8
+
9
+ * Make error_handler plugin rescue ScriptError in addition to StandardError (jeremyevans)
10
+
11
+ * Make halt plugin integrate with symbol_views, json, and similar plugins (jeremyevans)
12
+
13
+ * Add padrino_render plugin, adding render/partial methods that work similar to Padrino (jeremyevans)
14
+
15
+ * Add Roda#render_template private method for template rendering, for use by plugins (jeremyevans)
16
+
17
+ * Make Roda#initialize take env hash, #call take route_block, remove private #_route (jeremyevans)
18
+
19
+ * Remove keep_remaining_path/update_remaining_path private request methods (jeremyevans)
20
+
21
+ * Don't modify SCRIPT_NAME/PATH_INFO during routing, merging static_path_info plugin into core (jeremyevans)
22
+
23
+ * Remove code deprecated in Roda 1.3.0 (jeremyevans)
24
+
1
25
  = 1.3.0 (2015-01-13)
2
26
 
3
27
  * Make static_path_info plugin restore original SCRIPT_NAME/PATH_INFO before returning from r.run (jeremyevans)
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Jeremy Evans
1
+ Copyright (c) 2014-2015 Jeremy Evans
2
2
  Copyright (c) 2010, 2011 Michel Martens, Damian Janowski and Cyril David
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
data/README.rdoc CHANGED
@@ -292,7 +292,8 @@ Colons that are not followed by a <tt>\\w</tt> character are matched literally:
292
292
 
293
293
  ":/a" # matches "/:/a"
294
294
 
295
- Note that strings must be escaped before being used in a regular expression, so:
295
+ Note that other than colons, strings do no handle regular expression syntax, the
296
+ string is matched verbatim:
296
297
 
297
298
  "\\d+(/\\w+)?" # matches "/\d+(/\w+)?"
298
299
  "\\d+(/\\w+)?" # does not match "/123/abc"
@@ -499,12 +500,14 @@ you can use the module_include plugin.
499
500
 
500
501
  Roda tries very hard to avoid polluting the scope of the +route+ block.
501
502
  This should make it unlikely that Roda will cause namespace issues
502
- with your application code. Some of the things Roda does
503
+ with your application code. Some of the things Roda does:
503
504
 
504
505
  - The only instance variables defined by default in the scope of the +route+ block
505
- are <tt>@_request</tt> and <tt>@_response</tt>.
506
+ are <tt>@_request</tt> and <tt>@_response</tt>. All instance variables in the
507
+ scope of the +route+ block used by plugins that ship with Roda are prefixed
508
+ with an underscore.
506
509
  - The only methods defined (beyond the default methods for +Object+) are:
507
- +env+, +opts+, +request+, +response+, +call+, +session+, and +_route+ (private).
510
+ +call+, +env+, +opts+, +request+, +response+, and +session+.
508
511
  - Constants inside the Roda namespace are all prefixed with +Roda+
509
512
  (e.g., <tt>Roda::RodaRequest</tt>).
510
513
 
@@ -0,0 +1,75 @@
1
+ = Backwards Compatibility
2
+
3
+ * RodaResponse#set_cookie and #delete_cookie have been removed.
4
+
5
+ * Roda.request_module and .response_module have been removed.
6
+
7
+ * Roda.hash_matcher has been removed.
8
+
9
+ * The :extension hash matcher has been removed.
10
+
11
+ * The :param and :param! hash matchers have been removed.
12
+
13
+ * RodaRequest#full_path_info has been removed.
14
+
15
+ * The :opts render plugin option is no longer respected, Use the
16
+ :template_opts option instead.
17
+
18
+ * Plugin option hashes for the chunked, default_headers,
19
+ error_email, and render plugins are now frozen.
20
+
21
+ * The :header hash matcher in the header_matchers plugin now
22
+ yields the header value to the block.
23
+
24
+ * Roda.json_result_classes in the json plugin is now frozen.
25
+
26
+ * The PATH_INFO and SCRIPT_NAME env variables are no longer modified
27
+ during routing.
28
+
29
+ * Roda#initialize now takes an env hash, and #call now takes the
30
+ route block. The private #_route method has been removed.
31
+
32
+ * RodaRequest#keep_remaining_path/#updating_remaining_path private
33
+ methods have been removed.
34
+
35
+ * The render plugin's :layout option is now always set to true or
36
+ false, specifying whether a layout should be used by default.
37
+ The template used for a layout is now located as the :template
38
+ option inside :layout_opts.
39
+
40
+ = New Plugins
41
+
42
+ * A padrino_render plugin has been added, which adds render/partial
43
+ methods that work similarly to Padrino's.
44
+
45
+ = Other New Features
46
+
47
+ * A Roda#render_template private method has been added to the render
48
+ plugin. All internal users of render should switch to calling
49
+ render_template.
50
+
51
+ * The halt plugin now integrates with the symbol_views and json
52
+ plugins, allowing things like:
53
+
54
+ r.halt(:template)
55
+ r.halt('key'=>'value')
56
+
57
+ = Other Improvements
58
+
59
+ * The error_handler plugin now rescues ScriptError in addition to
60
+ StandardError. This handles SyntaxError (raised by ERB),
61
+ LoadError (raised by require), and NotImplementedError (raised
62
+ by TSort).
63
+
64
+ * Using a :layout=>true option to the render plugin's view method
65
+ now uses the default layout template, instead of a template named
66
+ 'true'. It can be used to force a layout even if the render
67
+ plugin has been configured to not use a layout by default.
68
+
69
+ * Roda apps that use the middleware plugin can now be used as regular
70
+ rack apps. Previously, using the middleware plugin made it
71
+ impossible to use the app as a regular rack app.
72
+
73
+ * Roda#request and #response are now faster.
74
+
75
+ * Roda avoids creating unnecessary hashes in more places now.
@@ -396,7 +396,7 @@ class Roda
396
396
  def compile_assets_files(files, type, dirs)
397
397
  dirs = nil if dirs && dirs.empty?
398
398
  o = assets_opts
399
- app = new
399
+ app = allocate
400
400
 
401
401
  content = files.map do |file|
402
402
  file = "#{dirs.join('/')}/#{file}" if dirs && o[:group_subdirs]
@@ -548,7 +548,7 @@ class Roda
548
548
  # Render the given asset file using the render plugin, with the given options.
549
549
  # +file+ should be the relative path to the file from the current directory.
550
550
  def render_asset_file(file, options)
551
- render({:path => file}, options)
551
+ render_template({:path => file}, options)
552
552
  end
553
553
  end
554
554
 
@@ -36,11 +36,7 @@ class Roda
36
36
  def _match_array(arg, rest=nil)
37
37
  return super unless rest
38
38
 
39
- unless path = @remaining_path
40
- e = @env
41
- script = e[SCRIPT_NAME]
42
- path = e[PATH_INFO]
43
- end
39
+ path = @remaining_path
44
40
  captures = @captures
45
41
  caps = captures.dup
46
42
  arg.each do |v|
@@ -55,12 +51,7 @@ class Roda
55
51
 
56
52
  # Matching all remaining elements failed, reset state
57
53
  captures.replace(caps)
58
- if @remaining_path
59
- @remaining_path = path
60
- else
61
- e[SCRIPT_NAME] = script
62
- e[PATH_INFO] = path
63
- end
54
+ @remaining_path = path
64
55
  end
65
56
  end
66
57
  false
@@ -66,6 +66,8 @@ class Roda
66
66
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
67
67
  # OTHER DEALINGS IN THE SOFTWARE.
68
68
  module Caching
69
+ OPTS = {}.freeze
70
+
69
71
  module RequestMethods
70
72
  LAST_MODIFIED = 'Last-Modified'.freeze
71
73
  HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
@@ -118,7 +120,7 @@ class Roda
118
120
  #
119
121
  # When the current request includes an If-Match header with a
120
122
  # etag that doesn't match, immediately returns a response with a 412 status.
121
- def etag(value, opts={})
123
+ def etag(value, opts=OPTS)
122
124
  # Before touching this code, please double check RFC 2616 14.24 and 14.26.
123
125
  weak = opts[:weak]
124
126
  new_resource = opts.fetch(:new_resource){post?}
@@ -194,7 +196,7 @@ class Roda
194
196
  # be an integer number of seconds that the current request should be
195
197
  # cached for. Also sets the Expires header, useful if you have
196
198
  # HTTP 1.0 clients (Cache-Control is an HTTP 1.1 header).
197
- def expires(max_age, opts={})
199
+ def expires(max_age, opts=OPTS)
198
200
  cache_control(opts.merge(:max_age=>max_age))
199
201
  self[EXPIRES] = (Time.now + max_age).httpdate
200
202
  end
@@ -142,8 +142,7 @@ class Roda
142
142
  def self.configure(app, opts=OPTS)
143
143
  app.opts[:chunk_by_default] = opts[:chunk_by_default]
144
144
  if opts[:headers]
145
- app.opts[:chunk_headers] = (app.opts[:chunk_headers] || {}).merge(opts[:headers])
146
- app.opts[:chunk_headers].extend(RodaDeprecateMutation)
145
+ app.opts[:chunk_headers] = (app.opts[:chunk_headers] || {}).merge(opts[:headers]).freeze
147
146
  end
148
147
  end
149
148
 
@@ -241,15 +240,11 @@ class Roda
241
240
  @_out_buf = ''
242
241
  end
243
242
 
244
- if layout = opts.fetch(:layout, render_opts[:layout])
245
- if layout_opts = opts[:layout_opts]
246
- layout_opts = render_opts[:layout_opts].merge(layout_opts)
247
- end
248
-
249
- @_out_buf = render(layout, layout_opts||OPTS) do
243
+ if layout_opts = view_layout_opts(opts)
244
+ @_out_buf = render_template(layout_opts) do
250
245
  flush
251
246
  block.call if block
252
- yield opts[:content] || render(template, opts)
247
+ yield opts[:content] || render_template(template, opts)
253
248
  nil
254
249
  end
255
250
  else
@@ -70,11 +70,9 @@ class Roda
70
70
  end
71
71
 
72
72
  module InstanceMethods
73
- private
74
-
75
73
  # If the normal routing tree doesn't handle an action, try each class level route
76
74
  # to see if it matches.
77
- def _route(&block)
75
+ def call
78
76
  result = super
79
77
 
80
78
  if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
@@ -20,8 +20,7 @@ class Roda
20
20
  module DefaultHeaders
21
21
  # Merge the given headers into the existing default headers, if any.
22
22
  def self.configure(app, headers={})
23
- app.opts[:default_headers] = (app.opts[:default_headers] || {}).merge(headers)
24
- app.opts[:default_headers].extend(RodaDeprecateMutation)
23
+ app.opts[:default_headers] = (app.opts[:default_headers] || {}).merge(headers).freeze
25
24
  end
26
25
 
27
26
  module ClassMethods
@@ -28,6 +28,7 @@ class Roda
28
28
  # for low traffic web applications. For high traffic web applications,
29
29
  # use an error reporting service instead of this plugin.
30
30
  module ErrorEmail
31
+ OPTS = {}.freeze
31
32
  DEFAULTS = {
32
33
  :headers=>{},
33
34
  :host=>'localhost',
@@ -74,7 +75,7 @@ END
74
75
  }
75
76
 
76
77
  # Set default opts for plugin. See ErrorEmail module RDoc for options.
77
- def self.configure(app, opts={})
78
+ def self.configure(app, opts=OPTS)
78
79
  email_opts = app.opts[:error_email] ||= DEFAULTS
79
80
  email_opts = email_opts.merge(opts)
80
81
  email_opts[:headers] = email_opts[:headers].dup
@@ -82,19 +83,8 @@ END
82
83
  raise RodaError, "must provide :to and :from options to error_email plugin"
83
84
  end
84
85
  app.opts[:error_email] = email_opts
85
- app.opts[:error_email].extend(RodaDeprecateMutation)
86
- app.opts[:error_email][:headers].extend(RodaDeprecateMutation)
87
- end
88
-
89
- module ClassMethods
90
- # Dup the error email opts in the subclass so changes to the subclass do not affect
91
- # the superclass.
92
- def inherited(subclass)
93
- super
94
- opts = subclass.opts[:error_email].dup
95
- opts[:headers] = opts[:headers].dup.extend(RodaDeprecateMutation)
96
- subclass.opts[:error_email] = opts.extend(RodaDeprecateMutation)
97
- end
86
+ app.opts[:error_email][:headers].freeze
87
+ app.opts[:error_email].freeze
98
88
  end
99
89
 
100
90
  module InstanceMethods
@@ -46,18 +46,18 @@ class Roda
46
46
  end
47
47
 
48
48
  module InstanceMethods
49
- private
50
-
51
49
  # If an error occurs, set the response status to 500 and call
52
50
  # the error handler.
53
- def _route
51
+ def call
54
52
  super
55
- rescue => e
53
+ rescue StandardError, ScriptError => e
56
54
  res = @_response = self.class::RodaResponse.new
57
55
  res.status = 500
58
56
  super{handle_error(e)}
59
57
  end
60
58
 
59
+ private
60
+
61
61
  # By default, have the error handler reraise the error, so using
62
62
  # the plugin without installing an error handler doesn't change
63
63
  # behavior.
@@ -87,11 +87,9 @@ class Roda
87
87
  @_flash ||= FlashHash.new(session[KEY])
88
88
  end
89
89
 
90
- private
91
-
92
90
  # If the routing doesn't raise an error, rotate the flash
93
91
  # hash in the session so the next request has access to it.
94
- def _route
92
+ def call
95
93
  res = super
96
94
 
97
95
  if f = @_flash
@@ -35,6 +35,23 @@ class Roda
35
35
  # arguments and providing them as a single rack response array. With a rack response array,
36
36
  # the values are used directly, while with 3 arguments, the headers given are merged into
37
37
  # the existing headers and the given body is written to the existing response body.
38
+ #
39
+ # If using other plugins that recognize additional types of match block responses, such
40
+ # as +symbol_views+ and +json+, you can pass those additional types to +r.halt+:
41
+ #
42
+ # plugin :halt
43
+ # plugin :symbol_views
44
+ # plugin :json
45
+ # route do |r|
46
+ # r.halt(:template)
47
+ # r.halt(500, [{'error'=>'foo'}])
48
+ # r.halt(500, 'header=>'value', :other_template)
49
+ # end
50
+ #
51
+ # Note that when using the +json+ plugin with the +halt+ plugin, you cannot return a
52
+ # array as a single argument and have it be converted to json, since it would be interpreted
53
+ # as a rack response. You must use call +r.halt+ with either two or three argument forms
54
+ # in that case.
38
55
  module Halt
39
56
  module RequestMethods
40
57
  # Expand default halt method to handle status codes, headers, and bodies. See Halt.
@@ -45,22 +62,24 @@ class Roda
45
62
  case v = res[0]
46
63
  when Integer
47
64
  response.status = v
48
- when String
49
- response.write v
50
65
  when Array
51
66
  throw :halt, v
52
67
  else
53
- raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
68
+ if result = block_result_body(v)
69
+ response.write(result)
70
+ else
71
+ raise Roda::RodaError, "singular argument given to #halt not handled: #{v.inspect}"
72
+ end
54
73
  end
55
74
  when 2
56
75
  resp = response
57
76
  resp.status = res[0]
58
- resp.write res[1]
77
+ resp.write(block_result_body(res[1]))
59
78
  when 3
60
79
  resp = response
61
80
  resp.status = res[0]
62
81
  resp.headers.merge!(res[1])
63
- resp.write res[2]
82
+ resp.write(block_result_body(res[2]))
64
83
  else
65
84
  raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
66
85
  end
@@ -8,7 +8,7 @@ class Roda
8
8
  # It adds a +:header+ matcher for matching on arbitrary headers, which matches
9
9
  # if the header is present:
10
10
  #
11
- # r.on :header=>'X-App-Token' do
11
+ # r.on :header=>'X-App-Token' do |header_value|
12
12
  # end
13
13
  #
14
14
  # It adds a +:host+ matcher for matching by the host of the request:
@@ -45,13 +45,8 @@ class Roda
45
45
  # Match if the given uppercase key is present inside the environment.
46
46
  def match_header(key)
47
47
  if v = @env[key.upcase.tr("-","_")]
48
- if roda_class.opts[:match_header_yield]
49
- @captures << v
50
- else
51
- RodaPlugins.deprecate("The :header hash matcher will yield the header value in Roda 2. To turn on the Roda 2 behavior, set opts[:match_header_yield] to true for your Roda class.")
52
- end
48
+ @captures << v
53
49
  end
54
- v
55
50
  end
56
51
 
57
52
  # Match if the host of the request is the same as the hostname. +hostname+
@@ -67,11 +67,9 @@ class Roda
67
67
  end
68
68
 
69
69
  module InstanceMethods
70
- private
71
-
72
70
  # Before routing, execute the before hooks, and
73
71
  # execute the after hooks before returning.
74
- def _route(*, &block)
72
+ def call
75
73
  if b = opts[:before_hook]
76
74
  instance_exec(&b)
77
75
  end
@@ -33,13 +33,15 @@ class Roda
33
33
  #
34
34
  # plugin :json, :classes=>[Array, Hash, Sequel::Model]
35
35
  module Json
36
+ OPTS = {}.freeze
37
+
36
38
  # Set the classes to automatically convert to JSON
37
- def self.configure(app, opts={})
39
+ def self.configure(app, opts=OPTS)
38
40
  classes = opts[:classes] || [Array, Hash]
39
41
  app.opts[:json_result_classes] ||= []
40
42
  app.opts[:json_result_classes] += classes
41
43
  app.opts[:json_result_classes].uniq!
42
- app.opts[:json_result_classes].extend(RodaDeprecateMutation)
44
+ app.opts[:json_result_classes].freeze
43
45
  end
44
46
 
45
47
  module ClassMethods
@@ -74,7 +74,7 @@ class Roda
74
74
  # end
75
75
  # end
76
76
  #
77
- # When sending a mail via +mail+ or +sendmail+, an Error will be raised
77
+ # When sending a mail via +mail+ or +sendmail+, a RodaError will be raised
78
78
  # if the mail object does not have a body. This is similar to the 404
79
79
  # status that Roda uses by default for web requests that don't have
80
80
  # a body. If you want to specifically send an email with an empty body, you
@@ -110,6 +110,7 @@ class Roda
110
110
  MAIL = "MAIL".freeze
111
111
  CONTENT_TYPE = 'Content-Type'.freeze
112
112
  TEXT_PLAIN = "text/plain".freeze
113
+ OPTS = {}.freeze
113
114
 
114
115
  # Error raised when the using the mail class method, but the routing
115
116
  # tree doesn't return the mail object.
@@ -117,7 +118,7 @@ class Roda
117
118
 
118
119
  # Set the options for the mailer. Options:
119
120
  # :content_type :: The default content type for emails (default: text/plain)
120
- def self.configure(app, opts={})
121
+ def self.configure(app, opts=OPTS)
121
122
  app.opts[:mailer] = (app.opts[:mailer]||{}).merge(opts).freeze
122
123
  end
123
124
 
@@ -127,7 +128,7 @@ class Roda
127
128
  # calling +deliver+ to send the mail.
128
129
  def mail(path, *args)
129
130
  mail = ::Mail.new
130
- unless mail.equal?(allocate.call(PATH_INFO=>path, SCRIPT_NAME=>EMPTY_STRING, REQUEST_METHOD=>MAIL, RACK_INPUT=>StringIO.new, RODA_MAIL=>mail, RODA_MAIL_ARGS=>args, &route_block))
131
+ unless mail.equal?(new(PATH_INFO=>path, SCRIPT_NAME=>EMPTY_STRING, REQUEST_METHOD=>MAIL, RACK_INPUT=>StringIO.new, RODA_MAIL=>mail, RODA_MAIL_ARGS=>args).call(&route_block))
131
132
  raise Error, "route did not return mail instance for #{path.inspect}, #{args.inspect}"
132
133
  end
133
134
  mail
@@ -203,19 +204,19 @@ class Roda
203
204
  end
204
205
  end
205
206
 
206
- private
207
-
208
207
  # If this is an email request, set the mail object in the response, as well
209
208
  # as the default content_type for the email.
210
- def _route
209
+ def initialize(env)
210
+ super
211
211
  if mail = env[RODA_MAIL]
212
212
  res = @_response
213
213
  res.mail = mail
214
214
  res.headers.delete(CONTENT_TYPE)
215
215
  end
216
- super
217
216
  end
218
217
 
218
+ private
219
+
219
220
  # Set the text_part or html_part (depending on the method) in the related email,
220
221
  # using the given body and optional headers.
221
222
  def _mail_part(meth, body, headers=nil)
@@ -30,15 +30,14 @@ class Roda
30
30
  #
31
31
  # run App
32
32
  #
33
- # Note that once you use the middleware plugin, you can only use the
34
- # Roda app as middleware, and you will get errors if you attempt to
35
- # use it as a regular app.
33
+ # It is possible to use the Roda app as a regular app even when using
34
+ # the middleware plugin.
36
35
  module Middleware
37
36
  # Forward instances are what is actually used as middleware.
38
37
  class Forwarder
39
38
  # Store the current middleware and the next middleware to call.
40
39
  def initialize(mid, app)
41
- @mid = mid.app
40
+ @mid = mid
42
41
  @app = app
43
42
  end
44
43
 
@@ -49,7 +48,9 @@ class Roda
49
48
  res = nil
50
49
 
51
50
  call_next = catch(:next) do
52
- res = @mid.call(env)
51
+ scope = @mid.new(env)
52
+ scope.request.forward_next = true
53
+ res = scope.call(&@mid.route_block)
53
54
  false
54
55
  end
55
56
 
@@ -62,20 +63,31 @@ class Roda
62
63
  end
63
64
 
64
65
  module ClassMethods
65
- # Create a Forwarder instead of a new instance.
66
+ # Create a Forwarder instead of a new instance if a non-Hash is given.
66
67
  def new(app)
67
- Forwarder.new(self, app)
68
+ if app.is_a?(Hash)
69
+ super
70
+ else
71
+ Forwarder.new(self, app)
72
+ end
68
73
  end
69
74
 
70
75
  # Override the route block so that if no route matches, we throw so
71
76
  # that the next middleware is called.
72
77
  def route(&block)
73
78
  super do |r|
74
- instance_exec(r, &block)
75
- throw :next, true
79
+ res = instance_exec(r, &block)
80
+ throw :next, true if r.forward_next
81
+ res
76
82
  end
77
83
  end
78
84
  end
85
+
86
+ module RequestMethods
87
+ # Whether to forward the request to the next application. Set only if
88
+ # this request is being performed for middleware.
89
+ attr_accessor :forward_next
90
+ end
79
91
  end
80
92
 
81
93
  register_plugin(:middleware, Middleware)
@@ -40,11 +40,9 @@ class Roda
40
40
  end
41
41
 
42
42
  module InstanceMethods
43
- private
44
-
45
43
  # If routing returns a 404 response with an empty body, call
46
44
  # the not_found handler.
47
- def _route
45
+ def call
48
46
  result = super
49
47
 
50
48
  if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
@@ -55,6 +53,8 @@ class Roda
55
53
  end
56
54
  end
57
55
 
56
+ private
57
+
58
58
  # Use an empty not_found_handler by default, so that loading
59
59
  # the plugin without defining a not_found handler doesn't
60
60
  # break things.