rack-mount 0.6.1 → 0.6.2

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 (30) hide show
  1. data/lib/rack/mount.rb +3 -13
  2. data/lib/rack/mount/analysis/frequency.rb +0 -2
  3. data/lib/rack/mount/analysis/splitting.rb +2 -2
  4. data/lib/rack/mount/code_generation.rb +113 -0
  5. data/lib/rack/mount/generatable_regexp.rb +1 -1
  6. data/lib/rack/mount/route.rb +69 -5
  7. data/lib/rack/mount/route_set.rb +289 -46
  8. data/lib/rack/mount/strexp.rb +53 -50
  9. data/lib/rack/mount/utils.rb +7 -7
  10. data/lib/rack/mount/vendor/reginald/reginald.rb +2 -1
  11. data/lib/rack/mount/vendor/reginald/reginald/alternation.rb +11 -1
  12. data/lib/rack/mount/vendor/reginald/reginald/atom.rb +17 -5
  13. data/lib/rack/mount/vendor/reginald/reginald/character.rb +11 -2
  14. data/lib/rack/mount/vendor/reginald/reginald/character_class.rb +8 -20
  15. data/lib/rack/mount/vendor/reginald/reginald/collection.rb +10 -6
  16. data/lib/rack/mount/vendor/reginald/reginald/expression.rb +30 -20
  17. data/lib/rack/mount/vendor/reginald/reginald/group.rb +22 -8
  18. data/lib/rack/mount/vendor/reginald/reginald/options.rb +47 -0
  19. data/lib/rack/mount/vendor/reginald/reginald/parser.rb +137 -273
  20. data/lib/rack/mount/vendor/reginald/reginald/tokenizer.rb +1 -1
  21. data/lib/rack/mount/vendor/reginald/reginald/version.rb +3 -0
  22. data/lib/rack/mount/version.rb +1 -1
  23. metadata +6 -10
  24. data/lib/rack/mount/exceptions.rb +0 -3
  25. data/lib/rack/mount/generation/route.rb +0 -59
  26. data/lib/rack/mount/generation/route_set.rb +0 -200
  27. data/lib/rack/mount/mixover.rb +0 -60
  28. data/lib/rack/mount/recognition/code_generation.rb +0 -124
  29. data/lib/rack/mount/recognition/route.rb +0 -62
  30. data/lib/rack/mount/recognition/route_set.rb +0 -121
data/lib/rack/mount.rb CHANGED
@@ -11,32 +11,22 @@ module Rack #:nodoc:
11
11
  # other request attributes, session information, or even data dynamically
12
12
  # pulled from a database.
13
13
  module Mount
14
+ autoload :CodeGeneration, 'rack/mount/code_generation'
14
15
  autoload :GeneratableRegexp, 'rack/mount/generatable_regexp'
15
- autoload :Mixover, 'rack/mount/mixover'
16
16
  autoload :Multimap, 'rack/mount/multimap'
17
17
  autoload :Prefix, 'rack/mount/prefix'
18
18
  autoload :RegexpWithNamedGroups, 'rack/mount/regexp_with_named_groups'
19
19
  autoload :Route, 'rack/mount/route'
20
20
  autoload :RouteSet, 'rack/mount/route_set'
21
- autoload :RoutingError, 'rack/mount/exceptions'
21
+ autoload :RoutingError, 'rack/mount/route_set'
22
22
  autoload :Strexp, 'rack/mount/strexp'
23
23
  autoload :Utils, 'rack/mount/utils'
24
+ autoload :Version, 'rack/mount/version'
24
25
 
25
26
  module Analysis #:nodoc:
26
27
  autoload :Frequency, 'rack/mount/analysis/frequency'
27
28
  autoload :Histogram, 'rack/mount/analysis/histogram'
28
29
  autoload :Splitting, 'rack/mount/analysis/splitting'
29
30
  end
30
-
31
- module Generation #:nodoc:
32
- autoload :Route, 'rack/mount/generation/route'
33
- autoload :RouteSet, 'rack/mount/generation/route_set'
34
- end
35
-
36
- module Recognition #:nodoc:
37
- autoload :CodeGeneration, 'rack/mount/recognition/code_generation'
38
- autoload :Route, 'rack/mount/recognition/route'
39
- autoload :RouteSet, 'rack/mount/recognition/route_set'
40
- end
41
31
  end
42
32
  end
@@ -3,8 +3,6 @@ require 'rack/mount/utils'
3
3
  module Rack::Mount
