actionpack 4.1.7 → 4.2.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +311 -527
  3. data/README.rdoc +7 -2
  4. data/lib/abstract_controller/base.rb +16 -6
  5. data/lib/abstract_controller/callbacks.rb +28 -51
  6. data/lib/abstract_controller/helpers.rb +11 -4
  7. data/lib/abstract_controller/railties/routes_helpers.rb +3 -3
  8. data/lib/abstract_controller/url_for.rb +1 -1
  9. data/lib/action_controller/base.rb +2 -1
  10. data/lib/action_controller/caching/fragments.rb +7 -1
  11. data/lib/action_controller/caching.rb +1 -1
  12. data/lib/action_controller/log_subscriber.rb +26 -26
  13. data/lib/action_controller/metal/conditional_get.rb +37 -12
  14. data/lib/action_controller/metal/etag_with_template_digest.rb +50 -0
  15. data/lib/action_controller/metal/exceptions.rb +1 -1
  16. data/lib/action_controller/metal/force_ssl.rb +1 -1
  17. data/lib/action_controller/metal/head.rb +7 -3
  18. data/lib/action_controller/metal/http_authentication.rb +14 -9
  19. data/lib/action_controller/metal/instrumentation.rb +8 -5
  20. data/lib/action_controller/metal/live.rb +57 -6
  21. data/lib/action_controller/metal/mime_responds.rb +23 -246
  22. data/lib/action_controller/metal/params_wrapper.rb +2 -2
  23. data/lib/action_controller/metal/rack_delegation.rb +1 -1
  24. data/lib/action_controller/metal/redirecting.rb +14 -8
  25. data/lib/action_controller/metal/renderers.rb +30 -10
  26. data/lib/action_controller/metal/rendering.rb +2 -6
  27. data/lib/action_controller/metal/request_forgery_protection.rb +78 -7
  28. data/lib/action_controller/metal/streaming.rb +1 -1
  29. data/lib/action_controller/metal/strong_parameters.rb +125 -12
  30. data/lib/action_controller/metal/url_for.rb +11 -12
  31. data/lib/action_controller/metal.rb +12 -11
  32. data/lib/action_controller/model_naming.rb +1 -1
  33. data/lib/action_controller/railtie.rb +4 -0
  34. data/lib/action_controller/test_case.rb +112 -75
  35. data/lib/action_controller.rb +1 -1
  36. data/lib/action_dispatch/http/cache.rb +5 -4
  37. data/lib/action_dispatch/http/filter_parameters.rb +2 -2
  38. data/lib/action_dispatch/http/headers.rb +43 -9
  39. data/lib/action_dispatch/http/mime_negotiation.rb +10 -3
  40. data/lib/action_dispatch/http/mime_type.rb +2 -2
  41. data/lib/action_dispatch/http/parameter_filter.rb +1 -1
  42. data/lib/action_dispatch/http/parameters.rb +11 -26
  43. data/lib/action_dispatch/http/request.rb +37 -11
  44. data/lib/action_dispatch/http/response.rb +70 -18
  45. data/lib/action_dispatch/http/upload.rb +3 -8
  46. data/lib/action_dispatch/http/url.rb +88 -69
  47. data/lib/action_dispatch/journey/formatter.rb +33 -17
  48. data/lib/action_dispatch/journey/gtg/builder.rb +3 -3
  49. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -7
  50. data/lib/action_dispatch/journey/gtg/transition_table.rb +20 -28
  51. data/lib/action_dispatch/journey/nfa/dot.rb +2 -2
  52. data/lib/action_dispatch/journey/nfa/simulator.rb +1 -1
  53. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -5
  54. data/lib/action_dispatch/journey/nodes/node.rb +4 -0
  55. data/lib/action_dispatch/journey/parser.rb +52 -60
  56. data/lib/action_dispatch/journey/parser.y +11 -10
  57. data/lib/action_dispatch/journey/path/pattern.rb +16 -19
  58. data/lib/action_dispatch/journey/route.rb +3 -18
  59. data/lib/action_dispatch/journey/router/strexp.rb +9 -6
  60. data/lib/action_dispatch/journey/router.rb +53 -77
  61. data/lib/action_dispatch/journey/scanner.rb +5 -5
  62. data/lib/action_dispatch/journey/visitors.rb +81 -92
  63. data/lib/action_dispatch/journey/visualizer/fsm.css +0 -4
  64. data/lib/action_dispatch/journey/visualizer/index.html.erb +2 -2
  65. data/lib/action_dispatch/middleware/callbacks.rb +1 -1
  66. data/lib/action_dispatch/middleware/cookies.rb +29 -29
  67. data/lib/action_dispatch/middleware/debug_exceptions.rb +15 -4
  68. data/lib/action_dispatch/middleware/exception_wrapper.rb +50 -18
  69. data/lib/action_dispatch/middleware/flash.rb +13 -7
  70. data/lib/action_dispatch/middleware/params_parser.rb +1 -1
  71. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -3
  72. data/lib/action_dispatch/middleware/remote_ip.rb +40 -54
  73. data/lib/action_dispatch/middleware/request_id.rb +1 -1
  74. data/lib/action_dispatch/middleware/session/cookie_store.rb +1 -1
  75. data/lib/action_dispatch/middleware/show_exceptions.rb +1 -0
  76. data/lib/action_dispatch/middleware/static.rb +66 -37
  77. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +21 -19
  78. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +37 -9
  79. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +2 -8
  80. data/lib/action_dispatch/middleware/templates/rescues/{diagnostics.erb → diagnostics.html.erb} +0 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  82. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +6 -0
  83. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +4 -0
  84. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +2 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -24
  86. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +0 -1
  87. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +120 -64
  88. data/lib/action_dispatch/routing/endpoint.rb +10 -0
  89. data/lib/action_dispatch/routing/inspector.rb +5 -12
  90. data/lib/action_dispatch/routing/mapper.rb +410 -281
  91. data/lib/action_dispatch/routing/polymorphic_routes.rb +191 -79
  92. data/lib/action_dispatch/routing/redirection.rb +10 -12
  93. data/lib/action_dispatch/routing/route_set.rb +297 -168
  94. data/lib/action_dispatch/routing/url_for.rb +15 -4
  95. data/lib/action_dispatch/testing/assertions/dom.rb +2 -26
  96. data/lib/action_dispatch/testing/assertions/response.rb +2 -7
  97. data/lib/action_dispatch/testing/assertions/routing.rb +22 -22
  98. data/lib/action_dispatch/testing/assertions/selector.rb +2 -429
  99. data/lib/action_dispatch/testing/assertions/tag.rb +2 -134
  100. data/lib/action_dispatch/testing/assertions.rb +11 -7
  101. data/lib/action_dispatch/testing/integration.rb +24 -19
  102. data/lib/action_dispatch/testing/test_request.rb +1 -1
  103. data/lib/action_dispatch/testing/test_response.rb +7 -0
  104. data/lib/action_pack/gem_version.rb +3 -3
  105. metadata +55 -13
  106. data/lib/action_controller/metal/responder.rb +0 -297
