actionpack 4.1.16 → 4.2.0.beta1

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