roda 3.17.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +48 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +22 -4
  5. data/doc/release_notes/3.18.0.txt +170 -0
  6. data/lib/roda.rb +249 -26
  7. data/lib/roda/plugins/_after_hook.rb +4 -26
  8. data/lib/roda/plugins/_before_hook.rb +30 -2
  9. data/lib/roda/plugins/branch_locals.rb +2 -2
  10. data/lib/roda/plugins/class_level_routing.rb +9 -7
  11. data/lib/roda/plugins/default_headers.rb +15 -1
  12. data/lib/roda/plugins/default_status.rb +9 -10
  13. data/lib/roda/plugins/direct_call.rb +38 -0
  14. data/lib/roda/plugins/error_email.rb +1 -1
  15. data/lib/roda/plugins/error_handler.rb +37 -11
  16. data/lib/roda/plugins/hooks.rb +28 -30
  17. data/lib/roda/plugins/mail_processor.rb +16 -11
  18. data/lib/roda/plugins/mailer.rb +1 -1
  19. data/lib/roda/plugins/middleware.rb +13 -3
  20. data/lib/roda/plugins/multi_route.rb +3 -3
  21. data/lib/roda/plugins/named_templates.rb +4 -4
  22. data/lib/roda/plugins/path.rb +13 -8
  23. data/lib/roda/plugins/render.rb +2 -2
  24. data/lib/roda/plugins/route_block_args.rb +4 -3
  25. data/lib/roda/plugins/route_csrf.rb +9 -4
  26. data/lib/roda/plugins/sessions.rb +2 -1
  27. data/lib/roda/plugins/shared_vars.rb +1 -1
  28. data/lib/roda/plugins/static_routing.rb +7 -17
  29. data/lib/roda/plugins/status_handler.rb +5 -3
  30. data/lib/roda/plugins/view_options.rb +2 -2
  31. data/lib/roda/version.rb +1 -1
  32. data/spec/define_roda_method_spec.rb +257 -0
  33. data/spec/plugin/class_level_routing_spec.rb +0 -27
  34. data/spec/plugin/default_headers_spec.rb +7 -0
  35. data/spec/plugin/default_status_spec.rb +31 -1
  36. data/spec/plugin/direct_call_spec.rb +28 -0
  37. data/spec/plugin/error_handler_spec.rb +27 -0
  38. data/spec/plugin/hooks_spec.rb +21 -0
  39. data/spec/plugin/middleware_spec.rb +108 -36
  40. data/spec/plugin/multi_route_spec.rb +12 -0
  41. data/spec/plugin/route_csrf_spec.rb +27 -0
  42. data/spec/plugin/sessions_spec.rb +26 -1
  43. data/spec/plugin/static_routing_spec.rb +25 -3
  44. data/spec/plugin/status_handler_spec.rb +17 -0
  45. data/spec/route_spec.rb +39 -0
  46. data/spec/spec_helper.rb +2 -2
  47. metadata +9 -3
@@ -140,7 +140,7 @@ class Roda
140
140
  def mail(path, *args)
141
141
  mail = ::Mail.new
142
142
  catch(:no_mail) do
143
- unless mail.equal?(new("PATH_INFO"=>path, 'SCRIPT_NAME'=>'', "REQUEST_METHOD"=>"MAIL", 'rack.input'=>StringIO.new, 'roda.mail'=>mail, 'roda.mail_args'=>args).call(&@rack_app_route_block))
143
+ unless mail.equal?(new("PATH_INFO"=>path, 'SCRIPT_NAME'=>'', "REQUEST_METHOD"=>"MAIL", 'rack.input'=>StringIO.new, 'roda.mail'=>mail, 'roda.mail_args'=>args)._roda_handle_main_route)
144
144
  raise Error, "route did not return mail instance for #{path.inspect}, #{args.inspect}"
145
145
  end
146
146
  mail
@@ -127,16 +127,26 @@ class Roda
127
127
  Forwarder.new(self, app, *args, &block)
128
128
  end
129
129
  end
130
+ end
130
131
 
132
+ module InstanceMethods
131
133
  # Override the route block so that if no route matches, we throw so
132
- # that the next middleware is called.
133
- def route(*args, &block)
134
+ # that the next middleware is called. Old Dispatch API.
135
+ def call(&block)
134
136
  super do |r|
135
- res = instance_exec(r, &block)
137
+ res = instance_exec(r, &block) # call Fallback
136
138
  throw :next, true if r.forward_next