@@ -0,0 +1,10 @@
1
+ module ActionDispatch
2
+ module Routing
3
+ class Endpoint # :nodoc:
4
+ def dispatcher?; false; end
5
+ def redirect?; false; end
6
+ def matches?(req); true; end
7
+ def app; self; end
8
+ end
9
+ end
10
+ end
@@ -5,22 +5,15 @@ module ActionDispatch
5
5
  module Routing
6
6
  class RouteWrapper < SimpleDelegator
7
7
  def endpoint
8
- rack_app ? rack_app.inspect : "#{controller}##{action}"
8
+ app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
9
9
  end
10
10
 
11
11
  def constraints
12
12
  requirements.except(:controller, :action)
13
13
  end
14
14
 
15
- def rack_app(app = self.app)
16
- @rack_app ||= begin
17
- class_name = app.class.name.to_s
18
- if class_name == "ActionDispatch::Routing::Mapper::Constraints"
19
- rack_app(app.app)
20
- elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
21
- app
22
- end
23
- end
15
+ def rack_app
16
+ app.app
24
17
  end
25
18
 
26
19
  def verb
@@ -55,7 +48,7 @@ module ActionDispatch
55
48
  def reqs
56
49
  @reqs ||= begin
57
50
  reqs = endpoint
58
- reqs += " #{constraints.to_s}" unless constraints.empty?
51
+ reqs += " #{constraints}" unless constraints.empty?
59
52
  reqs
60
53
  end
61
54
  end
@@ -73,7 +66,7 @@ module ActionDispatch
73
66
  end
74
67
 
75
68
  def engine?
76
- rack_app && rack_app.respond_to?(:routes)
69
+ rack_app.respond_to?(:routes)
77
70
  end
78
71
  end
79
72
 
@@ -4,132 +4,201 @@ require 'active_support/core_ext/hash/slice'
4
4
  require 'active_support/core_ext/enumerable'
5
5
  require 'active_support/core_ext/array/extract_options'
6
6
  require 'active_support/core_ext/module/remove_method'
7
+ require 'active_support/core_ext/string/filters'
7
8
  require 'active_support/inflector'
8
9
  require 'action_dispatch/routing/redirection'
10
+ require 'action_dispatch/routing/endpoint'
11
+ require 'active_support/deprecation'
9
12
 
10
13
  module ActionDispatch
11
14
  module Routing
12
15
  class Mapper
13
16
  URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
14
- SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
15
- :controller, :action, :path_names, :constraints,
16
- :shallow, :blocks, :defaults, :options]
17
-
18
- class Constraints #:nodoc:
19
- def self.new(app, constraints, request = Rack::Request)
20
- if constraints.any?
21
- super(app, constraints, request)
22
- else
23
- app
24
- end
25
- end
26
17
 
18
+ class Constraints < Endpoint #:nodoc:
27
19
  attr_reader :app, :constraints
28
20
 
29
- def initialize(app, constraints, request)
30
- @app, @constraints, @request = app, constraints, request
21
+ def initialize(app, constraints, dispatcher_p)
22
+ # Unwrap Constraints objects. I don't actually think it's possible
23
+ # to pass a Constraints object to this constructor, but there were
24
+ # multiple places that kept testing children of this object. I
25
+ # *think* they were just being defensive, but I have no idea.
26
+ if app.is_a?(self.class)
27
+ constraints += app.constraints
28
+ app = app.app
29
+ end
30
+
31
+ @dispatcher = dispatcher_p
32
+
33
+ @app, @constraints, = app, constraints
31
34
  end
32
35
 
33
- def matches?(env)
34
- req = @request.new(env)
36
+ def dispatcher?; @dispatcher; end
35
37
 
38
+ def matches?(req)
36
39
  @constraints.all? do |constraint|