4
4
  module Analysis
5
5
  class Frequency #:nodoc:
6
- extend Mixover
7
-
8
6
  def initialize(*keys)
9
7
  clear
10
8
  keys.each { |key| self << key }
@@ -2,7 +2,7 @@ require 'rack/mount/utils'
2
2
 
3
3
  module Rack::Mount
4
4
  module Analysis
5
- module Splitting
5
+ class Splitting < Frequency
6
6
  NULL = "\0".freeze
7
7
 
8
8
  class Key < Struct.new(:method, :index, :separators)
@@ -67,7 +67,7 @@ module Rack::Mount
67
67
  end
68
68
  end
69
69
 
70
- if inside = part[0][0]
70
+ if inside = part.expression[0]
71
71
  if inside.is_a?(Reginald::Character) && inside.literal?
72
72
  boundaries << inside.to_s
73
73
  end
@@ -0,0 +1,113 @@
1
+ module Rack::Mount
2
+ module CodeGeneration #:nodoc:
3
+ def _expired_recognize(env) #:nodoc:
4
+ raise 'route set not finalized'
5
+ end
6
+
7
+ def rehash
8
+ super
9
+ optimize_recognize!
10
+ end
11
+
12
+ private
13
+ def expire!
14
+ if @optimized_recognize_defined
15
+ remove_metaclass_method :recognize
16
+
17
+ class << self
18
+ alias_method :recognize, :_expired_recognize
19
+ end
20
+
21
+ @optimized_recognize_defined = false
22
+ end
23
+
24
+ super
25
+ end
26
+
27
+ def optimize_container_iterator(container)
28
+ body = []
29
+
30
+ container.each_with_index { |route, i|
31
+ body << "route = self[#{i}]"
32
+ body << 'matches = {}'
33
+ body << 'params = route.defaults.dup'
34
+
35
+ conditions = []
36
+ route.conditions.each do |method, condition|
37
+ b = []
38
+ if condition.is_a?(Regexp)
39
+ b << "if m = obj.#{method}.match(#{condition.inspect})"
40
+ b << "matches[:#{method}] = m"
41
+ if (named_captures = route.named_captures[method]) && named_captures.any?
42
+ b << 'captures = m.captures'
43
+ b << 'p = nil'
44
+ b << named_captures.map { |k, j| "params[#{k.inspect}] = p if p = captures[#{j}]" }.join('; ')
45
+ end
46
+ else
47
+ b << "if m = obj.#{method} == route.conditions[:#{method}]"
48
+ end
49
+ b << 'true'
50
+ b << 'end'
51
+ conditions << "(#{b.join('; ')})"
52
+ end
53
+
54
+ body << <<-RUBY
55
+ if #{conditions.join(' && ')}
56
+ yield route, matches, params
57
+ end
58
+ RUBY
59
+ }
60
+
61
+ container.instance_eval(<<-RUBY, __FILE__, __LINE__)
62
+ def optimized_each(obj)
63
+ #{body.join("\n")}
64
+ nil
65
+ end
66
+ RUBY
67
+ end
68
+
69
+ def optimize_recognize!
70
+ keys = @recognition_keys.map { |key|
71
+ if key.respond_to?(:call_source)
72
+ key.call_source(:cache, :obj)
73
+ else
74
+ "obj.#{key}"
75
+ end
76
+ }.join(', ')
77
+
78
+ @optimized_recognize_defined = true
79
+
80
+ remove_metaclass_method :recognize
81
+
82
+ instance_eval(<<-RUBY, __FILE__, __LINE__)
83
+ def recognize(obj)
84
+ cache = {}
85
+ container = @recognition_graph[#{keys}]
86
+ optimize_container_iterator(container) unless container.respond_to?(:optimized_each)
87
+
88
+ if block_given?
89
+ container.optimized_each(obj) do |route, matches, params|
90
+ yield route, matches, params
91
+ end
92
+ else
93
+ container.optimized_each(obj) do |route, matches, params|
94
+ return route, matches, params
95
+ end
96
+ end
97
+
98
+ nil
99
+ end
100
+ RUBY
101
+ end
102
+
103
+ # method_defined? can't distinguish between instance
104
+ # and meta methods. So we have to rescue if the method
105
+ # has not been defined in the metaclass yet.
106
+ def remove_metaclass_method(symbol)
107
+ metaclass = class << self; self; end
108
+ metaclass.send(:remove_method, symbol)
109
+ rescue NameError => e
110
+ nil
111
+ end
112
+ end
113
+ end
@@ -121,7 +121,7 @@ module Rack::Mount
121
121
  if part.name
122
122
  s << DynamicSegment.new(part.name, part.expression.to_regexp)
123
123
  else
124
- s << parse_segments(part)
124
+ s << parse_segments(part.expression)
125
125
  end
126
126
  when Reginald::Expression
127
127
  return parse_segments(part)
@@ -9,11 +9,6 @@ module Rack::Mount
9
9
  # new Route objects. Instead use the factory method, RouteSet#add_route
10
10
  # to create new routes and add them to the set.
11
11
  class Route
12
- extend Mixover
13
-
14
- # Include generation and recognition concerns
15
- include Generation::Route, Recognition::Route
16
-
17
12
  # Valid rack application to call if conditions are met
18
13
  attr_reader :app
19
14
 
@@ -27,6 +22,8 @@ module Rack::Mount
27
22
  # Symbol identifier for the route used with named route generations
28
23
  attr_reader :name
29
24
 
25
+ attr_reader :named_captures
26
+
30
27
  def initialize(app, conditions, defaults, name)
31
28
  unless app.respond_to?(:call)
32
29
  raise ArgumentError, 'app must be a valid rack application' \
@@ -54,9 +51,76 @@ module Rack::Mount
54
51
  @conditions[method] = pattern.freeze
55
52
  end
56
53
 
54
+ @named_captures = {}
55
+ @conditions.map { |method, condition|
56
+ next unless condition.respond_to?(:named_captures)
57
+ @named_captures[method] = condition.named_captures.inject({}) { |named_captures, (k, v)|
58
+ named_captures[k.to_sym] = v.last - 1
59
+ named_captures
60
+ }.freeze
61
+ }
62
+ @named_captures.freeze
63
+
64
+ if @conditions.has_key?(:path_info) &&
65
+ !Utils.regexp_anchored?(@conditions[:path_info])
66
+ @prefix = true
67
+ @app = Prefix.new(@app)
68
+ else
69
+ @prefix = false
70
+ end
71
+
57
72
  @conditions.freeze
58
73
  end
59
74
 
75
+ def prefix?
76
+ @prefix
77
+ end
78
+
79
+
80
+ def generation_keys
81
+ @conditions.inject({}) { |keys, (method, condition)|
82
+ if condition.respond_to?(:required_defaults)
83
+ keys.merge!(condition.required_defaults)
84
+ else
85
+ keys
86
+ end
87
+ }
88
+ end
89
+
90
+ def significant_params?
91
+ @has_significant_params ||= @conditions.any? { |method, condition|
92
+ (condition.respond_to?(:required_params) && condition.required_params.any?) ||
93
+ (condition.respond_to?(:required_defaults) && condition.required_defaults.any?)
94
+ }
95
+ end
96
+
97
+ def generate(method, params = {}, recall = {}, options = {})
98
+ if method.nil?
99
+ result = @conditions.inject({}) { |h, (m, condition)|
100
+ if condition.respond_to?(:generate)
101
+ h[m] = condition.generate(params, recall, options)
102
+ end
103
+ h
104
+ }
105
+ return nil if result.values.compact.empty?
106
+ else
107
+ if condition = @conditions[method]
108
+ if condition.respond_to?(:generate)
109
+ result = condition.generate(params, recall, options)
110
+ end
111
+ end
112
+ end
113
+
114
+ if result
115
+ @defaults.each do |key, value|
116
+ params.delete(key) if params[key] == value
117
+ end
118
+ end
119
+
120
+ result
121
+ end
122
+
123
+
60
124
  def inspect #:nodoc:
61
125
  "#<#{self.class.name} @app=#{@app.inspect} @conditions=#{@conditions.inspect} @defaults=#{@defaults.inspect} @name=#{@name.inspect}>"
62
126
  end
@@ -3,16 +3,12 @@ require 'rack/mount/route'
3
3
  require 'rack/mount/utils'
4
4
 
5
5
  module Rack::Mount
6
- class RouteSet
7
- extend Mixover
8
-
9
- # Include generation and recognition concerns
10
- include Generation::RouteSet, Recognition::RouteSet
11
- include Recognition::CodeGeneration
6
+ class RoutingError < StandardError; end
12
7
 
8
+ class RouteSet
13
9
  # Initialize a new RouteSet without optimizations
14
- def self.new_without_optimizations(*args, &block)
15
- new_without_module(Recognition::CodeGeneration, *args, &block)
10
+ def self.new_without_optimizations(options = {}, &block)
11
+ new(options.merge(:_optimize => false), &block)
16
12
  end
17
13
 
18
14
  # Basic RouteSet initializer.
@@ -23,12 +19,19 @@ module Rack::Mount
23
19
  # - <tt>Generation::RouteSet.new</tt>
24
20
  # - <tt>Recognition::RouteSet.new</tt>
25
21
  def initialize(options = {}, &block)
22
+ @parameters_key = options.delete(:parameters_key) || 'rack.routing_args'
23
+ @parameters_key.freeze
24
+
25
+ @named_routes = {}
26
+
27
+ @recognition_key_analyzer = Analysis::Splitting.new
28
+ @generation_key_analyzer = Analysis::Frequency.new
29
+
26
30
  @request_class = options.delete(:request_class) || Rack::Request
27
- @valid_conditions = begin
28
- conditions = @request_class.public_instance_methods
29
- conditions.map! { |m| m.to_sym }
30
- conditions
31
- end
31
+ @valid_conditions = @request_class.public_instance_methods.map! { |m| m.to_sym }
32
+
33
+ extend CodeGeneration unless options[:_optimize] == false
34
+ @optimized_recognize_defined = false
32
35
 
33
36
  @routes = []
34
37
  expire!
@@ -49,26 +52,191 @@ module Rack::Mount
49
52
  # <tt>name</tt>:: Symbol identifier for the route used with named
50
53
  # route generations
51
54
  def add_route(app, conditions = {}, defaults = {}, name = nil)
52
- validate_conditions!(conditions)
55
+ unless conditions.is_a?(Hash)
56
+ raise ArgumentError, 'conditions must be a Hash'
57
+ end
58
+
59
+ unless conditions.all? { |method, pattern|
60
+ @valid_conditions.include?(method)
61
+ }
62
+ raise ArgumentError, 'conditions may only include ' +
63
+ @valid_conditions.inspect
64
+ end
65
+
53
66
  route = Route.new(app, conditions, defaults, name)
54
67
  @routes << route
68
+
69
+ @recognition_key_analyzer << route.conditions
70
+
71
+ @named_routes[route.name] = route if route.name
72
+ @generation_key_analyzer << route.generation_keys
73
+
55
74
  expire!
56
75
  route
57
76
  end
58
77
 
59
- # See <tt>Recognition::RouteSet#call</tt>
78
+ def recognize(obj)
79
+ raise 'route set not finalized' unless @recognition_graph
80
+
81
+ cache = {}
82
+ keys = @recognition_keys.map { |key|
83
+ if key.respond_to?(:call)
84
+ key.call(cache, obj)
85
+ else
86
+ obj.send(key)
87
+ end
88
+ }
89
+
90
+ @recognition_graph[*keys].each do |route|
91
+ matches = {}
92
+ params = route.defaults.dup
93
+
94
+ if route.conditions.all? { |method, condition|
95
+ value = obj.send(method)
96
+ if condition.is_a?(Regexp) && (m = value.match(condition))
97
+ matches[method] = m
98
+ captures = m.captures
99
+ route.named_captures[method].each do |k, i|
100
+ if v = captures[i]
101
+ params[k] = v
102
+ end
103
+ end
104
+ true
105
+ elsif value == condition
106
+ true
107
+ else
108
+ false
109
+ end
110
+ }
111
+ if block_given?
112
+ yield route, matches, params
113
+ else
114
+ return route, matches, params
115
+ end
116
+ end
117
+ end
118
+
119
+ nil
120
+ end
121
+
122
+ X_CASCADE = 'X-Cascade'.freeze
123
+ PASS = 'pass'.freeze
124
+ PATH_INFO = 'PATH_INFO'.freeze
125
+
126
+ # Rack compatible recognition and dispatching method. Routes are
127
+ # tried until one returns a non-catch status code. If no routes
128
+ # match, the catch status code is returned.
129
+ #
130
+ # This method can only be invoked after the RouteSet has been
131
+ # finalized.
60
132
  def call(env)
61
- raise NotImplementedError
133
+ raise 'route set not finalized' unless @recognition_graph
134
+
135
+ env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO])
136
+
137
+ request = nil
138
+ req = @request_class.new(env)
139
+ recognize(req) do |route, matches, params|
140
+ # TODO: We only want to unescape params from uri related methods
141
+ params.each { |k, v| params[k] = Utils.unescape_uri(v) if v.is_a?(String) }
142
+
143
+ if route.prefix?
144
+ env[Prefix::KEY] = matches[:path_info].to_s
145
+ end
146
+
147
+ env[@parameters_key] = params
148
+ result = route.app.call(env)
149
+ return result unless result[1][X_CASCADE] == PASS
150
+ end
151
+
152
+ request || [404, {'Content-Type' => 'text/html', 'X-Cascade' => 'pass'}, ['Not Found']]
62
153
  end