137
139
  res
138
140
  end
139
141
  end
142
+
143
+ # Override the route block so that if no route matches, we throw so
144
+ # that the next middleware is called.
145
+ def _roda_run_main_route(r)
146
+ res = super
147
+ throw :next, true if r.forward_next
148
+ res
149
+ end
140
150
  end
141
151
 
142
152
  module RequestMethods
@@ -169,8 +169,8 @@ class Roda
169
169
  # call super.
170
170
  def route(name=nil, namespace=nil, &block)
171
171
  if name
172
- opts[:namespaced_routes][namespace] ||= {}
173
- opts[:namespaced_routes][namespace][name] = convert_route_block(block)
172
+ routes = opts[:namespaced_routes][namespace] ||= {}
173
+ routes[name] = define_roda_method(routes[name] || "multi_route_#{namespace}_#{name}", 1, &convert_route_block(block))
174
174
  self::RodaRequest.clear_named_route_regexp!(namespace)
175
175
  else
176
176
  super(&block)
@@ -213,7 +213,7 @@ class Roda
213
213
 
214
214
  # Dispatch to the named route with the given name.
215
215
  def route(name, namespace=nil)
216
- scope.instance_exec(self, &roda_class.named_route(name, namespace))
216
+ scope.send(roda_class.named_route(name, namespace), self)
217
217
  end
218
218
  end
219
219
  end
@@ -69,7 +69,7 @@ class Roda
69
69
 
70
70
  # Store a new template block and options for the given template name.
71
71
  def template(name, options=nil, &block)
72
- opts[:named_templates][name.to_s] = [options, block].freeze
72
+ opts[:named_templates][name.to_s] = [options, define_roda_method("named_templates_#{name}", 0, &block)].freeze
73
73
  nil
74
74
  end
75
75
  end
@@ -80,14 +80,14 @@ class Roda
80
80
  # If a template name is given and it matches a named template, call
81
81
  # the named template block to get the inline template to use.
82
82
  def find_template(options)
83
- if options[:template] && (template_opts, block = opts[:named_templates][template_name(options)]; block)
83
+ if options[:template] && (template_opts, meth = opts[:named_templates][template_name(options)]; meth)
84
84
  if template_opts
85
- options = Hash[template_opts].merge!(options)
85
+ options = template_opts.merge(options)
86
86
  else
87
87
  options = Hash[options]
88
88
  end
89
89
 
90
- options[:inline] = instance_exec(&block)
90
+ options[:inline] = send(meth)
91
91
 
92
92
  super(options)
93
93
  else
@@ -10,7 +10,7 @@ class Roda
10
10
  #
11
11
  # Additionally, you can call the +path+ class method with a class and a block, and it will register
12
12
  # the class. You can then call the +path+ instance method with an instance of that class, and it will
13
- # instance_exec the block with the arguments provided to path.
13
+ # execute the block in the context of the route block scope with the arguments provided to path.
14
14
  #
15
15
  # Example:
16
16
  #
@@ -60,9 +60,8 @@ class Roda
60
60
  # method. If a Symbol or String, uses the value as the url method name.
61
61
  # :url_only :: Do not create a path method, just a url method.
62
62
  #
63
- # Note that if :add_script_name, :url, or :url_only is used, will also create a <tt>_*_path</tt>
64
- # method. This is necessary in order to support path methods that accept blocks, as you can't pass
65
- # a block to a block that is instance_execed.
63
+ # Note that if :add_script_name, :url, or :url_only is used, the path method will also create a
64
+ # <tt>_*_path</tt> private method.
66
65
  module Path
67
66
  DEFAULT_PORTS = {'http' => 80, 'https' => 443}.freeze
68
67
 
@@ -73,6 +72,7 @@ class Roda
73
72
  app.instance_eval do
74
73
  self.opts[:path_class_by_name] = opts.fetch(:by_name, ENV['RACK_ENV'] == 'development')
75
74
  self.opts[:path_classes] ||= {}
75
+ self.opts[:path_class_methods] ||= {}
76
76
  unless path_block(String)
77
77
  path(String){|str| str}
78
78
  end
@@ -88,6 +88,7 @@ class Roda
88
88
  # Freeze the path classes when freezing the app.
89
89
  def freeze
90
90
  path_classes.freeze
91
+ opts[:path_classes_methods].freeze
91
92
  super
