roda 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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.