63
154
 
64
- # See <tt>Generation::RouteSet#url</tt>
65
- def url(*args)
66
- raise NotImplementedError
155
+ # Generates a url from Rack env and identifiers or significant keys.
156
+ #
157
+ # To generate a url by named route, pass the name in as a +Symbol+.
158
+ # url(env, :dashboard) # => "/dashboard"
159
+ #
160
+ # Additional parameters can be passed in as a hash
161
+ # url(env, :people, :id => "1") # => "/people/1"
162
+ #
163
+ # If no name route is given, it will fall back to a slower
164
+ # generation search.
165
+ # url(env, :controller => "people", :action => "show", :id => "1")
166
+ # # => "/people/1"
167
+ def url(env, *args)
168
+ named_route, params = nil, {}
169
+
170
+ case args.length
171
+ when 2
172
+ named_route, params = args[0], args[1].dup
173
+ when 1
174
+ if args[0].is_a?(Hash)
175
+ params = args[0].dup
176
+ else
177
+ named_route = args[0]
178
+ end
179
+ else
180
+ raise ArgumentError
181
+ end
182
+
183
+ only_path = params.delete(:only_path)
184
+ recall = env[@parameters_key] || {}
185
+
186
+ unless result = generate(:all, named_route, params, recall,
187
+ :parameterize => lambda { |name, param| Utils.escape_uri(param) })
188
+ return
189
+ end
190
+
191
+ parts, params = result
192
+ return unless parts
193
+
194
+ params.each do |k, v|
195
+ if v
196
+ params[k] = v
197
+ else
198
+ params.delete(k)
199
+ end
200
+ end
201
+
202
+ req = stubbed_request_class.new(env)
203
+ req._stubbed_values = parts.merge(:query_string => Utils.build_nested_query(params))
204
+ only_path ? req.fullpath : req.url
67
205
  end