37
40
  (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
38
41
  (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
39
42
  end
40
- ensure
41
- req.reset_parameters
42
43
  end
43
44
 
44
- def call(env)
45
- matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
45
+ def serve(req)
46
+ return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
47
+
48
+ if dispatcher?
49
+ @app.serve req
50
+ else
51
+ @app.call req.env
52
+ end
46
53
  end
47
54
 
48
55
  private
49
56
  def constraint_args(constraint, request)
50
- constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
57
+ constraint.arity == 1 ? [request] : [request.path_parameters, request]
51
58
  end
52
59
  end
53
60
 
54
61
  class Mapping #:nodoc:
55
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
56
62
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
57
- WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
58
63
 
59
- attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
64
+ attr_reader :requirements, :conditions, :defaults
65
+ attr_reader :to, :default_controller, :default_action, :as, :anchor
60
66
 
61
- def initialize(set, scope, path, options)
62
- @set, @scope, @path, @options = set, scope, path, options
63
- @requirements, @conditions, @defaults = {}, {}, {}
67
+ def self.build(scope, set, path, as, options)
68
+ options = scope[:options].merge(options) if scope[:options]
64
69
 
65
- normalize_options!
66
- normalize_path!
67
- normalize_requirements!
68
- normalize_conditions!
69
- normalize_defaults!
70
+ options.delete :only
71
+ options.delete :except
72
+ options.delete :shallow_path
73
+ options.delete :shallow_prefix
74
+ options.delete :shallow
75
+
76
+ defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
77
+
78
+ new scope, set, path, defaults, as, options
79
+ end
80
+
81
+ def initialize(scope, set, path, defaults, as, options)
82
+ @requirements, @conditions = {}, {}
83
+ @defaults = defaults
84
+ @set = set
85
+
86
+ @to = options.delete :to
87
+ @default_controller = options.delete(:controller) || scope[:controller]
88
+ @default_action = options.delete(:action) || scope[:action]
89
+ @as = as
90
+ @anchor = options.delete :anchor
91
+
92
+ formatted = options.delete :format
93
+ via = Array(options.delete(:via) { [] })
94
+ options_constraints = options.delete :constraints
95
+
96
+ path = normalize_path! path, formatted
97
+ ast = path_ast path
98
+ path_params = path_params ast
99
+
100
+ options = normalize_options!(options, formatted, path_params, ast, scope[:module])
101
+
102
+
103
+ split_constraints(path_params, scope[:constraints]) if scope[:constraints]
104
+ constraints = constraints(options, path_params)
105
+
106
+ split_constraints path_params, constraints
107
+
108
+ @blocks = blocks(options_constraints, scope[:blocks])
109
+
110
+ if options_constraints.is_a?(Hash)
111
+ split_constraints path_params, options_constraints
112
+ options_constraints.each do |key, default|
113
+ if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
114
+ @defaults[key] ||= default
115
+ end
116
+ end
117
+ end
118
+
119
+ normalize_format!(formatted)
120
+
121
+ @conditions[:path_info] = path
122
+ @conditions[:parsed_path_info] = ast
123
+
124
+ add_request_method(via, @conditions)
125
+ normalize_defaults!(options)
70
126
  end
71
127
 
72
128
  def to_route
73
- [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
129
+ [ app(@blocks), conditions, requirements, defaults, as, anchor ]
74
130
  end
75
131
 
76
132
  private
77
133
 
78
- def normalize_path!
79
- raise ArgumentError, "path is required" if @path.blank?
80
- @path = Mapper.normalize_path(@path)
134
+ def normalize_path!(path, format)
135
+ path = Mapper.normalize_path(path)
81
136
 
82
- if required_format?
83
- @path = "#{@path}.:format"
84
- elsif optional_format?
85
- @path = "#{@path}(.:format)"
137
+ if format == true
138
+ "#{path}.:format"
139
+ elsif optional_format?(path, format)
140
+ "#{path}(.:format)"
141
+ else
142
+ path
86
143
  end
87
144
  end
88
145
 
89
- def required_format?
90
- options[:format] == true
146
+ def optional_format?(path, format)
147
+ format != false && !path.include?(':format') && !path.end_with?('/')
91
148
  end
92
149
 
93
- def optional_format?
94
- options[:format] != false && !path.include?(':format') && !path.end_with?('/')
95
- end
96
-
97
- def normalize_options!
98
- @options.reverse_merge!(scope[:options]) if scope[:options]
99
- path_without_format = path.sub(/\(\.:format\)$/, '')
100
-
150
+ def normalize_options!(options, formatted, path_params, path_ast, modyoule)
101
151
  # Add a constraint for wildcard route to make it non-greedy and match the
102
152
  # optional format part of the route by default
103
- if path_without_format.match(WILDCARD_PATH) && @options[:format] != false
104
- @options[$1.to_sym] ||= /.+?/
153
+ if formatted != false
154
+ path_ast.grep(Journey::Nodes::Star) do |node|
155
+ options[node.name.to_sym] ||= /.+?/
156
+ end
105
157
  end
106
158
 
107
- if path_without_format.match(':controller')
108
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]
159
+ if path_params.include?(:controller)
160
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
109
161
 
110
162
  # Add a default constraint for :controller path segments that matches namespaced
111
163
  # controllers with default routes like :controller/:action/:id(.:format), e.g:
112
164
  # GET /admin/products/show/1
113
165
  # => { controller: 'admin/products', action: 'show', id: '1' }
114
- @options[:controller] ||= /.+?/
166
+ options[:controller] ||= /.+?/
115
167
  end
116
168
 
117
- @options.merge!(default_controller_and_action)
169
+ if to.respond_to? :call
170
+ options
171
+ else
172
+ to_endpoint = split_to to
173
+ controller = to_endpoint[0] || default_controller
174
+ action = to_endpoint[1] || default_action
175
+
176
+ controller = add_controller_module(controller, modyoule)
177
+
178
+ options.merge! check_controller_and_action(path_params, controller, action)
179
+ end
118
180
  end
119
181
 
120
- def normalize_requirements!
121
- constraints.each do |key, requirement|
122
- next unless segment_keys.include?(key) || key == :controller
123
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
124
- @requirements[key] = requirement
182
+ def split_constraints(path_params, constraints)
183
+ constraints.each_pair do |key, requirement|
184
+ if path_params.include?(key) || key == :controller
185
+ verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
186
+ @requirements[key] = requirement
187
+ else
188
+ @conditions[key] = requirement
189
+ end
125
190
  end
191
+ end
126
192
 
127
- if options[:format] == true
193
+ def normalize_format!(formatted)
194
+ if formatted == true
128
195
  @requirements[:format] ||= /.+/
129
- elsif Regexp === options[:format]
130
- @requirements[:format] = options[:format]
131
- elsif String === options[:format]
132
- @requirements[:format] = Regexp.compile(options[:format])
196
+ elsif Regexp === formatted
197
+ @requirements[:format] = formatted
198
+ @defaults[:format] = nil
199
+ elsif String === formatted
200
+ @requirements[:format] = Regexp.compile(formatted)
201
+ @defaults[:format] = formatted
133
202
  end
134
203
  end
135
204
 
@@ -143,169 +212,147 @@ module ActionDispatch
143
212
  end
144
213
  end
145
214
 
146
- def normalize_defaults!
147
- @defaults.merge!(scope[:defaults]) if scope[:defaults]
148
- @defaults.merge!(options[:defaults]) if options[:defaults]
149
-
150
- options.each do |key, default|
151
- unless Regexp === default || IGNORE_OPTIONS.include?(key)
215
+ def normalize_defaults!(options)
216
+ options.each_pair do |key, default|
217
+ unless Regexp === default
152
218
  @defaults[key] = default
153
219
  end
154
220
  end
155
-
156
- if options[:constraints].is_a?(Hash)
157
- options[:constraints].each do |key, default|
158
- if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
159
- @defaults[key] ||= default
160
- end
161
- end
162
- end
163
-
164
- if Regexp === options[:format]
165
- @defaults[:format] = nil
166
- elsif String === options[:format]
167
- @defaults[:format] = options[:format]
168
- end
169
221
  end
170
222
 
171
- def normalize_conditions!
172
- @conditions[:path_info] = path
173
-
174
- constraints.each do |key, condition|
175
- unless segment_keys.include?(key) || key == :controller
176
- @conditions[key] = condition
177
- end
178
- end
179
-
180
- required_defaults = []
181
- options.each do |key, required_default|
182
- unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
183
- required_defaults << key
184
- end
223
+ def verify_callable_constraint(callable_constraint)
224
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
225
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
185
226
  end
186
- @conditions[:required_defaults] = required_defaults
227
+ end
187
228
 
188
- via_all = options.delete(:via) if options[:via] == :all
229
+ def add_request_method(via, conditions)
230
+ return if via == [:all]
189
231
 
190
- if !via_all && options[:via].blank?
232
+ if via.empty?
191
233
  msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
192
234
  "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
193
235
  "If you want to expose your action to GET, use `get` in the router:\n" \
194
236
  " Instead of: match \"controller#action\"\n" \
195
237
  " Do: get \"controller#action\""
196
- raise msg
238
+ raise ArgumentError, msg
197
239
  end
198
240
 
199
- if via = options[:via]
200
- @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase }
201
- end
241
+ conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
202
242
  end
203
243
 
204
- def app
205
- Constraints.new(endpoint, blocks, @set.request_class)
206
- end
207
-
208
- def default_controller_and_action
244
+ def app(blocks)
209
245
  if to.respond_to?(:call)
210
- { }
246
+ Constraints.new(to, blocks, false)
247
+ elsif blocks.any?
248
+ Constraints.new(dispatcher(defaults), blocks, true)
211
249
  else
212
- if to.is_a?(String)
213
- controller, action = to.split('#')
214
- elsif to.is_a?(Symbol)
215
- action = to.to_s
216
- end
217
-
218
- controller ||= default_controller
219
- action ||= default_action
220
-
221
- if @scope[:module] && !controller.is_a?(Regexp)
222
- if controller =~ %r{\A/}
223
- controller = controller[1..-1]
224
- else
225
- controller = [@scope[:module], controller].compact.join("/").presence
226
- end
227
- end
228
-
229
- if controller.is_a?(String) && controller =~ %r{\A/}
230
- raise ArgumentError, "controller name should not start with a slash"
231
- end
250
+ dispatcher(defaults)
251
+ end
252
+ end
232
253
 
233
- controller = controller.to_s unless controller.is_a?(Regexp)
234
- action = action.to_s unless action.is_a?(Regexp)
254
+ def check_controller_and_action(path_params, controller, action)
255
+ hash = check_part(:controller, controller, path_params, {}) do |part|
256
+ translate_controller(part) {
257
+ message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
258
+ message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
235
259
 
236
- if controller.blank? && segment_keys.exclude?(:controller)
237
- message = "Missing :controller key on routes definition, please check your routes."
238
260
  raise ArgumentError, message
239
- end
261
+ }
262
+ end
240
263
 
241
- if action.blank? && segment_keys.exclude?(:action)
242
- message = "Missing :action key on routes definition, please check your routes."
243
- raise ArgumentError, message
244
- end
264
+ check_part(:action, action, path_params, hash) { |part|
265
+ part.is_a?(Regexp) ? part : part.to_s
266
+ }
267
+ end
245
268
 
246
- if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
247
- message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems."
248
- message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
269
+ def check_part(name, part, path_params, hash)
270
+ if part
271
+ hash[name] = yield(part)
272
+ else
273
+ unless path_params.include?(name)
274
+ message = "Missing :#{name} key on routes definition, please check your routes."
249
275
  raise ArgumentError, message
250
276
  end
251
-
252
- hash = {}
253
- hash[:controller] = controller unless controller.blank?
254
- hash[:action] = action unless action.blank?
255
- hash
256
277
  end
257
- end
258
-
259
- def blocks
260
- if options[:constraints].present? && !options[:constraints].is_a?(Hash)
261
- [options[:constraints]]
278
+ hash
279
+ end
280
+
281
+ def split_to(to)
282
+ case to
283
+ when Symbol
284
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
285
+ Defining a route where `to` is a symbol is deprecated.
286
+ Please change `to: :#{to}` to `action: :#{to}`.
287
+ MSG
288
+
289
+ [nil, to.to_s]
290
+ when /#/ then to.split('#')
291
+ when String
292
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
293
+ Defining a route where `to` is a controller without an action is deprecated.
294
+ Please change `to: :#{to}` to `controller: :#{to}`.
295
+ MSG
296
+
297
+ [to, nil]
262
298
  else
263
- scope[:blocks] || []
299
+ []
264
300
  end
265
301
  end
266
302
 
267
- def constraints
268
- @constraints ||= {}.tap do |constraints|
269
- constraints.merge!(scope[:constraints]) if scope[:constraints]
270
-
271
- options.except(*IGNORE_OPTIONS).each do |key, option|
272
- constraints[key] = option if Regexp === option
303
+ def add_controller_module(controller, modyoule)
304
+ if modyoule && !controller.is_a?(Regexp)
305
+ if controller =~ %r{\A/}
306
+ controller[1..-1]
307
+ else
308
+ [modyoule, controller].compact.join("/")
273
309
  end
274
-
275
- constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
310
+ else
311
+ controller
276
312
  end
277
313
  end
278
314
 
279
- def segment_keys
280
- @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
281
- end
282
-
283
- def path_pattern
284
- Journey::Path::Pattern.new(strexp)
285
- end
315
+ def translate_controller(controller)
316
+ return controller if Regexp === controller
317
+ return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
286
318
 
287
- def strexp
288
- Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
319
+ yield
289
320
  end
290
321
 
291
- def endpoint
292
- to.respond_to?(:call) ? to : dispatcher
322
+ def blocks(options_constraints, scope_blocks)
323
+ if options_constraints && !options_constraints.is_a?(Hash)
324
+ verify_callable_constraint(options_constraints)
325
+ [options_constraints]
326
+ else
327
+ scope_blocks || []
328
+ end
293
329
  end
294
330
 
295
- def dispatcher
296
- Routing::RouteSet::Dispatcher.new(:defaults => defaults)
331
+ def constraints(options, path_params)
332
+ constraints = {}
333
+ required_defaults = []
334
+ options.each_pair do |key, option|
335
+ if Regexp === option
336
+ constraints[key] = option
337
+ else
338
+ required_defaults << key unless path_params.include?(key)
339
+ end
340
+ end
341
+ @conditions[:required_defaults] = required_defaults
342
+ constraints
297
343
  end
298
344
 
299
- def to
300
- options[:to]
345
+ def path_params(ast)
346
+ ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
301
347
  end
302
348
 
303
- def default_controller
304
- options[:controller] || scope[:controller]
349
+ def path_ast(path)
350
+ parser = Journey::Parser.new
351
+ parser.parse path
305
352
  end
306
353
 
307
- def default_action
308
- options[:action] || scope[:action]
354
+ def dispatcher(defaults)
355
+ @set.dispatcher defaults
309
356
  end
310
357
  end
311
358
 
@@ -342,19 +389,18 @@ module ActionDispatch
342
389
 
343
390
  # Matches a url pattern to one or more routes.
344
391
  #
345
- # You should not use the `match` method in your router
392
+ # You should not use the +match+ method in your router
346
393
  # without specifying an HTTP method.
347
394
  #
348
395
  # If you want to expose your action to both GET and POST, use:
349
- #
396
+ #
350
397
  # # sets :controller, :action and :id in params
351
398
  # match ':controller/:action/:id', via: [:get, :post]
352
399
  #
353
- # Note that +:controller+, +:action+, +:id+ are interpreted as url query
354
- # parameters and thus available as +params+
355
- # in an action.
400
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as url
401
+ # query parameters and thus available through +params+ in an action.
356
402
  #
357
- # If you want to expose your action to GET, use `get` in the router:
403
+ # If you want to expose your action to GET, use +get+ in the router:
358
404
  #
359
405
  # Instead of:
360
406
  #
@@ -374,24 +420,28 @@ module ActionDispatch
374
420
  # # params[:category] = 'rock/classic'
375
421
  # # params[:title] = 'stairway-to-heaven'
376
422
  #
423
+ # To match a wildcard parameter, it must have a name assigned to it.
424
+ # Without a variable name to attach the glob parameter to, the route
425
+ # can't be parsed.
426
+ #
377
427
  # When a pattern points to an internal route, the route's +:action+ and
378
428
  # +:controller+ should be set in options or hash shorthand. Examples:
379
429
  #
380
- # match 'photos/:id' => 'photos#show', via: [:get]
381
- # match 'photos/:id', to: 'photos#show', via: [:get]
382
- # match 'photos/:id', controller: 'photos', action: 'show', via: [:get]
430
+ # match 'photos/:id' => 'photos#show', via: :get
431
+ # match 'photos/:id', to: 'photos#show', via: :get
432
+ # match 'photos/:id', controller: 'photos', action: 'show', via: :get
383
433
  #
384
434
  # A pattern can also point to a +Rack+ endpoint i.e. anything that
385
435
  # responds to +call+:
386
436
  #
387
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: [:get]
388
- # match 'photos/:id', to: PhotoRackApp, via: [:get]
437
+ # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
438
+ # match 'photos/:id', to: PhotoRackApp, via: :get
389
439
  # # Yes, controller actions are just rack endpoints
390
- # match 'photos/:id', to: PhotosController.action(:show), via: [:get]
440
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
391
441
  #
392
442
  # Because requesting various HTTP verbs with a single action has security
393
443
  # implications, you must either specify the actions in
394
- # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers]
444
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
395
445
  # instead +match+
396
446
  #
397
447
  # === Options
@@ -405,7 +455,7 @@ module ActionDispatch
405
455
  # The route's action.
406
456
  #
407
457
  # [:param]
408
- # Overrides the default resource identifier `:id` (name of the
458
+ # Overrides the default resource identifier +:id+ (name of the
409
459
  # dynamic segment used to generate the routes).
410
460
  # You can access that segment from your controller using
411
461
  # <tt>params[<:param>]</tt>.
@@ -416,7 +466,7 @@ module ActionDispatch
416
466
  # [:module]
417
467
  # The namespace for :controller.
418
468
  #
419
- # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: [:get]
469
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
420
470
  # # => Sekret::PostsController
421
471
  #
422
472
  # See <tt>Scoping#namespace</tt> for its scope equivalent.
@@ -435,9 +485,9 @@ module ActionDispatch
435
485
  # Points to a +Rack+ endpoint. Can be an object that responds to
436
486
  # +call+ or a string representing a controller's action.
437
487
  #
438
- # match 'path', to: 'controller#action', via: [:get]
439
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: [:get]
440
- # match 'path', to: RackApp, via: [:get]
488
+ # match 'path', to: 'controller#action', via: :get
489
+ # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
490
+ # match 'path', to: RackApp, via: :get
441
491
  #
442
492
  # [:on]
443
493
  # Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -462,14 +512,14 @@ module ActionDispatch
462
512
  # other than path can also be specified with any object
463
513
  # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
464
514
  #
465
- # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: [:get]
515
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
466
516
  #
467
- # match 'json_only', constraints: { format: 'json' }, via: [:get]
517
+ # match 'json_only', constraints: { format: 'json' }, via: :get
468
518
  #
469
519
  # class Whitelist
470
520
  # def matches?(request) request.remote_ip == '1.2.3.4' end
471
521
  # end
472
- # match 'path', to: 'c#a', constraints: Whitelist.new, via: [:get]
522
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
473
523
  #
474
524
  # See <tt>Scoping#constraints</tt> for more examples with its scope
475
525
  # equivalent.
@@ -478,7 +528,7 @@ module ActionDispatch
478
528
  # Sets defaults for parameters
479
529
  #
480
530
  # # Sets params[:format] to 'jpg' by default
481
- # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: [:get]
531
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
482
532
  #
483
533
  # See <tt>Scoping#defaults</tt> for its scope equivalent.
484
534
  #
@@ -487,7 +537,7 @@ module ActionDispatch
487
537
  # false, the pattern matches any request prefixed with the given path.
488
538
  #
489
539
  # # Matches any request starting with 'path'
490
- # match 'path', to: 'c#a', anchor: false, via: [:get]
540
+ # match 'path', to: 'c#a', anchor: false, via: :get
491
541
  #
492
542
  # [:format]
493
543
  # Allows you to specify the default value for optional +format+
@@ -529,13 +579,15 @@ module ActionDispatch
529
579
 
530
580
  raise "A rack application must be specified" unless path
531
581
 
532
- options[:as] ||= app_name(app)
582
+ rails_app = rails_app? app
583
+ options[:as] ||= app_name(app, rails_app)
584
+
533
585
  target_as = name_for_action(options[:as], path)
534
586
  options[:via] ||= :all
535
587
 
536
588
  match(path, options.merge(:to => app, :anchor => false, :format => false))
537
589
 
538
- define_generate_prefix(app, target_as)
590
+ define_generate_prefix(app, target_as) if rails_app
539
591
  self
540
592
  end
541
593
 
@@ -556,35 +608,36 @@ module ActionDispatch
556
608
  end
557
609
 
558
610
  private
559
- def app_name(app)
560
- return unless app.respond_to?(:routes)
611
+ def rails_app?(app)
612
+ app.is_a?(Class) && app < Rails::Railtie
613
+ end
561
614
 
562
- if app.respond_to?(:railtie_name)
615
+ def app_name(app, rails_app)
616
+ if rails_app
563
617
  app.railtie_name
564
- else
565
- class_name = app.class.is_a?(Class) ? app.name : app.class.name
618
+ elsif app.is_a?(Class)
619
+ class_name = app.name
566
620
  ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
567
621
  end
568
622
  end
569
623
 
570
624
  def define_generate_prefix(app, name)
571
- return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
572
-
573
- _route = @set.named_routes.routes[name.to_sym]
625
+ _route = @set.named_routes.get name
574
626
  _routes = @set
575
627
  app.routes.define_mounted_helper(name)
576
- app.routes.singleton_class.class_eval do
577
- redefine_method :mounted? do
578
- true
579
- end
580
-
581
- redefine_method :_generate_prefix do |options|
582
- prefix_options = options.slice(*_route.segment_keys)
583
- # we must actually delete prefix segment keys to avoid passing them to next url_for
584
- _route.segment_keys.each { |k| options.delete(k) }
585
- _routes.url_helpers.send("#{name}_path", prefix_options)
628
+ app.routes.extend Module.new {
629
+ def optimize_routes_generation?; false; end
630
+ define_method :find_script_name do |options|
631
+ if options.key? :script_name
632
+ super(options)
633
+ else
634
+ prefix_options = options.slice(*_route.segment_keys)
635
+ # we must actually delete prefix segment keys to avoid passing them to next url_for
636
+ _route.segment_keys.each { |k| options.delete(k) }
637
+ _routes.url_helpers.send("#{name}_path", prefix_options)
638
+ end
586
639
  end
587
- end
640
+ }
588
641
  end
589
642
  end
590
643
 
@@ -671,7 +724,7 @@ module ActionDispatch
671
724
  # resources :posts, module: "admin"
672
725
  #
673
726
  # If you want to route /admin/posts to +PostsController+
674
- # (without the Admin:: module prefix), you could use
727
+ # (without the <tt>Admin::</tt> module prefix), you could use
675
728
  #
676
729
  # scope "/admin" do
677
730
  # resources :posts
@@ -725,7 +778,7 @@ module ActionDispatch
725
778
  # end
726
779
  def scope(*args)
727
780
  options = args.extract_options!.dup
728
- recover = {}
781
+ scope = {}
729
782
 
730
783
  options[:path] = args.flatten.join('/') if args.any?
731
784
  options[:constraints] ||= {}
@@ -745,7 +798,7 @@ module ActionDispatch
745
798
  block, options[:constraints] = options[:constraints], {}
746
799
  end
747
800
 
748
- SCOPE_OPTIONS.each do |option|
801
+ @scope.options.each do |option|
749
802
  if option == :blocks
750
803
  value = block
751
804
  elsif option == :options
@@ -755,15 +808,15 @@ module ActionDispatch
755
808
  end
756
809
 
757
810
  if value
758
- recover[option] = @scope[option]
759
- @scope[option] = send("merge_#{option}_scope", @scope[option], value)
811
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
760
812
  end
761
813
  end
762
814
 
815
+ @scope = @scope.new scope
763
816
  yield
764
817
  self
765
818
  ensure
766
- @scope.merge!(recover)
819
+ @scope = @scope.parent
767
820
  end
768
821
 
769
822
  # Scopes routes to a specific controller
@@ -1001,8 +1054,6 @@ module ActionDispatch
1001
1054
  VALID_ON_OPTIONS = [:new, :collection, :member]
1002
1055
  RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
1003
1056
  CANONICAL_ACTIONS = %w(index create new show update destroy)
1004
- RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1005
- RESOURCE_SCOPES = [:resource, :resources]
1006
1057
 
1007
1058
  class Resource #:nodoc:
1008
1059
  attr_reader :controller, :path, :options, :param
@@ -1422,7 +1473,20 @@ module ActionDispatch
1422
1473
  if rest.empty? && Hash === path
1423
1474
  options = path
1424
1475
  path, to = options.find { |name, _value| name.is_a?(String) }
1425
- options[:to] = to
1476
+
1477
+ case to
1478
+ when Symbol
1479
+ options[:action] = to
1480
+ when String
1481
+ if to =~ /#/
1482
+ options[:to] = to
1483
+ else
1484
+ options[:controller] = to
1485
+ end
1486
+ else
1487
+ options[:to] = to
1488
+ end
1489
+
1426
1490
  options.delete(path)
1427
1491
  paths = [path]
1428
1492
  else
@@ -1456,14 +1520,14 @@ module ActionDispatch
1456
1520
  end
1457
1521
 
1458
1522
  def using_match_shorthand?(path, options)
1459
- path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
1523
+ path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
1460
1524
  end
1461
1525
 
1462
1526
  def decomposed_match(path, options) # :nodoc:
1463
1527
  if on = options.delete(:on)
1464
1528
  send(on) { decomposed_match(path, options) }
1465
1529
  else
1466
- case @scope[:scope_level]
1530
+ case @scope.scope_level
1467
1531
  when :resources
1468
1532
  nested { decomposed_match(path, options) }
1469
1533
  when :resource
@@ -1476,6 +1540,8 @@ module ActionDispatch
1476
1540
 
1477
1541
  def add_route(action, options) # :nodoc:
1478
1542
  path = path_for_action(action, options.delete(:path))
1543
+ raise ArgumentError, "path is required" if path.blank?
1544
+
1479
1545
  action = action.to_s.dup
1480
1546
 
1481
1547
  if action =~ /^[\w\-\/]+$/
@@ -1484,13 +1550,13 @@ module ActionDispatch
1484
1550
  action = nil
1485
1551
  end
1486
1552
 
1487
- if !options.fetch(:as, true)
1488
- options.delete(:as)
1489
- else
1490
- options[:as] = name_for_action(options[:as], action)
1491
- end
1553
+ as = if !options.fetch(:as, true) # if it's set to nil or false
1554
+ options.delete(:as)
1555
+ else
1556
+ name_for_action(options.delete(:as), action)
1557
+ end
1492
1558
 
1493
- mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1559
+ mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
1494
1560
  app, conditions, requirements, defaults, as, anchor = mapping.to_route
1495
1561
  @set.add_route(app, conditions, requirements, defaults, as, anchor)
1496
1562
  end
@@ -1504,7 +1570,7 @@ module ActionDispatch
1504
1570
  raise ArgumentError, "must be called with a path and/or options"
1505
1571
  end
1506
1572
 
1507
- if @scope[:scope_level] == :resources
1573
+ if @scope.resources?
1508
1574
  with_scope_level(:root) do
1509
1575
  scope(parent_resource.path) do
1510
1576
  super(options)
@@ -1571,40 +1637,39 @@ module ActionDispatch
1571
1637
  end
1572
1638
 
1573
1639
  def resource_scope? #:nodoc:
1574
- RESOURCE_SCOPES.include? @scope[:scope_level]
1640
+ @scope.resource_scope?
1575
1641
  end
1576
1642
 
1577
1643
  def resource_method_scope? #:nodoc:
1578
- RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1644
+ @scope.resource_method_scope?
1579
1645
  end
1580
1646
 
1581
1647
  def nested_scope? #:nodoc:
1582
- @scope[:scope_level] == :nested
1648
+ @scope.nested?
1583
1649
  end
1584
1650
 
1585
1651
  def with_exclusive_scope
1586
1652
  begin
1587
- old_name_prefix, old_path = @scope[:as], @scope[:path]
1588
- @scope[:as], @scope[:path] = nil, nil
1653
+ @scope = @scope.new(:as => nil, :path => nil)
1589
1654
 
1590
1655
  with_scope_level(:exclusive) do
1591
1656
  yield
1592
1657
  end
1593
1658
  ensure
1594
- @scope[:as], @scope[:path] = old_name_prefix, old_path
1659
+ @scope = @scope.parent
1595
1660
  end
1596
1661
  end
1597
1662
 
1598
1663
  def with_scope_level(kind)
1599
- old, @scope[:scope_level] = @scope[:scope_level], kind
1664
+ @scope = @scope.new_level(kind)
1600
1665
  yield
1601
1666
  ensure
1602
- @scope[:scope_level] = old
1667
+ @scope = @scope.parent
1603
1668
  end
1604
1669
 
1605
1670
  def resource_scope(kind, resource) #:nodoc:
1606
1671
  resource.shallow = @scope[:shallow]
1607
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1672
+ @scope = @scope.new(:scope_level_resource => resource)
1608
1673
  @nesting.push(resource)
1609
1674
 
1610
1675
  with_scope_level(kind) do
@@ -1612,7 +1677,7 @@ module ActionDispatch
1612
1677
  end
1613
1678
  ensure
1614
1679
  @nesting.pop
1615
- @scope[:scope_level_resource] = old_resource
1680
+ @scope = @scope.parent
1616
1681
  end
1617
1682
 
1618
1683
  def nested_options #:nodoc:
@@ -1640,21 +1705,22 @@ module ActionDispatch
1640
1705
  @scope[:constraints][parent_resource.param]
1641
1706
  end
1642
1707
 
1643
- def canonical_action?(action, flag) #:nodoc:
1644
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1708
+ def canonical_action?(action) #:nodoc:
1709
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1645
1710
  end
1646
1711
 
1647
1712
  def shallow_scope(path, options = {}) #:nodoc:
1648
- old_name_prefix, old_path = @scope[:as], @scope[:path]
1649
- @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path]
1713
+ scope = { :as => @scope[:shallow_prefix],
1714
+ :path => @scope[:shallow_path] }
1715
+ @scope = @scope.new scope
1650
1716
 