92
93
  end
93
94
 
@@ -100,6 +101,7 @@ class Roda
100
101
  name = name.name
101
102
  end
102
103
  path_classes[name] = block
104
+ self.opts[:path_class_methods][name] = define_roda_method("path_#{name}", :any, &block)
103
105
  return
104
106
  end
105
107
 
@@ -164,6 +166,7 @@ class Roda
164
166
 
165
167
  # Return the block related to the given class, or nil if there is no block.
166
168
  def path_block(klass)
169
+ # RODA4: Remove
167
170
  if opts[:path_class_by_name]
168
171
  klass = klass.name
169
172
  end
@@ -175,14 +178,16 @@ class Roda
175
178
  # Return a path based on the class of the object. The object passed must have
176
179
  # had its class previously registered with the application. If the app's
177
180
  # :add_script_name option is true, this prepends the SCRIPT_NAME to the path.
178
- def path(obj, *args)
181
+ def path(obj, *args, &block)
179
182
  app = self.class
180
- unless blk = app.path_block(obj.class)
183
+ opts = app.opts
184
+ klass = opts[:path_class_by_name] ? obj.class.name : obj.class
185
+ unless meth = opts[:path_class_methods][klass]
181
186
  raise RodaError, "unrecognized object given to Roda#path: #{obj.inspect}"
182
187
  end
183
188
 
184
- path = instance_exec(obj, *args, &blk)
185
- path = request.script_name.to_s + path if app.opts[:add_script_name]
189
+ path = send(meth, obj, *args, &block)
190
+ path = request.script_name.to_s + path if opts[:add_script_name]
186
191
  path
187
192
  end
188
193
  end
@@ -342,10 +342,10 @@ class Roda
342
342
  opts = found_template_opts || find_template(opts)
343
343
  template_opts = render_opts[:template_opts]
344
344
  if engine_opts = render_opts[:engine_opts][opts[:engine]]
345
- template_opts = Hash[template_opts].merge!(engine_opts)
345
+ template_opts = template_opts.merge(engine_opts)
346
346
  end
347
347
  if current_template_opts = opts[:template_opts]
348
- template_opts = Hash[template_opts].merge!(current_template_opts)
348
+ template_opts = template_opts.merge(current_template_opts)
349
349
  end
350
350
  opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block])
351
351
  end
@@ -25,7 +25,7 @@ class Roda
25
25
  module RouteBlockArgs
26
26
  def self.configure(app, &block)
27
27
  app.instance_exec do
28
- opts[:route_block_args] = block
28
+ define_roda_method(:_roda_route_block_args, 0, &block)
29
29
  route(&@raw_route_block) if @raw_route_block
30
30
  end
31
31
  end
@@ -37,8 +37,9 @@ class Roda
37
37
  private
38
38
 
39
39
  def convert_route_block(block)
40
- proc do |r|
41
- instance_exec(*instance_exec(&opts[:route_block_args]), &block)
40
+ meth = define_roda_method("convert_route_block_args", :any, &super(block))
41
+ lambda do |_|
42
+ send(meth, *_roda_route_block_args)
42
43
  end
43
44
  end
44
45
  end
@@ -164,11 +164,13 @@ class Roda
164
164
 
165
165
  def self.configure(app, opts=OPTS, &block)
166
166
  options = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts)
167
- if block
168
- if opts[:csrf_failure]
167
+ if block || opts[:csrf_failure].is_a?(Proc)
168
+ if block && opts[:csrf_failure]
169
169
  raise RodaError, "Cannot specify both route_csrf plugin block and :csrf_failure option"
170
170
  end
171
- options[:csrf_failure] = block
171
+ block ||= opts[:csrf_failure]
172
+ options[:csrf_failure] = :csrf_failure_method
173
+ app.define_roda_method(:_roda_route_csrf_failure, 1, &app.send(:convert_route_block, block))
172
174
  end
173
175
  options[:env_header] = "HTTP_#{options[:header].to_s.gsub('-', '_').upcase}".freeze
174
176
  options.freeze