68
206
 
69
- # See <tt>Generation::RouteSet#generate</tt>
70
- def generate(*args)
71
- raise NotImplementedError
207
+ def generate(method, *args) #:nodoc:
208
+ raise 'route set not finalized' unless @generation_graph
209
+
210
+ method = nil if method == :all
211
+ named_route, params, recall, options = extract_params!(*args)
212
+ merged = recall.merge(params)
213
+ route = nil
214
+
215
+ if named_route
216
+ if route = @named_routes[named_route.to_sym]
217
+ recall = route.defaults.merge(recall)
218
+ url = route.generate(method, params, recall, options)
219
+ [url, params]
220
+ else
221
+ raise RoutingError, "#{named_route} failed to generate from #{params.inspect}"
222
+ end
223
+ else
224
+ keys = @generation_keys.map { |key|
225
+ if k = merged[key]
226
+ k.to_s
227
+ else
228
+ nil
229
+ end
230
+ }
231
+ @generation_graph[*keys].each do |r|
232
+ next unless r.significant_params?
233
+ if url = r.generate(method, params, recall, options)
234
+ return [url, params]
235
+ end
236
+ end
237
+
238
+ raise RoutingError, "No route matches #{params.inspect}"
239
+ end
72
240
  end
73
241
 
74
242
  # Number of routes in the set