1651
1717
  scope(path, options) { yield }
1652
1718
  ensure
1653
- @scope[:as], @scope[:path] = old_name_prefix, old_path
1719
+ @scope = @scope.parent
1654
1720
  end
1655
1721
 
1656
1722
  def path_for_action(action, path) #:nodoc:
1657
- if canonical_action?(action, path.blank?)
1723
+ if path.blank? && canonical_action?(action)
1658
1724
  @scope[:path].to_s
1659
1725
  else
1660
1726
  "#{@scope[:path]}/#{action_path(action, path)}"
@@ -1669,15 +1735,17 @@ module ActionDispatch
1669
1735
  def prefix_name_for_action(as, action) #:nodoc:
1670
1736
  if as
1671
1737
  prefix = as
1672
- elsif !canonical_action?(action, @scope[:scope_level])
1738
+ elsif !canonical_action?(action)
1673
1739
  prefix = action
1674
1740
  end
1675
- prefix.to_s.tr('-', '_') if prefix
1741
+
1742
+ if prefix && prefix != '/' && !prefix.empty?
1743
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
1744
+ end
1676
1745
  end
1677
1746
 
1678
1747
  def name_for_action(as, action) #:nodoc:
1679
1748
  prefix = prefix_name_for_action(as, action)
1680
- prefix = Mapper.normalize_name(prefix) if prefix
1681
1749
  name_prefix = @scope[:as]