@@ -192,8 +194,11 @@ class Roda
192
194
  throw :halt, [403, {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, []]
193
195
  when :clear_session
194
196
  session.clear
197
+ when :csrf_failure_method
198
+ @_request.on{_roda_route_csrf_failure(@_request)}
195
199
  when Proc
196
- @_request.on{instance_exec(@_request, &failure_action)}
200
+ RodaPlugins.warn "Passing a Proc as the :csrf_failure option value to check_csrf! is deprecated"
201
+ @_request.on{instance_exec(@_request, &failure_action)} # Deprecated
197
202
  else
198
203
  raise RodaError, "Unsupported :csrf_failure option: #{failure_action.inspect}"
199
204
  end
@@ -162,6 +162,7 @@ class Roda
162
162
  plugin_opts = opts
163
163
  opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts)
164
164
  co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze
165
+ opts[:remove_cookie_options] = co.merge(:max_age=>'0', :expires=>Time.at(0))
165
166
  opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse)
166
167
  opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc
167
168
 
@@ -231,7 +232,7 @@ class Roda
231
232
  if session.empty?
232
233
  if env[SESSION_SERIALIZED]
233
234
  # If session was submitted and is now empty, remove the cookie
234
- Rack::Utils.delete_cookie_header!(headers, opts[:key])
235
+ Rack::Utils.delete_cookie_header!(headers, opts[:key], opts[:remove_cookie_options])
235
236
  # else
236
237
  # If no session was submitted, and the session is empty
237
238
  # then there is no need to do anything
@@ -64,7 +64,7 @@ class Roda
64
64
  if block_given?
65
65
  if vars
66
66
  begin
67
- env['roda.shared'] = Hash[h].merge!(vars)
67
+ env['roda.shared'] = h.merge(vars)
68
68
  yield
69
69
  ensure
70
70
  env['roda.shared'] = h
@@ -50,7 +50,7 @@ class Roda
50
50
  # while still handling the request methods differently.
51
51
  module StaticRouting
52
52
  def self.configure(app)
53
- app.opts[:static_routes] = {}
53
+ app.opts[:static_routes] ||= {}
54
54
  end
55
55
 
56
56
  module ClassMethods
@@ -96,7 +96,8 @@ class Roda
96
96
 
97
97
  # Add a static route for the given method.
98
98
  def add_static_route(method, path, &block)
99
- (opts[:static_routes][path] ||= {})[method] = convert_route_block(block)
99
+ routes = opts[:static_routes][path] ||= {}
100
+ routes[method] = define_roda_method(routes[method] || "static_route_#{method}_#{path}", 1, &convert_route_block(block))
100
101
  end
101
102
  end
102
103
 
@@ -107,21 +108,10 @@ class Roda
107
108
  # instead having the routing tree handle the request.
108
109
  def _roda_before_30__static_routing
109
110
  r = @_request
110
- if route = self.class.static_route_for(r.request_method, r.path_info)
111
- r.static_route(&route)
112
- end
113
- end
114
- end
115
-
116
- module RequestMethods
117
- # Assume that this request matches a static route, setting
118
- # the remaining path to the emptry string and passing
119
- # control to the given block.
120
- def static_route(&block)
121
- @remaining_path = ''
122
-
123
- always do
124
- scope.instance_exec(self, &block)
111
+ if meth = self.class.static_route_for(r.request_method, r.path_info)
112
+ r.instance_variable_set(:@remaining_path, '')
113
+ r.send(:block_result, send(meth, r))
114
+ throw :halt, @_response.finish
125
115
  end
126
116
  end
127
117
  end
@@ -30,7 +30,9 @@ class Roda
30
30
  module ClassMethods
31
31
  # Install the given block as a status handler for the given HTTP response code.
32
32
  def status_handler(code, &block)
33
- opts[:status_handler][code] = block
33
+ # For backwards compatibility, pass request argument if block accepts argument
34
+ arity = block.arity == 0 ? 0 : 1
35
+ opts[:status_handler][code] = [define_roda_method(:"_roda_status_handler_#{code}", arity, &block), arity]
34
36
  end
35
37
 
36
38
  # Freeze the hash of status handlers so that there can be no thread safety issues at runtime.
@@ -45,11 +47,11 @@ class Roda
45
47
 
46
48
  # If routing returns a response we have a handler for, call that handler.
47
49
  def _roda_after_20__status_handler(result)