@@ -77,6 +245,10 @@ module Rack::Mount
77
245
  end
78
246
 
79
247
  def rehash #:nodoc:
248
+ @recognition_keys = build_recognition_keys
249
+ @recognition_graph = build_recognition_graph
250
+ @generation_keys = build_generation_keys
251
+ @generation_graph = build_generation_graph
80
252
  end
81
253
 
82
254
  # Finalizes the set and builds optimized data structures. You *must*
@@ -85,7 +257,11 @@ module Rack::Mount
85
257
  def freeze
86
258
  unless frozen?
87
259
  rehash
88
- flush!
260
+
261
+ @recognition_key_analyzer = nil
262
+ @generation_key_analyzer = nil
263
+ @valid_conditions = nil
264
+
89
265
  @routes.each { |route| route.freeze }
90
266
  @routes.freeze
91
267
  end
@@ -104,44 +280,35 @@ module Rack::Mount
104
280
  hash[:@recognition_graph] = graph.dup
105
281
  end
106
282
 
107
- included_modules = (class << self; included_modules; end)
108
- included_modules.reject! { |mod| mod == Kernel }
109
- hash[:included_modules] = included_modules
110
-
111
283
  hash
112
284
  end
113
285
 
114
286
  def marshal_load(hash) #:nodoc:
