rack-mount 0.6.1 → 0.6.2

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