48
- if result && (block = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
50
+ if result && (meth, arity = opts[:status_handler][result[0]]; meth) && (v = result[2]).is_a?(Array) && v.empty?
49
51
  res = @_response
50
52
  res.headers.clear
51
53
  res.status = result[0]
52
- result.replace(_call(&block))
54
+ result.replace(_roda_handle_route{arity == 1 ? send(meth, @_request) : send(meth)})
53
55
  end
54
56
  end
55
57
  end
@@ -107,7 +107,7 @@ class Roda
107
107
  # Set branch/route options to use when rendering the layout
108
108
  def set_layout_options(opts)
109
109
  if options = @_layout_options
110
- @_layout_options = Hash[options].merge!(opts)
110
+ @_layout_options = options.merge!(opts)
111
111
  else
112
112
  @_layout_options = opts
113
113
  end
@@ -116,7 +116,7 @@ class Roda
116
116
  # Set branch/route options to use when rendering the view
117
117
  def set_view_options(opts)
118
118
  if options = @_view_options
119
- @_view_options = Hash[options].merge!(opts)
119
+ @_view_options = options.merge!(opts)
120
120
  else
121
121
  @_view_options = opts
122
122
  end
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 17
7
+ RodaMinorVersion = 18
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -0,0 +1,257 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe "Roda.define_roda_method" do
4
+ before do
5
+ @scope = app.new({})
6
+ end
7
+
8
+ it "should define methods using block" do
9
+ m0 = app.define_roda_method("x", 0){1}
10
+ m0.must_be_kind_of Symbol
11
+ m0.must_match /\A_roda_x_\d+\z/
12
+ @scope.send(m0).must_equal 1
13
+
14
+ m1 = app.define_roda_method("x", 1){|x| [x, 2]}
15
+ m1.must_be_kind_of Symbol
16
+ m1.must_match /\A_roda_x_\d+\z/
17
+ @scope.send(m1, 3).must_equal [3, 2]
18
+ end
19
+
20
+ it "should define private methods" do
21
+ proc{@scope.public_send(app.define_roda_method("x", 0){1})}.must_raise NoMethodError
22
+ end
23
+
24
+ it "should accept symbols as method name and return the same symbol" do
25
+ m0 = app.define_roda_method(:_roda_foo, 0){1}
26
+ m0.must_equal :_roda_foo
27
+ @scope.send(m0).must_equal 1
28
+ end
29
+
30
+ it "should handle optional arguments and splats for expected_arity 0" do
31
+ m2 = app.define_roda_method("x", 0){|*x| [x, 3]}
32
+ @scope.send(m2).must_equal [[], 3]
33
+
34
+ m3 = app.define_roda_method("x", 0){|x=5| [x, 4]}
35
+ @scope.send(m3).must_equal [5, 4]
36
+
37
+ m4 = app.define_roda_method("x", 0){|x=6, *y| [x, y, 5]}
38
+ @scope.send(m4).must_equal [6, [], 5]
39
+ end
40
+
41
+ it "should should optional arguments and splats for expected_arity 1" do
42
+ m2 = app.define_roda_method("x", 1){|y, *x| [y, x, 3]}
43
+ @scope.send(m2, :a).must_equal [:a, [], 3]
44
+
45
+ m3 = app.define_roda_method("x", 1){|y, x=5| [y, x, 4]}
46
+ @scope.send(m3, :b).must_equal [:b, 5, 4]
47
+
48
+ m4 = app.define_roda_method("x", 1){|y, x=6, *z| [y, x, z, 5]}
49
+ @scope.send(m4, :c).must_equal [:c, 6, [], 5]
50
+ end
51
+
52
+ it "should handle differences in arity" do
53
+ m0 = app.define_roda_method("x", 0){|x| [x, 1]}
54
+ @scope.send(m0).must_equal [nil, 1]
55
+
56
+ m1 = app.define_roda_method("x", 1){2}
57
+ @scope.send(m1, 3).must_equal 2
58
+ end
59
+
60
+ it "should raise for unexpected expected_arity" do
61
+ proc{app.define_roda_method("x", 2){|x|}}.must_raise Roda::RodaError
62
+ end
63
+
64
+ it "should fail if :check_arity false app option is used and a block with invalid arity is passed" do
65
+ app.opts[:check_arity] = false
66
+ m0 = app.define_roda_method("x", 0){|x| [x, 1]}
67
+ proc{@scope.send(m0)}.must_raise ArgumentError
68
+
69
+ m1 = app.define_roda_method("x", 1){2}
70
+ proc{@scope.send(m1, 1)}.must_raise ArgumentError
71
+ end
72
+
73
+ deprecated "should warn if :check_arity :warn app option is used and a block with invalid arity is passed" do
74
+ app.opts[:check_arity] = :warn
75
+ m0 = app.define_roda_method("x", 0){|x| [x, 1]}
76
+ @scope.send(m0).must_equal [nil, 1]
77
+
78
+ m1 = app.define_roda_method("x", 1){2}
79
+ @scope.send(m1, 3).must_equal 2
80
+ end
81
+
82
+ [false, true].each do |warn_dynamic_arity|
83
+ meth = warn_dynamic_arity ? :deprecated : :it
84
+ send meth, "should handle expected_arity :any and do dynamic arity check/fix" do
85
+ if warn_dynamic_arity
86
+ app.opts[:check_dynamic_arity] = :warn
87
+ end
88
+
89
+ m0 = app.define_roda_method("x", :any){1}
90
+ @scope.send(m0).must_equal 1
91
+ @scope.send(m0, 2).must_equal 1
92
+
93
+ m1 = app.define_roda_method("x", :any){|x| [x, 1]}
94
+ @scope.send(m1).must_equal [nil, 1]
95
+ @scope.send(m1, 2).must_equal [2, 1]
96
+ @scope.send(m1, 2, 3).must_equal [2, 1]
97
+
98
+ m2 = app.define_roda_method("x", :any){|x=5| [x, 2]}
99
+ @scope.send(m2).must_equal [5, 2]
100
+ @scope.send(m2, 2).must_equal [2, 2]
101
+ @scope.send(m2, 2, 3).must_equal [2, 2]
102
+
103
+ m3 = app.define_roda_method("x", :any){|y, x=5| [x, y, 3]}
104
+ @scope.send(m3).must_equal [5, nil, 3]
105
+ @scope.send(m3, 2).must_equal [5, 2, 3]
106
+ @scope.send(m3, 2, 3).must_equal [3, 2, 3]
107
+ @scope.send(m3, 2, 3, 4).must_equal [3, 2, 3]
108
+
109
+ m4 = app.define_roda_method("x", :any){|*z| [z, 1]}
110
+ @scope.send(m4).must_equal [[], 1]
111
+ @scope.send(m4, 2).must_equal [[2], 1]
112
+
113
+ m5 = app.define_roda_method("x", :any){|x, *z| [x, z, 1]}
114
+ @scope.send(m5).must_equal [nil, [], 1]
115
+ @scope.send(m5, 2).must_equal [2, [], 1]
116
+ @scope.send(m5, 2, 3).must_equal [2, [3], 1]
117
+
118
+ m6 = app.define_roda_method("x", :any){|x=5, *z| [x, z, 2]}
119
+ @scope.send(m6).must_equal [5, [], 2]
120
+ @scope.send(m6, 2).must_equal [2, [], 2]
121
+ @scope.send(m6, 2, 3).must_equal [2, [3], 2]
122
+
123
+ m7 = app.define_roda_method("x", :any){|y, x=5, *z| [x, y, z, 3]}
124
+ @scope.send(m7).must_equal [5, nil, [], 3]
125
+ @scope.send(m7, 2).must_equal [5, 2, [], 3]
126
+ @scope.send(m7, 2, 3).must_equal [3, 2, [], 3]
127
+ @scope.send(m7, 2, 3, 4).must_equal [3, 2, [4], 3]
128
+ end
129
+ end
130
+
131
+ it "should not fix dynamic arity issues if :check_dynamic_arity false app option is used" do
132
+ app.opts[:check_dynamic_arity] = false
133
+
134
+ m0 = app.define_roda_method("x", :any){1}
135
+ @scope.send(m0).must_equal 1
136
+ proc{@scope.send(m0, 2)}.must_raise ArgumentError
137
+
138
+ m1 = app.define_roda_method("x", :any){|x| [x, 1]}
139
+ proc{@scope.send(m1)}.must_raise ArgumentError
140
+ @scope.send(m1, 2).must_equal [2, 1]
141
+ proc{@scope.send(m1, 2, 3)}.must_raise ArgumentError
142
+
143
+ m2 = app.define_roda_method("x", :any){|x=5| [x, 2]}
144
+ @scope.send(m2).must_equal [5, 2]
145
+ @scope.send(m2, 2).must_equal [2, 2]
146
+ proc{@scope.send(m2, 2, 3)}.must_raise ArgumentError
147
+
148
+ m3 = app.define_roda_method("x", :any){|y, x=5| [x, y, 3]}
149
+ proc{@scope.send(m3)}.must_raise ArgumentError
150
+ @scope.send(m3, 2).must_equal [5, 2, 3]
151
+ @scope.send(m3, 2, 3).must_equal [3, 2, 3]
152
+ proc{@scope.send(m3, 2, 3, 4)}.must_raise ArgumentError
153
+
154
+ m4 = app.define_roda_method("x", :any){|*z| [z, 1]}
155
+ @scope.send(m4).must_equal [[], 1]
156
+ @scope.send(m4, 2).must_equal [[2], 1]
157
+
158
+ m5 = app.define_roda_method("x", :any){|x, *z| [x, z, 1]}
159
+ proc{@scope.send(m5)}.must_raise ArgumentError
160
+ @scope.send(m5, 2).must_equal [2, [], 1]
161
+ @scope.send(m5, 2, 3).must_equal [2, [3], 1]
162
+
163
+ m6 = app.define_roda_method("x", :any){|x=5, *z| [x, z, 2]}
164
+ @scope.send(m6).must_equal [5, [], 2]
165
+ @scope.send(m6, 2).must_equal [2, [], 2]
166
+ @scope.send(m6, 2, 3).must_equal [2, [3], 2]
167
+
168
+ m7 = app.define_roda_method("x", :any){|y, x=5, *z| [x, y, z, 3]}
169
+ proc{@scope.send(m7)}.must_raise ArgumentError
170
+ @scope.send(m7, 2).must_equal [5, 2, [], 3]
171
+ @scope.send(m7, 2, 3).must_equal [3, 2, [], 3]
172
+ @scope.send(m7, 2, 3, 4).must_equal [3, 2, [4], 3]
173
+ end
174
+
175
+ if RUBY_VERSION > '2.1'
176
+ it "should raise for required keyword arguments for expected_arity 0 or 1" do
177
+ proc{eval("app.define_roda_method('x', 0){|b:| [b, 1]}", binding)}.must_raise Roda::RodaError
178
+ proc{eval("app.define_roda_method('x', 0){|c=1, b:| [c, b, 1]}", binding)}.must_raise Roda::RodaError
179
+ proc{eval("app.define_roda_method('x', 1){|x, b:| [b, 1]}", binding)}.must_raise Roda::RodaError
180
+ proc{eval("app.define_roda_method('x', 1){|x, c=1, b:| [c, b, 1]}", binding)}.must_raise Roda::RodaError
181
+ end
182
+
183
+ it "should ignore keyword arguments for expected_arity 0" do
184
+ @scope.send(eval("app.define_roda_method('x', 0){|b:2| [b, 1]}", binding)).must_equal [2, 1]
185
+ @scope.send(eval("app.define_roda_method('x', 0){|**b| [b, 1]}", binding)).must_equal [{}, 1]
186
+ @scope.send(eval("app.define_roda_method('x', 0){|c=1, b:2| [c, b, 1]}", binding)).must_equal [1, 2, 1]
187
+ @scope.send(eval("app.define_roda_method('x', 0){|c=1, **b| [c, b, 1]}", binding)).must_equal [1, {}, 1]
188
+ @scope.send(eval("app.define_roda_method('x', 0){|x, b:2| [x, b, 1]}", binding)).must_equal [nil, 2, 1]
189
+ @scope.send(eval("app.define_roda_method('x', 0){|x, **b| [x, b, 1]}", binding)).must_equal [nil, {}, 1]
190
+ @scope.send(eval("app.define_roda_method('x', 0){|x, c=1, b:2| [x, c, b, 1]}", binding)).must_equal [nil, 1, 2, 1]
191
+ @scope.send(eval("app.define_roda_method('x', 0){|x, c=1, **b| [x, c, b, 1]}", binding)).must_equal [nil, 1, {}, 1]
192
+ end
193
+
194
+ it "should ignore keyword arguments for expected_arity 1" do
195
+ @scope.send(eval("app.define_roda_method('x', 1){|b:2| [b, 1]}", binding), 3).must_equal [2, 1]
196
+ @scope.send(eval("app.define_roda_method('x', 1){|**b| [b, 1]}", binding), 3).must_equal [{}, 1]
197
+ @scope.send(eval("app.define_roda_method('x', 1){|c=1, b:2| [c, b, 1]}", binding), 3).must_equal [3, 2, 1]
198
+ @scope.send(eval("app.define_roda_method('x', 1){|c=1, **b| [c, b, 1]}", binding), 3).must_equal [3, {}, 1]
199
+ @scope.send(eval("app.define_roda_method('x', 1){|x, b:2| [x, b, 1]}", binding), 3).must_equal [3, 2, 1]
200
+ @scope.send(eval("app.define_roda_method('x', 1){|x, **b| [x, b, 1]}", binding), 3).must_equal [3, {}, 1]
201
+ @scope.send(eval("app.define_roda_method('x', 1){|x, c=1, b:2| [x, c, b, 1]}", binding), 3).must_equal [3, 1, 2, 1]
202
+ @scope.send(eval("app.define_roda_method('x', 1){|x, c=1, **b| [x, c, b, 1]}", binding), 3).must_equal [3, 1, {}, 1]
203
+ end
204
+
205
+ it "should handle expected_arity :any with keyword arguments" do
206
+ m = eval('app.define_roda_method("x", :any){|b:2| b}', binding)
207
+ @scope.send(m).must_equal 2
208
+ @scope.send(m, 4).must_equal 2
209
+ @scope.send(m, b: 3).must_equal 3
210
+ @scope.send(m, 4, b: 3).must_equal 3
211
+
212
+ m = eval('app.define_roda_method("x", :any){|b:| b}', binding)
213
+ proc{@scope.send(m)}.must_raise ArgumentError
214
+ proc{@scope.send(m, 4)}.must_raise ArgumentError
215
+ @scope.send(m, b: 3).must_equal 3
216
+ @scope.send(m, 4, b: 3).must_equal 3
217
+
218
+ m = eval('app.define_roda_method("x", :any){|**b| b}', binding)
219
+ @scope.send(m).must_equal({})
220
+ @scope.send(m, 4).must_equal({})
221
+ @scope.send(m, b: 3).must_equal(b: 3)
222
+ @scope.send(m, 4, b: 3).must_equal(b: 3)
223
+
224
+ m = eval('app.define_roda_method("x", :any){|x, b:9| [x, b, 1]}', binding)
225
+ @scope.send(m).must_equal [nil, 9, 1]
226
+ @scope.send(m, 2).must_equal [2, 9, 1]
227
+ @scope.send(m, 2, 3).must_equal [2, 9, 1]
228
+ @scope.send(m, b: 4).must_equal [{b: 4}, 9, 1]
229
+ @scope.send(m, 2, b: 4).must_equal [2, 4, 1]
230
+ @scope.send(m, 2, 3, b: 4).must_equal [2, 4, 1]
231
+
232
+ m = eval('app.define_roda_method("x", :any){|x, b:| [x, b, 1]}', binding)
233
+ proc{@scope.send(m)}.must_raise ArgumentError
234
+ proc{@scope.send(m, 2)}.must_raise ArgumentError
235
+ proc{@scope.send(m, 2, 3)}.must_raise ArgumentError
236
+ proc{@scope.send(m, b: 4)}.must_raise ArgumentError
237
+ @scope.send(m, 2, b: 4).must_equal [2, 4, 1]
238
+ @scope.send(m, 2, 3, b: 4).must_equal [2, 4, 1]
239
+
240
+ m = eval('app.define_roda_method("x", :any){|x, **b| [x, b, 1]}', binding)
241
+ @scope.send(m).must_equal [nil, {}, 1]
242
+ @scope.send(m, 2).must_equal [2, {}, 1]
243
+ @scope.send(m, 2, 3).must_equal [2, {}, 1]
244
+ @scope.send(m, b: 4).must_equal [{b: 4}, {}, 1]
245
+ @scope.send(m, 2, b: 4).must_equal [2, {b: 4}, 1]
246
+ @scope.send(m, 2, 3, b: 4).must_equal [2, {b: 4}, 1]
247
+
248
+ m = eval('m = app.define_roda_method("x", :any){|x=5, b:9| [x, b, 2]}', binding)
249
+ @scope.send(m).must_equal [5, 9, 2]
250
+ @scope.send(m, 2).must_equal [2, 9, 2]
251
+ @scope.send(m, 2, 3).must_equal [2, 9, 2]
252
+ @scope.send(m, b: 4).must_equal [5, 4, 2]
253
+ @scope.send(m, 2, b: 4).must_equal [2, 4, 2]
254
+ @scope.send(m, 2, 3, b: 4).must_equal [2, 4, 2]
255
+ end
256
+ end
257
+ end