115
- hash.delete(:included_modules).reverse.each { |mod| extend(mod) }
116
-
117
287
  hash.each do |ivar, value|
118
288
  instance_variable_set(ivar, value)
119
289
  end
120
290
  end
121
291
 
292
+ protected
293
+ def recognition_stats
294
+ { :keys => @recognition_keys,
295
+ :keys_size => @recognition_keys.size,
296
+ :graph_size => @recognition_graph.size,
297
+ :graph_height => @recognition_graph.height,
298
+ :graph_average_height => @recognition_graph.average_height }
299
+ end
300
+
122
301
  private
123
302
  def expire! #:nodoc:
124
- end
303
+ @recognition_keys = @recognition_graph = nil
304
+ @recognition_key_analyzer.expire!
125
305
 
126
- def flush! #:nodoc:
127
- @valid_conditions = nil
306
+ @generation_keys = @generation_graph = nil
307
+ @generation_key_analyzer.expire!
128
308
  end
129
309
 
130
310
  def instance_variables_to_serialize
131
- instance_variables.map { |ivar| ivar.to_sym }
132
- end
133
-
134
- def validate_conditions!(conditions)
135
- unless conditions.is_a?(Hash)
136
- raise ArgumentError, 'conditions must be a Hash'
137
- end
138
-
139
- unless conditions.all? { |method, pattern|
140
- @valid_conditions.include?(method)
141
- }
142
- raise ArgumentError, 'conditions may only include ' +
143
- @valid_conditions.inspect
144
- end
311
+ instance_variables.map { |ivar| ivar.to_sym } - [:@stubbed_request_class, :@optimized_recognize_defined]
145
312
  end
146
313
 
147
314
  # An internal helper method for constructing a nested set from
@@ -162,5 +329,81 @@ module Rack::Mount
162
329
  end
163
330
  graph
164
331
  end
332
+
333
+ def build_recognition_graph
334
+ build_nested_route_set(@recognition_keys) { |k, i|
335
+ @recognition_key_analyzer.possible_keys[i][k]
336
+ }
337
+ end
338
+
339
+ def build_recognition_keys
340
+ @recognition_key_analyzer.report
341
+ end
342
+
343
+ def build_generation_graph
344
+ build_nested_route_set(@generation_keys) { |k, i|
345
+ throw :skip unless @routes[i].significant_params?
346
+
347
+ if k = @generation_key_analyzer.possible_keys[i][k]
348
+ k.to_s
349
+ else
350
+ nil
351
+ end
352
+ }
353
+ end
354
+
355
+ def build_generation_keys
356
+ @generation_key_analyzer.report
357
+ end
358
+
359
+ def extract_params!(*args)
360
+ case args.length
361
+ when 4
362
+ named_route, params, recall, options = args
363
+ when 3
364
+ if args[0].is_a?(Hash)
365
+ params, recall, options = args
366
+ else
367
+ named_route, params, recall = args
368
+ end
369
+ when 2
370
+ if args[0].is_a?(Hash)
371
+ params, recall = args
372
+ else
373
+ named_route, params = args
374
+ end
375
+ when 1
376
+ if args[0].is_a?(Hash)
377
+ params = args[0]
378
+ else
379
+ named_route = args[0]
380
+ end
381
+ else
382
+ raise ArgumentError
383
+ end
384
+
385
+ named_route ||= nil
386
+ params ||= {}
387
+ recall ||= {}
388
+ options ||= {}
389
+
390
+ [named_route, params.dup, recall.dup, options.dup]
391
+ end
392
+
393
+ def stubbed_request_class
394
+ @stubbed_request_class ||= begin
395
+ klass = Class.new(@request_class)
396
+ klass.public_instance_methods.each do |method|
397
+ next if method =~ /^__|object_id/
398
+ klass.class_eval <<-RUBY
399
+ def #{method}(*args, &block)
400
+ @_stubbed_values[:#{method}] || super
401
+ end
402
+ RUBY
403
+ end
404
+ klass.class_eval { attr_accessor :_stubbed_values }
405
+ klass
406
+ end
407
+ end
165
408
  end
166
409
  end