1682
1750
 
1683
1751
  if parent_resource
@@ -1687,27 +1755,14 @@ module ActionDispatch
1687
1755
  member_name = parent_resource.member_name
1688
1756
  end
1689
1757
 
1690
- name = case @scope[:scope_level]
1691
- when :nested
1692
- [name_prefix, prefix]
1693
- when :collection
1694
- [prefix, name_prefix, collection_name]
1695
- when :new
1696
- [prefix, :new, name_prefix, member_name]
1697
- when :member
1698
- [prefix, name_prefix, member_name]
1699
- when :root
1700
- [name_prefix, collection_name, prefix]
1701
- else
1702
- [name_prefix, member_name, prefix]
1703
- end
1758
+ name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1704
1759
 
1705
- if candidate = name.select(&:present?).join("_").presence
1760
+ if candidate = name.compact.join("_").presence
1706
1761
  # If a name was not explicitly given, we check if it is valid
1707
1762
  # and return nil in case it isn't. Otherwise, we pass the invalid name
1708
1763
  # forward so the underlying router engine treats it and raises an exception.
1709
1764
  if as.nil?
1710
- candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
1765
+ candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
1711
1766
  else
1712
1767
  candidate
1713
1768
  end
@@ -1832,9 +1887,83 @@ module ActionDispatch
1832
1887
  end
1833
1888
  end
1834
1889
 
1890
+ class Scope # :nodoc:
1891
+ OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
1892
+ :controller, :action, :path_names, :constraints,
1893
+ :shallow, :blocks, :defaults, :options]
1894
+
1895
+ RESOURCE_SCOPES = [:resource, :resources]
1896
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1897
+
1898
+ attr_reader :parent, :scope_level
1899
+
1900
+ def initialize(hash, parent = {}, scope_level = nil)
1901
+ @hash = hash
1902
+ @parent = parent
1903
+ @scope_level = scope_level
1904
+ end
1905
+
1906
+ def nested?
1907
+ scope_level == :nested
1908
+ end
1909
+
1910
+ def resources?
1911
+ scope_level == :resources
1912
+ end
1913
+
1914
+ def resource_method_scope?
1915
+ RESOURCE_METHOD_SCOPES.include? scope_level
1916
+ end
1917
+
1918
+ def action_name(name_prefix, prefix, collection_name, member_name)
1919
+ case scope_level
1920
+ when :nested
1921
+ [name_prefix, prefix]
1922
+ when :collection
1923
+ [prefix, name_prefix, collection_name]
1924
+ when :new
1925
+ [prefix, :new, name_prefix, member_name]
1926
+ when :member
1927
+ [prefix, name_prefix, member_name]
1928
+ when :root
1929
+ [name_prefix, collection_name, prefix]
1930
+ else
1931
+ [name_prefix, member_name, prefix]
1932
+ end
1933
+ end
1934
+
1935
+ def resource_scope?
1936
+ RESOURCE_SCOPES.include? scope_level
1937
+ end
1938
+
1939
+ def options
1940
+ OPTIONS
1941
+ end
1942
+
1943
+ def new(hash)
1944
+ self.class.new hash, self, scope_level
1945
+ end
1946
+
1947
+ def new_level(level)
1948
+ self.class.new(self, self, level)
1949
+ end
1950
+
1951
+ def fetch(key, &block)
1952
+ @hash.fetch(key, &block)
1953
+ end
1954
+
1955
+ def [](key)
1956
+ @hash.fetch(key) { @parent[key] }
1957
+ end
1958
+
1959
+ def []=(k,v)
1960
+ @hash[k] = v
1961
+ end
1962
+ end
1963
+
1835
1964
  def initialize(set) #:nodoc:
1836
1965
  @set = set
1837
- @scope = { :path_names => @set.resources_path_names }
1966
+ @scope = Scope.new({ :path_names => @set.resources_path_names })
1838
1967
  @concerns = {}
1839
1968
  @nesting = []
1840
1969
  end