actionpack 4.2.11.1 → 5.2.4.3

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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +287 -488
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +45 -49
  7. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +47 -31
  10. data/lib/abstract_controller/collector.rb +8 -11
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +25 -25
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  15. data/lib/abstract_controller/rendering.rb +42 -41
  16. data/lib/abstract_controller/translation.rb +10 -7
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +12 -5
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/base.rb +27 -19
  22. data/lib/action_controller/caching.rb +14 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +10 -15
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +118 -44
  27. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  28. data/lib/action_controller/metal/cookies.rb +3 -3
  29. data/lib/action_controller/metal/data_streaming.rb +27 -46
  30. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  31. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  32. data/lib/action_controller/metal/exceptions.rb +8 -14
  33. data/lib/action_controller/metal/flash.rb +4 -3
  34. data/lib/action_controller/metal/force_ssl.rb +23 -21
  35. data/lib/action_controller/metal/head.rb +21 -19
  36. data/lib/action_controller/metal/helpers.rb +24 -14
  37. data/lib/action_controller/metal/http_authentication.rb +64 -57
  38. data/lib/action_controller/metal/implicit_render.rb +62 -8
  39. data/lib/action_controller/metal/instrumentation.rb +19 -21
  40. data/lib/action_controller/metal/live.rb +90 -106
  41. data/lib/action_controller/metal/mime_responds.rb +33 -46
  42. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  43. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  44. data/lib/action_controller/metal/redirecting.rb +49 -28
  45. data/lib/action_controller/metal/renderers.rb +87 -44
  46. data/lib/action_controller/metal/rendering.rb +72 -50
  47. data/lib/action_controller/metal/request_forgery_protection.rb +229 -93
  48. data/lib/action_controller/metal/rescue.rb +9 -16
  49. data/lib/action_controller/metal/streaming.rb +12 -10
  50. data/lib/action_controller/metal/strong_parameters.rb +583 -164
  51. data/lib/action_controller/metal/testing.rb +2 -17
  52. data/lib/action_controller/metal/url_for.rb +19 -10
  53. data/lib/action_controller/metal.rb +98 -83
  54. data/lib/action_controller/railtie.rb +28 -10
  55. data/lib/action_controller/railties/helpers.rb +2 -0
  56. data/lib/action_controller/renderer.rb +117 -0
  57. data/lib/action_controller/template_assertions.rb +11 -0
  58. data/lib/action_controller/test_case.rb +280 -411
  59. data/lib/action_controller.rb +29 -21
  60. data/lib/action_dispatch/http/cache.rb +93 -47
  61. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  62. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  63. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  64. data/lib/action_dispatch/http/headers.rb +55 -22
  65. data/lib/action_dispatch/http/mime_negotiation.rb +56 -41
  66. data/lib/action_dispatch/http/mime_type.rb +134 -121
  67. data/lib/action_dispatch/http/mime_types.rb +20 -6
  68. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  69. data/lib/action_dispatch/http/parameters.rb +98 -39
  70. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  71. data/lib/action_dispatch/http/request.rb +200 -118
  72. data/lib/action_dispatch/http/response.rb +225 -110
  73. data/lib/action_dispatch/http/upload.rb +12 -6
  74. data/lib/action_dispatch/http/url.rb +110 -28
  75. data/lib/action_dispatch/journey/formatter.rb +55 -32
  76. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  77. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  78. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  79. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  80. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  81. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  82. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  83. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  84. data/lib/action_dispatch/journey/parser.rb +23 -22
  85. data/lib/action_dispatch/journey/parser.y +3 -2
  86. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  87. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  88. data/lib/action_dispatch/journey/route.rb +106 -28
  89. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  90. data/lib/action_dispatch/journey/router.rb +35 -23
  91. data/lib/action_dispatch/journey/routes.rb +18 -16
  92. data/lib/action_dispatch/journey/scanner.rb +18 -15
  93. data/lib/action_dispatch/journey/visitors.rb +99 -52
  94. data/lib/action_dispatch/journey.rb +7 -5
  95. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  96. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  97. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  98. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  99. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  100. data/lib/action_dispatch/middleware/executor.rb +21 -0
  101. data/lib/action_dispatch/middleware/flash.rb +78 -54
  102. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  103. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  104. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  105. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  106. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  107. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  108. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  109. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  110. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  111. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  112. data/lib/action_dispatch/middleware/stack.rb +31 -44
  113. data/lib/action_dispatch/middleware/static.rb +57 -50
  114. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  115. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  116. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  123. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  124. data/lib/action_dispatch/railtie.rb +19 -11
  125. data/lib/action_dispatch/request/session.rb +106 -59
  126. data/lib/action_dispatch/request/utils.rb +67 -24
  127. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  128. data/lib/action_dispatch/routing/inspector.rb +58 -67
  129. data/lib/action_dispatch/routing/mapper.rb +733 -447
  130. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  131. data/lib/action_dispatch/routing/redirection.rb +36 -26
  132. data/lib/action_dispatch/routing/route_set.rb +321 -291
  133. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  134. data/lib/action_dispatch/routing/url_for.rb +65 -25
  135. data/lib/action_dispatch/routing.rb +17 -18
  136. data/lib/action_dispatch/system_test_case.rb +147 -0
  137. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  138. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  139. data/lib/action_dispatch/system_testing/server.rb +31 -0
  140. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  143. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  144. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  145. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  146. data/lib/action_dispatch/testing/assertions.rb +6 -4
  147. data/lib/action_dispatch/testing/integration.rb +347 -209
  148. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  149. data/lib/action_dispatch/testing/test_process.rb +28 -22
  150. data/lib/action_dispatch/testing/test_request.rb +27 -34
  151. data/lib/action_dispatch/testing/test_response.rb +35 -7
  152. data/lib/action_dispatch.rb +27 -19
  153. data/lib/action_pack/gem_version.rb +5 -3
  154. data/lib/action_pack/version.rb +3 -1
  155. data/lib/action_pack.rb +4 -2
  156. metadata +56 -38
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,39 +1,39 @@
1
- require 'active_support/core_ext/hash/except'
2
- require 'active_support/core_ext/hash/reverse_merge'
3
- require 'active_support/core_ext/hash/slice'
4
- require 'active_support/core_ext/enumerable'
5
- require 'active_support/core_ext/array/extract_options'
6
- require 'active_support/core_ext/module/remove_method'
7
- require 'active_support/core_ext/string/filters'
8
- require 'active_support/inflector'
9
- require 'action_dispatch/routing/redirection'
10
- require 'action_dispatch/routing/endpoint'
11
- require 'active_support/deprecation'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/slice"
4
+ require "active_support/core_ext/enumerable"
5
+ require "active_support/core_ext/array/extract_options"
6
+ require "active_support/core_ext/regexp"
7
+ require "action_dispatch/routing/redirection"
8
+ require "action_dispatch/routing/endpoint"
12
9
 
13
10
  module ActionDispatch
14
11
  module Routing
15
12
  class Mapper
16
13
  URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
17
14
 
18
- class Constraints < Endpoint #:nodoc:
15
+ class Constraints < Routing::Endpoint #:nodoc:
19
16
  attr_reader :app, :constraints
20
17
 
21
- def initialize(app, constraints, dispatcher_p)
22
- # Unwrap Constraints objects. I don't actually think it's possible
18
+ SERVE = ->(app, req) { app.serve req }
19
+ CALL = ->(app, req) { app.call req.env }
20
+
21
+ def initialize(app, constraints, strategy)
22
+ # Unwrap Constraints objects. I don't actually think it's possible
23
23
  # to pass a Constraints object to this constructor, but there were
24
- # multiple places that kept testing children of this object. I
24
+ # multiple places that kept testing children of this object. I
25
25
  # *think* they were just being defensive, but I have no idea.
26
26
  if app.is_a?(self.class)
27
27
  constraints += app.constraints
28
28
  app = app.app
29
29
  end
30
30
 
31
- @dispatcher = dispatcher_p
31
+ @strategy = strategy
32
32
 
33
33
  @app, @constraints, = app, constraints
34
34
  end
35
35
 
36
- def dispatcher?; @dispatcher; end
36
+ def dispatcher?; @strategy == SERVE; end
37
37
 
38
38
  def matches?(req)
39
39
  @constraints.all? do |constraint|
@@ -43,13 +43,9 @@ module ActionDispatch
43
43
  end
44
44
 
45
45
  def serve(req)
46
- return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
46
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
47
47
 
48
- if dispatcher?
49
- @app.serve req
50
- else
51
- @app.call req.env
52
- end
48
+ @strategy.call @app, req
53
49
  end
54
50
 
55
51
  private
@@ -62,101 +58,180 @@ module ActionDispatch
62
58
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
63
59
  OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
64
60
 
65
- attr_reader :requirements, :conditions, :defaults
66
- attr_reader :to, :default_controller, :default_action, :as, :anchor
61
+ attr_reader :requirements, :defaults
62
+ attr_reader :to, :default_controller, :default_action
63
+ attr_reader :required_defaults, :ast
67
64
 
68
- def self.build(scope, set, path, as, options)
65
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
69
66
  options = scope[:options].merge(options) if scope[:options]
70
67
 
71
- options.delete :only
72
- options.delete :except
73
- options.delete :shallow_path
74
- options.delete :shallow_prefix
75
- options.delete :shallow
68
+ defaults = (scope[:defaults] || {}).dup
69
+ scope_constraints = scope[:constraints] || {}
70
+
71
+ new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
72
+ end
73
+
74
+ def self.check_via(via)
75
+ if via.empty?
76
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
77
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
78
+ "If you want to expose your action to GET, use `get` in the router:\n" \
79
+ " Instead of: match \"controller#action\"\n" \
80
+ " Do: get \"controller#action\""
81
+ raise ArgumentError, msg
82
+ end
83
+ via
84
+ end
85
+
86
+ def self.normalize_path(path, format)
87
+ path = Mapper.normalize_path(path)
76
88
 
77
- defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
89
+ if format == true
90
+ "#{path}.:format"
91
+ elsif optional_format?(path, format)
92
+ "#{path}(.:format)"
93
+ else
94
+ path
95
+ end
96
+ end
78
97
 
79
- new scope, set, path, defaults, as, options
98
+ def self.optional_format?(path, format)
99
+ format != false && path !~ OPTIONAL_FORMAT_REGEX
80
100
  end
81
101
 
82
- def initialize(scope, set, path, defaults, as, options)
83
- @requirements, @conditions = {}, {}
102
+ def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
84
103
  @defaults = defaults
85
104
  @set = set
86
105
 
87
- @to = options.delete :to
88
- @default_controller = options.delete(:controller) || scope[:controller]
89
- @default_action = options.delete(:action) || scope[:action]
90
- @as = as
91
- @anchor = options.delete :anchor
106
+ @to = to
107
+ @default_controller = controller
108
+ @default_action = default_action
109
+ @ast = ast
110
+ @anchor = anchor
111
+ @via = via
112
+ @internal = options.delete(:internal)
92
113
 
93
- formatted = options.delete :format
94
- via = Array(options.delete(:via) { [] })
95
- options_constraints = options.delete :constraints
114
+ path_params = ast.find_all(&:symbol?).map(&:to_sym)
96
115
 
97
- path = normalize_path! path, formatted
98
- ast = path_ast path
99
- path_params = path_params ast
116
+ options = add_wildcard_options(options, formatted, ast)
100
117
 
101
- options = normalize_options!(options, formatted, path_params, ast, scope[:module])
118
+ options = normalize_options!(options, path_params, modyoule)
102
119
 
120
+ split_options = constraints(options, path_params)
103
121
 
104
- split_constraints(path_params, scope[:constraints]) if scope[:constraints]
105
- constraints = constraints(options, path_params)
122
+ constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
123
+
124
+ if options_constraints.is_a?(Hash)
125
+ @defaults = Hash[options_constraints.find_all { |key, default|
126
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
127
+ }].merge @defaults
128
+ @blocks = blocks
129
+ constraints.merge! options_constraints
130
+ else
131
+ @blocks = blocks(options_constraints)
132
+ end
106
133
 
107
- split_constraints path_params, constraints
134
+ requirements, conditions = split_constraints path_params, constraints
135
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
108
136
 
109
- @blocks = blocks(options_constraints, scope[:blocks])
137
+ formats = normalize_format(formatted)
110
138
 
111
- if options_constraints.is_a?(Hash)
112
- split_constraints path_params, options_constraints
113
- options_constraints.each do |key, default|
114
- if URL_OPTIONS.include?(key) && (String === default || Integer === default)
115
- @defaults[key] ||= default
116
- end
117
- end
139
+ @requirements = formats[:requirements].merge Hash[requirements]
140
+ @conditions = Hash[conditions]
141
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
142
+
143
+ if path_params.include?(:action) && !@requirements.key?(:action)
144
+ @defaults[:action] ||= "index"
118
145
  end
119
146
 
120
- normalize_format!(formatted)
147
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
148
+ end
121
149
 
122
- @conditions[:path_info] = path
123
- @conditions[:parsed_path_info] = ast
150
+ def make_route(name, precedence)
151
+ route = Journey::Route.new(name,
152
+ application,
153
+ path,
154
+ conditions,
155
+ required_defaults,
156
+ defaults,
157
+ request_method,
158
+ precedence,
159
+ @internal)
160
+
161
+ route
162
+ end
124
163
 
125
- add_request_method(via, @conditions)
126
- normalize_defaults!(options)
164
+ def application
165
+ app(@blocks)
127
166
  end
128
167
 
129
- def to_route
130
- [ app(@blocks), conditions, requirements, defaults, as, anchor ]
168
+ def path
169
+ build_path @ast, requirements, @anchor
131
170
  end
132
171
 
133
- private
172
+ def conditions
173
+ build_conditions @conditions, @set.request_class
174
+ end
134
175
 
135
- def normalize_path!(path, format)
136
- path = Mapper.normalize_path(path)
176
+ def build_conditions(current_conditions, request_class)
177
+ conditions = current_conditions.dup
137
178
 
138
- if format == true
139
- "#{path}.:format"
140
- elsif optional_format?(path, format)
141
- "#{path}(.:format)"
179
+ conditions.keep_if do |k, _|
180
+ request_class.public_method_defined?(k)
181
+ end
182
+ end
183
+ private :build_conditions
184
+
185
+ def request_method
186
+ @via.map { |x| Journey::Route.verb_matcher(x) }
187
+ end
188
+ private :request_method
189
+
190
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
191
+
192
+ def build_path(ast, requirements, anchor)
193
+ pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
194
+
195
+ # Find all the symbol nodes that are adjacent to literal nodes and alter
196
+ # the regexp so that Journey will partition them into custom routes.
197
+ ast.find_all { |node|
198
+ next unless node.cat?
199
+
200
+ if node.left.literal? && node.right.symbol?
201
+ symbol = node.right
202
+ elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
203
+ symbol = node.right.left
204
+ elsif node.left.symbol? && node.right.literal?
205
+ symbol = node.left
206
+ elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
207
+ symbol = node.left
142
208
  else
143
- path
209
+ next
144
210
  end
145
- end
146
211
 
147
- def optional_format?(path, format)
148
- format != false && path !~ OPTIONAL_FORMAT_REGEX
149
- end
212
+ if symbol
213
+ symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
214
+ end
215
+ }
216
+
217
+ pattern
218
+ end
219
+ private :build_path
150
220
 
151
- def normalize_options!(options, formatted, path_params, path_ast, modyoule)
221
+ private
222
+ def add_wildcard_options(options, formatted, path_ast)
152
223
  # Add a constraint for wildcard route to make it non-greedy and match the
153
- # optional format part of the route by default
224
+ # optional format part of the route by default.
154
225
  if formatted != false
155
- path_ast.grep(Journey::Nodes::Star) do |node|
156
- options[node.name.to_sym] ||= /.+?/
157
- end
226
+ path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
227
+ hash[node.name.to_sym] ||= /.+?/
228
+ }.merge options
229
+ else
230
+ options
158
231
  end
232
+ end
159
233
 
234
+ def normalize_options!(options, path_params, modyoule)
160
235
  if path_params.include?(:controller)
161
236
  raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
162
237
 
@@ -167,7 +242,7 @@ module ActionDispatch
167
242
  options[:controller] ||= /.+?/
168
243
  end
169
244
 
170
- if to.respond_to? :call
245
+ if to.respond_to?(:action) || to.respond_to?(:call)
171
246
  options
172
247
  else
173
248
  to_endpoint = split_to to
@@ -181,81 +256,59 @@ module ActionDispatch
181
256
  end
182
257
 
183
258
  def split_constraints(path_params, constraints)
184
- constraints.each_pair do |key, requirement|
185
- if path_params.include?(key) || key == :controller
186
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
187
- @requirements[key] = requirement
188
- else
189
- @conditions[key] = requirement
190
- end
191
- end
192
- end
193
-
194
- def normalize_format!(formatted)
195
- if formatted == true
196
- @requirements[:format] ||= /.+/
197
- elsif Regexp === formatted
198
- @requirements[:format] = formatted
199
- @defaults[:format] = nil
200
- elsif String === formatted
201
- @requirements[:format] = Regexp.compile(formatted)
202
- @defaults[:format] = formatted
259
+ constraints.partition do |key, requirement|
260
+ path_params.include?(key) || key == :controller
203
261
  end
204
262
  end
205
263
 
206
- def verify_regexp_requirement(requirement)
207
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
208
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
209
- end
210
-
211
- if requirement.multiline?
212
- raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
264
+ def normalize_format(formatted)
265
+ case formatted
266
+ when true
267
+ { requirements: { format: /.+/ },
268
+ defaults: {} }
269
+ when Regexp
270
+ { requirements: { format: formatted },
271
+ defaults: { format: nil } }
272
+ when String
273
+ { requirements: { format: Regexp.compile(formatted) },
274
+ defaults: { format: formatted } }
275
+ else
276
+ { requirements: {}, defaults: {} }
213
277
  end
214
278
  end
215
279
 
216
- def normalize_defaults!(options)
217
- options.each_pair do |key, default|
218
- unless Regexp === default
219
- @defaults[key] = default
280
+ def verify_regexp_requirements(requirements)
281
+ requirements.each do |requirement|
282
+ if requirement.source =~ ANCHOR_CHARACTERS_REGEX
283
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
220
284
  end
221
- end
222
- end
223
285
 
224
- def verify_callable_constraint(callable_constraint)
225
- unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
226
- raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
286
+ if requirement.multiline?
287
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
288
+ end
227
289
  end
228
290
  end
229
291
 
230
- def add_request_method(via, conditions)
231
- return if via == [:all]
232
-
233
- if via.empty?
234
- msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
235
- "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
236
- "If you want to expose your action to GET, use `get` in the router:\n" \
237
- " Instead of: match \"controller#action\"\n" \
238
- " Do: get \"controller#action\""
239
- raise ArgumentError, msg
240
- end
241
-
242
- conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
292
+ def normalize_defaults(options)
293
+ Hash[options.reject { |_, default| Regexp === default }]
243
294
  end
244
295
 
245
296
  def app(blocks)
246
- if to.respond_to?(:call)
247
- Constraints.new(to, blocks, false)
297
+ if to.respond_to?(:action)
298
+ Routing::RouteSet::StaticDispatcher.new to
299
+ elsif to.respond_to?(:call)
300
+ Constraints.new(to, blocks, Constraints::CALL)
248
301
  elsif blocks.any?
249
- Constraints.new(dispatcher(defaults), blocks, true)
302
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
250
303
  else
251
- dispatcher(defaults)
304
+ dispatcher(defaults.key?(:controller))
252
305
  end
253
306
  end
254
307
 
255
308
  def check_controller_and_action(path_params, controller, action)
256
309
  hash = check_part(:controller, controller, path_params, {}) do |part|
257
310
  translate_controller(part) {
258
- message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
311
+ message = "'#{part}' is not a supported controller name. This can lead to potential routing problems.".dup
259
312
  message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
260
313
 
261
314
  raise ArgumentError, message
@@ -280,22 +333,8 @@ module ActionDispatch
280
333
  end
281
334
 
282
335
  def split_to(to)
283
- case to
284
- when Symbol
285
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
286
- Defining a route where `to` is a symbol is deprecated.
287
- Please change `to: :#{to}` to `action: :#{to}`.
288
- MSG
289
-
290
- [nil, to.to_s]
291
- when /#/ then to.split('#')
292
- when String
293
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
294
- Defining a route where `to` is a controller without an action is deprecated.
295
- Please change `to: '#{to}'` to `controller: '#{to}'`.
296
- MSG
297
-
298
- [to, nil]
336
+ if to =~ /#/
337
+ to.split("#")
299
338
  else
300
339
  []
301
340
  end
@@ -320,40 +359,29 @@ module ActionDispatch
320
359
  yield
321
360
  end
322
361
 
323
- def blocks(options_constraints, scope_blocks)
324
- if options_constraints && !options_constraints.is_a?(Hash)
325
- verify_callable_constraint(options_constraints)
326
- [options_constraints]
327
- else
328
- scope_blocks || []
362
+ def blocks(callable_constraint)
363
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
364
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
329
365
  end
366
+ [callable_constraint]
330
367
  end
331
368
 
332
369
  def constraints(options, path_params)
333
- constraints = {}
334
- required_defaults = []
335
- options.each_pair do |key, option|
370
+ options.group_by do |key, option|
336
371
  if Regexp === option
337
- constraints[key] = option
372
+ :constraints
338
373
  else
339
- required_defaults << key unless path_params.include?(key)
374
+ if path_params.include?(key)
375
+ :path_params
376
+ else
377
+ :required_defaults
378
+ end
340
379
  end
341
380
  end
342
- @conditions[:required_defaults] = required_defaults
343
- constraints
344
381
  end
345
382
 
346
- def path_params(ast)
347
- ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
348
- end
349
-
350
- def path_ast(path)
351
- parser = Journey::Parser.new
352
- parser.parse path
353
- end
354
-
355
- def dispatcher(defaults)
356
- @set.dispatcher defaults
383
+ def dispatcher(raise_on_name_error)
384
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
357
385
  end
358
386
  end
359
387
 
@@ -362,7 +390,7 @@ module ActionDispatch
362
390
  # for root cases, where the latter is the correct one.
363
391
  def self.normalize_path(path)
364
392
  path = Journey::Router::Utils.normalize_path(path)
365
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
393
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$}
366
394
  path
367
395
  end
368
396
 
@@ -371,24 +399,7 @@ module ActionDispatch
371
399
  end
372
400
 
373
401
  module Base
374
- # You can specify what Rails should route "/" to with the root method:
375
- #
376
- # root to: 'pages#main'
377
- #
378
- # For options, see +match+, as +root+ uses it internally.
379
- #
380
- # You can also pass a string which will expand
381
- #
382
- # root 'pages#main'
383
- #
384
- # You should put the root route at the top of <tt>config/routes.rb</tt>,
385
- # because this means it will be matched first. As this is the most popular route
386
- # of most Rails applications, this is beneficial.
387
- def root(options = {})
388
- match '/', { :as => :root, :via => :get }.merge!(options)
389
- end
390
-
391
- # Matches a url pattern to one or more routes.
402
+ # Matches a URL pattern to one or more routes.
392
403
  #
393
404
  # You should not use the +match+ method in your router
394
405
  # without specifying an HTTP method.
@@ -398,7 +409,7 @@ module ActionDispatch
398
409
  # # sets :controller, :action and :id in params
399
410
  # match ':controller/:action/:id', via: [:get, :post]
400
411
  #
401
- # Note that +:controller+, +:action+ and +:id+ are interpreted as url
412
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as URL
402
413
  # query parameters and thus available through +params+ in an action.
403
414
  #
404
415
  # If you want to expose your action to GET, use +get+ in the router:
@@ -435,7 +446,7 @@ module ActionDispatch
435
446
  # A pattern can also point to a +Rack+ endpoint i.e. anything that
436
447
  # responds to +call+:
437
448
  #
438
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
449
+ # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
439
450
  # match 'photos/:id', to: PhotoRackApp, via: :get
440
451
  # # Yes, controller actions are just rack endpoints
441
452
  # match 'photos/:id', to: PhotosController.action(:show), via: :get
@@ -447,7 +458,7 @@ module ActionDispatch
447
458
  #
448
459
  # === Options
449
460
  #
450
- # Any options not seen here are passed on as params with the url.
461
+ # Any options not seen here are passed on as params with the URL.
451
462
  #
452
463
  # [:controller]
453
464
  # The route's controller.
@@ -460,6 +471,31 @@ module ActionDispatch
460
471
  # dynamic segment used to generate the routes).
461
472
  # You can access that segment from your controller using
462
473
  # <tt>params[<:param>]</tt>.
474
+ # In your router:
475
+ #
476
+ # resources :users, param: :name
477
+ #
478
+ # The +users+ resource here will have the following routes generated for it:
479
+ #
480
+ # GET /users(.:format)
481
+ # POST /users(.:format)
482
+ # GET /users/new(.:format)
483
+ # GET /users/:name/edit(.:format)
484
+ # GET /users/:name(.:format)
485
+ # PATCH/PUT /users/:name(.:format)
486
+ # DELETE /users/:name(.:format)
487
+ #
488
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
489
+ # model to construct a URL:
490
+ #
491
+ # class User < ActiveRecord::Base
492
+ # def to_param
493
+ # name
494
+ # end
495
+ # end
496
+ #
497
+ # user = User.find_by(name: 'Phusion')
498
+ # user_path(user) # => "/users/Phusion"
463
499
  #
464
500
  # [:path]
465
501
  # The path prefix for the routes.
@@ -487,7 +523,7 @@ module ActionDispatch
487
523
  # +call+ or a string representing a controller's action.
488
524
  #
489
525
  # match 'path', to: 'controller#action', via: :get
490
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
526
+ # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
491
527
  # match 'path', to: RackApp, via: :get
492
528
  #
493
529
  # [:on]
@@ -543,7 +579,7 @@ module ActionDispatch
543
579
  # [:format]
544
580
  # Allows you to specify the default value for optional +format+
545
581
  # segment or disable it by supplying +false+.
546
- def match(path, options=nil)
582
+ def match(path, options = nil)
547
583
  end
548
584
 
549
585
  # Mount a Rack-based application to be used within the application.
@@ -568,17 +604,20 @@ module ActionDispatch
568
604
  def mount(app, options = nil)
569
605
  if options
570
606
  path = options.delete(:at)
571
- else
572
- unless Hash === app
573
- raise ArgumentError, "must be called with mount point"
574
- end
575
-
607
+ elsif Hash === app
576
608
  options = app
577
609
  app, path = options.find { |k, _| k.respond_to?(:call) }
578
610
  options.delete(app) if app
579
611
  end
580
612
 
581
- raise "A rack application must be specified" unless path
613
+ raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
614
+ raise ArgumentError, <<-MSG.strip_heredoc unless path
615
+ Must be called with mount point
616
+
617
+ mount SomeRackApp, at: "some_route"
618
+ or
619
+ mount(SomeRackApp => "some_route")
620
+ MSG
582
621
 
583
622
  rails_app = rails_app? app
584
623
  options[:as] ||= app_name(app, rails_app)
@@ -586,7 +625,7 @@ module ActionDispatch
586
625
  target_as = name_for_action(options[:as], path)
587
626
  options[:via] ||= :all
588
627
 
589
- match(path, options.merge(:to => app, :anchor => false, :format => false))
628
+ match(path, options.merge(to: app, anchor: false, format: false))
590
629
 
591
630
  define_generate_prefix(app, target_as) if rails_app
592
631
  self
@@ -605,7 +644,7 @@ module ActionDispatch
605
644
 
606
645
  # Query if the following named route was already defined.
607
646
  def has_named_route?(name)
608
- @set.named_routes.routes[name.to_sym]
647
+ @set.named_routes.key? name
609
648
  end
610
649
 
611
650
  private
@@ -625,18 +664,31 @@ module ActionDispatch
625
664
  def define_generate_prefix(app, name)
626
665
  _route = @set.named_routes.get name
627
666
  _routes = @set
628
- app.routes.define_mounted_helper(name)
667
+ _url_helpers = @set.url_helpers
668
+
669
+ script_namer = ->(options) do
670
+ prefix_options = options.slice(*_route.segment_keys)
671
+ prefix_options[:relative_url_root] = "".freeze
672
+
673
+ if options[:_recall]
674
+ prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
675
+ end
676
+
677
+ # We must actually delete prefix segment keys to avoid passing them to next url_for.
678
+ _route.segment_keys.each { |k| options.delete(k) }
679
+ _url_helpers.send("#{name}_path", prefix_options)
680
+ end
681
+
682
+ app.routes.define_mounted_helper(name, script_namer)
683
+
629
684
  app.routes.extend Module.new {
630
685
  def optimize_routes_generation?; false; end
686
+
631
687
  define_method :find_script_name do |options|
632
688
  if options.key? :script_name
633
689
  super(options)
634
690
  else
635
- prefix_options = options.slice(*_route.segment_keys)
636
- prefix_options[:relative_url_root] = ''.freeze
637
- # we must actually delete prefix segment keys to avoid passing them to next url_for
638
- _route.segment_keys.each { |k| options.delete(k) }
639
- _routes.url_helpers.send("#{name}_path", prefix_options)
691
+ script_namer.call(options)
640
692
  end
641
693
  end
642
694
  }
@@ -782,7 +834,7 @@ module ActionDispatch
782
834
  options = args.extract_options!.dup
783
835
  scope = {}
784
836
 
785
- options[:path] = args.flatten.join('/') if args.any?
837
+ options[:path] = args.flatten.join("/") if args.any?
786
838
  options[:constraints] ||= {}
787
839
 
788
840
  unless nested_scope?
@@ -795,21 +847,30 @@ module ActionDispatch
795
847
  URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
796
848
  end
797
849
 
798
- (options[:defaults] ||= {}).reverse_merge!(defaults)
850
+ options[:defaults] = defaults.merge(options[:defaults] || {})
799
851
  else
800
852
  block, options[:constraints] = options[:constraints], {}
801
853
  end
802
854
 
855
+ if options.key?(:only) || options.key?(:except)
856
+ scope[:action_options] = { only: options.delete(:only),
857
+ except: options.delete(:except) }
858
+ end
859
+
860
+ if options.key? :anchor
861
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
862
+ end
863
+
803
864
  @scope.options.each do |option|
804
865
  if option == :blocks
805
866
  value = block
806
867
  elsif option == :options
807
868
  value = options
808
869
  else
809
- value = options.delete(option)
870
+ value = options.delete(option) { POISON }
810
871
  end
811
872
 
812
- if value
873
+ unless POISON == value
813
874
  scope[option] = send("merge_#{option}_scope", @scope[option], value)
814
875
  end
815
876
  end
@@ -821,14 +882,18 @@ module ActionDispatch
821
882
  @scope = @scope.parent
822
883
  end
823
884
 
885
+ POISON = Object.new # :nodoc:
886
+
824
887
  # Scopes routes to a specific controller
825
888
  #
826
889
  # controller "food" do
827
- # match "bacon", action: "bacon"
890
+ # match "bacon", action: :bacon, via: :get
828
891
  # end
829
- def controller(controller, options={})
830
- options[:controller] = controller
831
- scope(options) { yield }
892
+ def controller(controller)
893
+ @scope = @scope.new(controller: controller)
894
+ yield
895
+ ensure
896
+ @scope = @scope.parent
832
897
  end
833
898
 
834
899
  # Scopes routes to a specific namespace. For example:
@@ -874,13 +939,14 @@ module ActionDispatch
874
939
 
875
940
  defaults = {
876
941
  module: path,
877
- path: options.fetch(:path, path),
878
942
  as: options.fetch(:as, path),
879
943
  shallow_path: options.fetch(:path, path),
880
944
  shallow_prefix: options.fetch(:as, path)
881
945
  }
882
946
 
883
- scope(defaults.merge!(options)) { yield }
947
+ path_scope(options.delete(:path) { path }) do
948
+ scope(defaults.merge!(options)) { yield }
949
+ end
884
950
  end
885
951
 
886
952
  # === Parameter Restriction
@@ -917,7 +983,7 @@ module ActionDispatch
917
983
  #
918
984
  # Requests to routes can be constrained based on specific criteria:
919
985
  #
920
- # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
986
+ # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
921
987
  # resources :iphones
922
988
  # end
923
989
  #
@@ -939,7 +1005,7 @@ module ActionDispatch
939
1005
  # resources :iphones
940
1006
  # end
941
1007
  def constraints(constraints = {})
942
- scope(:constraints => constraints) { yield }
1008
+ scope(constraints: constraints) { yield }
943
1009
  end
944
1010
 
945
1011
  # Allows you to set default parameters for a route, such as this:
@@ -948,66 +1014,77 @@ module ActionDispatch
948
1014
  # end
949
1015
  # Using this, the +:id+ parameter here will default to 'home'.
950
1016
  def defaults(defaults = {})
951
- scope(:defaults => defaults) { yield }
1017
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
1018
+ yield
1019
+ ensure
1020
+ @scope = @scope.parent
952
1021
  end
953
1022
 
954
1023
  private
955
- def merge_path_scope(parent, child) #:nodoc:
1024
+ def merge_path_scope(parent, child)
956
1025
  Mapper.normalize_path("#{parent}/#{child}")
957
1026
  end
958
1027
 
959
- def merge_shallow_path_scope(parent, child) #:nodoc:
1028
+ def merge_shallow_path_scope(parent, child)
960
1029
  Mapper.normalize_path("#{parent}/#{child}")
961
1030
  end
962
1031
 
963
- def merge_as_scope(parent, child) #:nodoc:
1032
+ def merge_as_scope(parent, child)
964
1033
  parent ? "#{parent}_#{child}" : child
965
1034
  end
966
1035
 
967
- def merge_shallow_prefix_scope(parent, child) #:nodoc:
1036
+ def merge_shallow_prefix_scope(parent, child)
968
1037
  parent ? "#{parent}_#{child}" : child
969
1038
  end
970
1039
 
971
- def merge_module_scope(parent, child) #:nodoc:
1040
+ def merge_module_scope(parent, child)
972
1041
  parent ? "#{parent}/#{child}" : child
973
1042
  end
974
1043
 
975
- def merge_controller_scope(parent, child) #:nodoc:
1044
+ def merge_controller_scope(parent, child)
1045
+ child
1046
+ end
1047
+
1048
+ def merge_action_scope(parent, child)
976
1049
  child
977
1050
  end
978
1051
 
979
- def merge_action_scope(parent, child) #:nodoc:
1052
+ def merge_via_scope(parent, child)
980
1053
  child
981
1054
  end
982
1055
 
983
- def merge_path_names_scope(parent, child) #:nodoc:
1056
+ def merge_format_scope(parent, child)
1057
+ child
1058
+ end
1059
+
1060
+ def merge_path_names_scope(parent, child)
984
1061
  merge_options_scope(parent, child)
985
1062
  end
986
1063
 
987
- def merge_constraints_scope(parent, child) #:nodoc:
1064
+ def merge_constraints_scope(parent, child)
988
1065
  merge_options_scope(parent, child)
989
1066
  end
990
1067
 
991
- def merge_defaults_scope(parent, child) #:nodoc:
1068
+ def merge_defaults_scope(parent, child)
992
1069
  merge_options_scope(parent, child)
993
1070
  end
994
1071
 
995
- def merge_blocks_scope(parent, child) #:nodoc:
1072
+ def merge_blocks_scope(parent, child)
996
1073
  merged = parent ? parent.dup : []
997
1074
  merged << child if child
998
1075
  merged
999
1076
  end
1000
1077
 
1001
- def merge_options_scope(parent, child) #:nodoc:
1002
- (parent || {}).except(*override_keys(child)).merge!(child)
1078
+ def merge_options_scope(parent, child)
1079
+ (parent || {}).merge(child)
1003
1080
  end
1004
1081
 
1005
- def merge_shallow_scope(parent, child) #:nodoc:
1082
+ def merge_shallow_scope(parent, child)
1006
1083
  child ? true : false
1007
1084
  end
1008
1085
 
1009
- def override_keys(child) #:nodoc:
1010
- child.key?(:only) || child.key?(:except) ? [:only, :except] : []
1086
+ def merge_to_scope(parent, child)
1087
+ child
1011
1088
  end
1012
1089
  end
1013
1090
 
@@ -1058,27 +1135,34 @@ module ActionDispatch
1058
1135
  CANONICAL_ACTIONS = %w(index create new show update destroy)
1059
1136
 
1060
1137
  class Resource #:nodoc:
1061
- attr_reader :controller, :path, :options, :param
1138
+ attr_reader :controller, :path, :param
1062
1139
 
1063
- def initialize(entities, options = {})
1140
+ def initialize(entities, api_only, shallow, options = {})
1064
1141
  @name = entities.to_s
1065
1142
  @path = (options[:path] || @name).to_s
1066
1143
  @controller = (options[:controller] || @name).to_s
1067
1144
  @as = options[:as]
1068
1145
  @param = (options[:param] || :id).to_sym
1069
1146
  @options = options
1070
- @shallow = false
1147
+ @shallow = shallow
1148
+ @api_only = api_only
1149
+ @only = options.delete :only
1150
+ @except = options.delete :except
1071
1151
  end
1072
1152
 
1073
1153
  def default_actions
1074
- [:index, :create, :new, :show, :update, :destroy, :edit]
1154
+ if @api_only
1155
+ [:index, :create, :show, :update, :destroy]
1156
+ else
1157
+ [:index, :create, :new, :show, :update, :destroy, :edit]
1158
+ end
1075
1159
  end
1076
1160
 
1077
1161
  def actions
1078
- if only = @options[:only]
1079
- Array(only).map(&:to_sym)
1080
- elsif except = @options[:except]
1081
- default_actions - Array(except).map(&:to_sym)
1162
+ if @only
1163
+ Array(@only).map(&:to_sym)
1164
+ elsif @except
1165
+ default_actions - Array(@except).map(&:to_sym)
1082
1166
  else
1083
1167
  default_actions
1084
1168
  end
@@ -1105,7 +1189,7 @@ module ActionDispatch
1105
1189
  end
1106
1190
 
1107
1191
  def resource_scope
1108
- { :controller => controller }
1192
+ controller
1109
1193
  end
1110
1194
 
1111
1195
  alias :collection_scope :path
@@ -1128,17 +1212,15 @@ module ActionDispatch
1128
1212
  "#{path}/:#{nested_param}"
1129
1213
  end
1130
1214
 
1131
- def shallow=(value)
1132
- @shallow = value
1133
- end
1134
-
1135
1215
  def shallow?
1136
1216
  @shallow
1137
1217
  end
1218
+
1219
+ def singleton?; false; end
1138
1220
  end
1139
1221
 
1140
1222
  class SingletonResource < Resource #:nodoc:
1141
- def initialize(entities, options)
1223
+ def initialize(entities, api_only, shallow, options)
1142
1224
  super
1143
1225
  @as = nil
1144
1226
  @controller = (options[:controller] || plural).to_s
@@ -1146,7 +1228,11 @@ module ActionDispatch
1146
1228
  end
1147
1229
 
1148
1230
  def default_actions
1149
- [:show, :create, :update, :destroy, :new, :edit]
1231
+ if @api_only
1232
+ [:show, :create, :update, :destroy]
1233
+ else
1234
+ [:show, :create, :update, :destroy, :new, :edit]
1235
+ end
1150
1236
  end
1151
1237
 
1152
1238
  def plural
@@ -1162,6 +1248,8 @@ module ActionDispatch
1162
1248
 
1163
1249
  alias :member_scope :path
1164
1250
  alias :nested_scope :path
1251
+
1252
+ def singleton?; true; end
1165
1253
  end
1166
1254
 
1167
1255
  def resources_path_names(options)
@@ -1176,19 +1264,19 @@ module ActionDispatch
1176
1264
  #
1177
1265
  # resource :profile
1178
1266
  #
1179
- # creates six different routes in your application, all mapping to
1267
+ # This creates six different routes in your application, all mapping to
1180
1268
  # the +Profiles+ controller (note that the controller is named after
1181
1269
  # the plural):
1182
1270
  #
1183
1271
  # GET /profile/new
1184
- # POST /profile
1185
1272
  # GET /profile
1186
1273
  # GET /profile/edit
1187
1274
  # PATCH/PUT /profile
1188
1275
  # DELETE /profile
1276
+ # POST /profile
1189
1277
  #
1190
1278
  # === Options
1191
- # Takes same options as +resources+.
1279
+ # Takes same options as resources[rdoc-ref:#resources]
1192
1280
  def resource(*resources, &block)
1193
1281
  options = resources.extract_options!.dup
1194
1282
 
@@ -1196,20 +1284,23 @@ module ActionDispatch
1196
1284
  return self
1197
1285
  end
1198
1286
 
1199
- resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1200
- yield if block_given?
1287
+ with_scope_level(:resource) do
1288
+ options = apply_action_options options
1289
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
1290
+ yield if block_given?
1201
1291
 
1202
- concerns(options[:concerns]) if options[:concerns]
1292
+ concerns(options[:concerns]) if options[:concerns]
1203
1293
 
1204
- collection do
1205
- post :create
1206
- end if parent_resource.actions.include?(:create)
1294
+ new do
1295
+ get :new
1296
+ end if parent_resource.actions.include?(:new)
1207
1297
 
1208
- new do
1209
- get :new
1210
- end if parent_resource.actions.include?(:new)
1298
+ set_member_mappings_for_resource
1211
1299
 
1212
- set_member_mappings_for_resource
1300
+ collection do
1301
+ post :create
1302
+ end if parent_resource.actions.include?(:create)
1303
+ end
1213
1304
  end
1214
1305
 
1215
1306
  self
@@ -1250,7 +1341,7 @@ module ActionDispatch
1250
1341
  # DELETE /photos/:photo_id/comments/:id
1251
1342
  #
1252
1343
  # === Options
1253
- # Takes same options as <tt>Base#match</tt> as well as:
1344
+ # Takes same options as match[rdoc-ref:Base#match] as well as:
1254
1345
  #
1255
1346
  # [:path_names]
1256
1347
  # Allows you to change the segment component of the +edit+ and +new+ actions.
@@ -1258,14 +1349,14 @@ module ActionDispatch
1258
1349
  #
1259
1350
  # resources :posts, path_names: { new: "brand_new" }
1260
1351
  #
1261
- # The above example will now change /posts/new to /posts/brand_new
1352
+ # The above example will now change /posts/new to /posts/brand_new.
1262
1353
  #
1263
1354
  # [:path]
1264
1355
  # Allows you to change the path prefix for the resource.
1265
1356
  #
1266
1357
  # resources :posts, path: 'postings'
1267
1358
  #
1268
- # The resource and all segments will now route to /postings instead of /posts
1359
+ # The resource and all segments will now route to /postings instead of /posts.
1269
1360
  #
1270
1361
  # [:only]
1271
1362
  # Only generate routes for the given actions.
@@ -1354,21 +1445,24 @@ module ActionDispatch
1354
1445
  return self
1355
1446
  end
1356
1447
 
1357
- resource_scope(:resources, Resource.new(resources.pop, options)) do
1358
- yield if block_given?
1448
+ with_scope_level(:resources) do
1449
+ options = apply_action_options options
1450
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
1451
+ yield if block_given?
1359
1452
 
1360
- concerns(options[:concerns]) if options[:concerns]
1453
+ concerns(options[:concerns]) if options[:concerns]
1361
1454
 
1362
- collection do
1363
- get :index if parent_resource.actions.include?(:index)
1364
- post :create if parent_resource.actions.include?(:create)
1365
- end
1455
+ collection do
1456
+ get :index if parent_resource.actions.include?(:index)
1457
+ post :create if parent_resource.actions.include?(:create)
1458
+ end
1366
1459
 
1367
- new do
1368
- get :new
1369
- end if parent_resource.actions.include?(:new)
1460
+ new do
1461
+ get :new
1462
+ end if parent_resource.actions.include?(:new)
1370
1463
 
1371
- set_member_mappings_for_resource
1464
+ set_member_mappings_for_resource
1465
+ end
1372
1466
  end
1373
1467
 
1374
1468
  self
@@ -1392,7 +1486,7 @@ module ActionDispatch
1392
1486
  end
1393
1487
 
1394
1488
  with_scope_level(:collection) do
1395
- scope(parent_resource.collection_scope) do
1489
+ path_scope(parent_resource.collection_scope) do
1396
1490
  yield
1397
1491
  end
1398
1492
  end
@@ -1416,9 +1510,11 @@ module ActionDispatch
1416
1510
 
1417
1511
  with_scope_level(:member) do
1418
1512
  if shallow?
1419
- shallow_scope(parent_resource.member_scope) { yield }
1513
+ shallow_scope {
1514
+ path_scope(parent_resource.member_scope) { yield }
1515
+ }
1420
1516
  else
1421
- scope(parent_resource.member_scope) { yield }
1517
+ path_scope(parent_resource.member_scope) { yield }
1422
1518
  end
1423
1519
  end
1424
1520
  end
@@ -1429,7 +1525,7 @@ module ActionDispatch
1429
1525
  end
1430
1526
 
1431
1527
  with_scope_level(:new) do
1432
- scope(parent_resource.new_scope(action_path(:new))) do
1528
+ path_scope(parent_resource.new_scope(action_path(:new))) do
1433
1529
  yield
1434
1530
  end
1435
1531
  end
@@ -1442,14 +1538,20 @@ module ActionDispatch
1442
1538
 
1443
1539
  with_scope_level(:nested) do
1444
1540
  if shallow? && shallow_nesting_depth >= 1
1445
- shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1541
+ shallow_scope do
1542
+ path_scope(parent_resource.nested_scope) do
1543
+ scope(nested_options) { yield }
1544
+ end
1545
+ end
1446
1546
  else
1447
- scope(parent_resource.nested_scope, nested_options) { yield }
1547
+ path_scope(parent_resource.nested_scope) do
1548
+ scope(nested_options) { yield }
1549
+ end
1448
1550
  end
1449
1551
  end
1450
1552
  end
1451
1553
 
1452
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
1554
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
1453
1555
  def namespace(path, options = {})
1454
1556
  if resource_scope?
1455
1557
  nested { super }
@@ -1459,23 +1561,29 @@ module ActionDispatch
1459
1561
  end
1460
1562
 
1461
1563
  def shallow
1462
- scope(:shallow => true) do
1463
- yield
1464
- end
1564
+ @scope = @scope.new(shallow: true)
1565
+ yield
1566
+ ensure
1567
+ @scope = @scope.parent
1465
1568
  end
1466
1569
 
1467
1570
  def shallow?
1468
- parent_resource.instance_of?(Resource) && @scope[:shallow]
1571
+ !parent_resource.singleton? && @scope[:shallow]
1469
1572
  end
1470
1573
 
1471
- # match 'path' => 'controller#action'
1472
- # match 'path', to: 'controller#action'
1473
- # match 'path', 'otherpath', on: :member, via: :get
1474
- def match(path, *rest)
1574
+ # Matches a URL pattern to one or more routes.
1575
+ # For more information, see match[rdoc-ref:Base#match].
1576
+ #
1577
+ # match 'path' => 'controller#action', via: :patch
1578
+ # match 'path', to: 'controller#action', via: :post
1579
+ # match 'path', 'otherpath', on: :member, via: :get
1580
+ def match(path, *rest, &block)
1475
1581
  if rest.empty? && Hash === path
1476
1582
  options = path
1477
1583
  path, to = options.find { |name, _value| name.is_a?(String) }
1478
1584
 
1585
+ raise ArgumentError, "Route path not specified" if path.nil?
1586
+
1479
1587
  case to
1480
1588
  when Symbol
1481
1589
  options[:action] = to
@@ -1496,77 +1604,30 @@ module ActionDispatch
1496
1604
  paths = [path] + rest
1497
1605
  end
1498
1606
 
1499
- options[:anchor] = true unless options.key?(:anchor)
1500
-
1501
- if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
1502
- raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1503
- end
1504
-
1505
- if @scope[:controller] && @scope[:action]
1506
- options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
1507
- end
1508
-
1509
- paths.each do |_path|
1510
- route_options = options.dup
1511
- route_options[:path] ||= _path if _path.is_a?(String)
1512
-
1513
- path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
1514
- if using_match_shorthand?(path_without_format, route_options)
1515
- route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
1516
- route_options[:to].tr!("-", "_")
1517
- end
1518
-
1519
- decomposed_match(_path, route_options)
1520
- end
1521
- self
1522
- end
1523
-
1524
- def using_match_shorthand?(path, options)
1525
- path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
1526
- end
1527
-
1528
- def decomposed_match(path, options) # :nodoc:
1529
- if on = options.delete(:on)
1530
- send(on) { decomposed_match(path, options) }
1607
+ if options.key?(:defaults)
1608
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
1531
1609
  else
1532
- case @scope.scope_level
1533
- when :resources
1534
- nested { decomposed_match(path, options) }
1535
- when :resource
1536
- member { decomposed_match(path, options) }
1537
- else
1538
- add_route(path, options)
1539
- end
1610
+ map_match(paths, options, &block)
1540
1611
  end
1541
1612
  end
1542
1613
 
1543
- def add_route(action, options) # :nodoc:
1544
- path = path_for_action(action, options.delete(:path))
1545
- raise ArgumentError, "path is required" if path.blank?
1546
-
1547
- action = action.to_s.dup
1548
-
1549
- if action =~ /^[\w\-\/]+$/
1550
- options[:action] ||= action.tr('-', '_') unless action.include?("/")
1551
- else
1552
- action = nil
1553
- end
1554
-
1555
- as = if !options.fetch(:as, true) # if it's set to nil or false
1556
- options.delete(:as)
1557
- else
1558
- name_for_action(options.delete(:as), action)
1559
- end
1560
-
1561
- mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
1562
- app, conditions, requirements, defaults, as, anchor = mapping.to_route
1563
- @set.add_route(app, conditions, requirements, defaults, as, anchor)
1564
- end
1565
-
1566
- def root(path, options={})
1614
+ # You can specify what Rails should route "/" to with the root method:
1615
+ #
1616
+ # root to: 'pages#main'
1617
+ #
1618
+ # For options, see +match+, as +root+ uses it internally.
1619
+ #
1620
+ # You can also pass a string which will expand
1621
+ #
1622
+ # root 'pages#main'
1623
+ #
1624
+ # You should put the root route at the top of <tt>config/routes.rb</tt>,
1625
+ # because this means it will be matched first. As this is the most popular route
1626
+ # of most Rails applications, this is beneficial.
1627
+ def root(path, options = {})
1567
1628
  if path.is_a?(String)
1568
1629
  options[:to] = path
1569
- elsif path.is_a?(Hash) and options.empty?
1630
+ elsif path.is_a?(Hash) && options.empty?
1570
1631
  options = path
1571
1632
  else
1572
1633
  raise ArgumentError, "must be called with a path and/or options"
@@ -1574,22 +1635,22 @@ module ActionDispatch
1574
1635
 
1575
1636
  if @scope.resources?
1576
1637
  with_scope_level(:root) do
1577
- scope(parent_resource.path) do
1578
- super(options)
1638
+ path_scope(parent_resource.path) do
1639
+ match_root_route(options)
1579
1640
  end
1580
1641
  end
1581
1642
  else
1582
- super(options)
1643
+ match_root_route(options)
1583
1644
  end
1584
1645
  end
1585
1646
 
1586
- protected
1647
+ private
1587
1648
 
1588
- def parent_resource #:nodoc:
1649
+ def parent_resource
1589
1650
  @scope[:scope_level_resource]
1590
1651
  end
1591
1652
 
1592
- def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1653
+ def apply_common_behavior_for(method, resources, options, &block)
1593
1654
  if resources.length > 1
1594
1655
  resources.each { |r| send(method, r, options, &block) }
1595
1656
  return true
@@ -1619,71 +1680,51 @@ module ActionDispatch
1619
1680
  return true
1620
1681
  end
1621
1682
 
1622
- unless action_options?(options)
1623
- options.merge!(scope_action_options) if scope_action_options?
1624
- end
1625
-
1626
1683
  false
1627
1684
  end
1628
1685
 
1629
- def action_options?(options) #:nodoc:
1630
- options[:only] || options[:except]
1686
+ def apply_action_options(options)
1687
+ return options if action_options? options
1688
+ options.merge scope_action_options
1631
1689
  end
1632
1690
 
1633
- def scope_action_options? #:nodoc:
1634
- @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1691
+ def action_options?(options)
1692
+ options[:only] || options[:except]
1635
1693
  end
1636
1694
 
1637
- def scope_action_options #:nodoc:
1638
- @scope[:options].slice(:only, :except)
1695
+ def scope_action_options
1696
+ @scope[:action_options] || {}
1639
1697
  end
1640
1698
 
1641
- def resource_scope? #:nodoc:
1699
+ def resource_scope?
1642
1700
  @scope.resource_scope?
1643
1701
  end
1644
1702
 
1645
- def resource_method_scope? #:nodoc:
1703
+ def resource_method_scope?
1646
1704
  @scope.resource_method_scope?
1647
1705
  end
1648
1706
 
1649
- def nested_scope? #:nodoc:
1707
+ def nested_scope?
1650
1708
  @scope.nested?
1651
1709
  end
1652
1710
 
1653
- def with_exclusive_scope
1654
- begin
1655
- @scope = @scope.new(:as => nil, :path => nil)
1656
-
1657
- with_scope_level(:exclusive) do
1658
- yield
1659
- end
1660
- ensure
1661
- @scope = @scope.parent
1662
- end
1663
- end
1664
-
1665
- def with_scope_level(kind)
1711
+ def with_scope_level(kind) # :doc:
1666
1712
  @scope = @scope.new_level(kind)
1667
1713
  yield
1668
1714
  ensure
1669
1715
  @scope = @scope.parent
1670
1716
  end
1671
1717
 
1672
- def resource_scope(kind, resource) #:nodoc:
1673
- resource.shallow = @scope[:shallow]
1674
- @scope = @scope.new(:scope_level_resource => resource)
1675
- @nesting.push(resource)
1718
+ def resource_scope(resource)
1719
+ @scope = @scope.new(scope_level_resource: resource)
1676
1720
 
1677
- with_scope_level(kind) do
1678
- scope(parent_resource.resource_scope) { yield }
1679
- end
1721
+ controller(resource.resource_scope) { yield }
1680
1722
  ensure
1681
- @nesting.pop
1682
1723
  @scope = @scope.parent
1683
1724
  end
1684
1725
 
1685
- def nested_options #:nodoc:
1686
- options = { :as => parent_resource.member_name }
1726
+ def nested_options
1727
+ options = { as: parent_resource.member_name }
1687
1728
  options[:constraints] = {
1688
1729
  parent_resource.nested_param => param_constraint
1689
1730
  } if param_constraint?
@@ -1691,62 +1732,61 @@ module ActionDispatch
1691
1732
  options
1692
1733
  end
1693
1734
 
1694
- def nesting_depth #:nodoc:
1695
- @nesting.size
1696
- end
1697
-
1698
- def shallow_nesting_depth #:nodoc:
1699
- @nesting.select(&:shallow?).size
1735
+ def shallow_nesting_depth
1736
+ @scope.find_all { |node|
1737
+ node.frame[:scope_level_resource]
1738
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
1700
1739
  end
1701
1740
 
1702
- def param_constraint? #:nodoc:
1741
+ def param_constraint?
1703
1742
  @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1704
1743
  end
1705
1744
 
1706
- def param_constraint #:nodoc:
1745
+ def param_constraint
1707
1746
  @scope[:constraints][parent_resource.param]
1708
1747
  end
1709
1748
 
1710
- def canonical_action?(action) #:nodoc:
1749
+ def canonical_action?(action)
1711
1750
  resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1712
1751
  end
1713
1752
 
1714
- def shallow_scope(path, options = {}) #:nodoc:
1715
- scope = { :as => @scope[:shallow_prefix],
1716
- :path => @scope[:shallow_path] }
1753
+ def shallow_scope
1754
+ scope = { as: @scope[:shallow_prefix],
1755
+ path: @scope[:shallow_path] }
1717
1756
  @scope = @scope.new scope
1718
1757
 
1719
- scope(path, options) { yield }
1758
+ yield
1720
1759
  ensure
1721
1760
  @scope = @scope.parent
1722
1761
  end
1723
1762
 
1724
- def path_for_action(action, path) #:nodoc:
1725
- if path.blank? && canonical_action?(action)
1763
+ def path_for_action(action, path)
1764
+ return "#{@scope[:path]}/#{path}" if path
1765
+
1766
+ if canonical_action?(action)
1726
1767
  @scope[:path].to_s
1727
1768
  else
1728
- "#{@scope[:path]}/#{action_path(action, path)}"
1769
+ "#{@scope[:path]}/#{action_path(action)}"
1729
1770
  end
1730
1771
  end
1731
1772
 
1732
- def action_path(name, path = nil) #:nodoc:
1733
- name = name.to_sym if name.is_a?(String)
1734
- path || @scope[:path_names][name] || name.to_s
1773
+ def action_path(name)
1774
+ @scope[:path_names][name.to_sym] || name
1735
1775
  end
1736
1776
 
1737
- def prefix_name_for_action(as, action) #:nodoc:
1777
+ def prefix_name_for_action(as, action)
1738
1778
  if as
1739
1779
  prefix = as
1740
1780
  elsif !canonical_action?(action)
1741
1781
  prefix = action
1742
1782
  end
1743
1783
 
1744
- if prefix && prefix != '/' && !prefix.empty?
1745
- Mapper.normalize_name prefix.to_s.tr('-', '_')
1784
+ if prefix && prefix != "/" && !prefix.empty?
1785
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
1746
1786
  end
1747
1787
  end
1748
1788
 
1749
- def name_for_action(as, action) #:nodoc:
1789
+ def name_for_action(as, action)
1750
1790
  prefix = prefix_name_for_action(as, action)
1751
1791
  name_prefix = @scope[:as]
1752
1792
 
@@ -1757,21 +1797,22 @@ module ActionDispatch
1757
1797
  member_name = parent_resource.member_name
1758
1798
  end
1759
1799
 
1760
- name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1800
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1801
+ candidate = action_name.select(&:present?).join("_")
1761
1802
 
1762
- if candidate = name.compact.join("_").presence
1803
+ unless candidate.empty?
1763
1804
  # If a name was not explicitly given, we check if it is valid
1764
1805
  # and return nil in case it isn't. Otherwise, we pass the invalid name
1765
1806
  # forward so the underlying router engine treats it and raises an exception.
1766
1807
  if as.nil?
1767
- candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
1808
+ candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
1768
1809
  else
1769
1810
  candidate
1770
1811
  end
1771
1812
  end
1772
1813
  end
1773
1814
 
1774
- def set_member_mappings_for_resource
1815
+ def set_member_mappings_for_resource # :doc:
1775
1816
  member do
1776
1817
  get :edit if parent_resource.actions.include?(:edit)
1777
1818
  get :show if parent_resource.actions.include?(:show)
@@ -1782,6 +1823,122 @@ module ActionDispatch
1782
1823
  delete :destroy if parent_resource.actions.include?(:destroy)
1783
1824
  end
1784
1825
  end
1826
+
1827
+ def api_only? # :doc:
1828
+ @set.api_only?
1829
+ end
1830
+
1831
+ def path_scope(path)
1832
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
1833
+ yield
1834
+ ensure
1835
+ @scope = @scope.parent
1836
+ end
1837
+
1838
+ def map_match(paths, options)
1839
+ if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
1840
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1841
+ end
1842
+
1843
+ if @scope[:to]
1844
+ options[:to] ||= @scope[:to]
1845
+ end
1846
+
1847
+ if @scope[:controller] && @scope[:action]
1848
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
1849
+ end
1850
+
1851
+ controller = options.delete(:controller) || @scope[:controller]
1852
+ option_path = options.delete :path
1853
+ to = options.delete :to
1854
+ via = Mapping.check_via Array(options.delete(:via) {
1855
+ @scope[:via]
1856
+ })
1857
+ formatted = options.delete(:format) { @scope[:format] }
1858
+ anchor = options.delete(:anchor) { true }
1859
+ options_constraints = options.delete(:constraints) || {}
1860
+
1861
+ path_types = paths.group_by(&:class)
1862
+ path_types.fetch(String, []).each do |_path|
1863
+ route_options = options.dup
1864
+ if _path && option_path
1865
+ raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
1866
+ end
1867
+ to = get_to_from_path(_path, to, route_options[:action])
1868
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
1869
+ end
1870
+
1871
+ path_types.fetch(Symbol, []).each do |action|
1872
+ route_options = options.dup
1873
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
1874
+ end
1875
+
1876
+ self
1877
+ end
1878
+
1879
+ def get_to_from_path(path, to, action)
1880
+ return to if to || action
1881
+
1882
+ path_without_format = path.sub(/\(\.:format\)$/, "")
1883
+ if using_match_shorthand?(path_without_format)
1884
+ path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
1885
+ else
1886
+ nil
1887
+ end
1888
+ end
1889
+
1890
+ def using_match_shorthand?(path)
1891
+ path =~ %r{^/?[-\w]+/[-\w/]+$}
1892
+ end
1893
+
1894
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
1895
+ if on = options.delete(:on)
1896
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1897
+ else
1898
+ case @scope.scope_level
1899
+ when :resources
1900
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1901
+ when :resource
1902
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1903
+ else
1904
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
1905
+ end
1906
+ end
1907
+ end
1908
+
1909
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
1910
+ path = path_for_action(action, _path)
1911
+ raise ArgumentError, "path is required" if path.blank?
1912
+
1913
+ action = action.to_s
1914
+
1915
+ default_action = options.delete(:action) || @scope[:action]
1916
+
1917
+ if action =~ /^[\w\-\/]+$/
1918
+ default_action ||= action.tr("-", "_") unless action.include?("/")
1919
+ else
1920
+ action = nil
1921
+ end
1922
+
1923
+ as = if !options.fetch(:as, true) # if it's set to nil or false
1924
+ options.delete(:as)
1925
+ else
1926
+ name_for_action(options.delete(:as), action)
1927
+ end
1928
+
1929
+ path = Mapping.normalize_path URI.parser.escape(path), formatted
1930
+ ast = Journey::Parser.parse path
1931
+
1932
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
1933
+ @set.add_route(mapping, as)
1934
+ end
1935
+
1936
+ def match_root_route(options)
1937
+ name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
1938
+ args = ["/", { as: name, via: :get }.merge!(options)]
1939
+
1940
+ match(*args)
1941
+ end
1785
1942
  end
1786
1943
 
1787
1944
  # Routing Concerns allow you to declare common routes that can be reused
@@ -1872,7 +2029,7 @@ module ActionDispatch
1872
2029
  # concerns :commentable
1873
2030
  # end
1874
2031
  #
1875
- # concerns also work in any routes helper that you want to use:
2032
+ # Concerns also work in any routes helper that you want to use:
1876
2033
  #
1877
2034
  # namespace :posts do
1878
2035
  # concerns :commentable
@@ -1889,17 +2046,131 @@ module ActionDispatch
1889
2046
  end
1890
2047
  end
1891
2048
 
2049
+ module CustomUrls
2050
+ # Define custom URL helpers that will be added to the application's
2051
+ # routes. This allows you to override and/or replace the default behavior
2052
+ # of routing helpers, e.g:
2053
+ #
2054
+ # direct :homepage do
2055
+ # "http://www.rubyonrails.org"
2056
+ # end
2057
+ #
2058
+ # direct :commentable do |model|
2059
+ # [ model, anchor: model.dom_id ]
2060
+ # end
2061
+ #
2062
+ # direct :main do
2063
+ # { controller: "pages", action: "index", subdomain: "www" }
2064
+ # end
2065
+ #
2066
+ # The return value from the block passed to +direct+ must be a valid set of
2067
+ # arguments for +url_for+ which will actually build the URL string. This can
2068
+ # be one of the following:
2069
+ #
2070
+ # * A string, which is treated as a generated URL
2071
+ # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt>
2072
+ # * An array, which is passed to +polymorphic_url+
2073
+ # * An Active Model instance
2074
+ # * An Active Model class
2075
+ #
2076
+ # NOTE: Other URL helpers can be called in the block but be careful not to invoke
2077
+ # your custom URL helper again otherwise it will result in a stack overflow error.
2078
+ #
2079
+ # You can also specify default options that will be passed through to
2080
+ # your URL helper definition, e.g:
2081
+ #
2082
+ # direct :browse, page: 1, size: 10 do |options|
2083
+ # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
2084
+ # end
2085
+ #
2086
+ # In this instance the +params+ object comes from the context in which the
2087
+ # block is executed, e.g. generating a URL inside a controller action or a view.
2088
+ # If the block is executed where there isn't a +params+ object such as this:
2089
+ #
2090
+ # Rails.application.routes.url_helpers.browse_path
2091
+ #
2092
+ # then it will raise a +NameError+. Because of this you need to be aware of the
2093
+ # context in which you will use your custom URL helper when defining it.
2094
+ #
2095
+ # NOTE: The +direct+ method can't be used inside of a scope block such as
2096
+ # +namespace+ or +scope+ and will raise an error if it detects that it is.
2097
+ def direct(name, options = {}, &block)
2098
+ unless @scope.root?
2099
+ raise RuntimeError, "The direct method can't be used inside a routes scope block"
2100
+ end
2101
+
2102
+ @set.add_url_helper(name, options, &block)
2103
+ end
2104
+
2105
+ # Define custom polymorphic mappings of models to URLs. This alters the
2106
+ # behavior of +polymorphic_url+ and consequently the behavior of
2107
+ # +link_to+ and +form_for+ when passed a model instance, e.g:
2108
+ #
2109
+ # resource :basket
2110
+ #
2111
+ # resolve "Basket" do
2112
+ # [:basket]
2113
+ # end
2114
+ #
2115
+ # This will now generate "/basket" when a +Basket+ instance is passed to
2116
+ # +link_to+ or +form_for+ instead of the standard "/baskets/:id".
2117
+ #
2118
+ # NOTE: This custom behavior only applies to simple polymorphic URLs where
2119
+ # a single model instance is passed and not more complicated forms, e.g:
2120
+ #
2121
+ # # config/routes.rb
2122
+ # resource :profile
2123
+ # namespace :admin do
2124
+ # resources :users
2125
+ # end
2126
+ #
2127
+ # resolve("User") { [:profile] }
2128
+ #
2129
+ # # app/views/application/_menu.html.erb
2130
+ # link_to "Profile", @current_user
2131
+ # link_to "Profile", [:admin, @current_user]
2132
+ #
2133
+ # The first +link_to+ will generate "/profile" but the second will generate
2134
+ # the standard polymorphic URL of "/admin/users/1".
2135
+ #
2136
+ # You can pass options to a polymorphic mapping - the arity for the block
2137
+ # needs to be two as the instance is passed as the first argument, e.g:
2138
+ #
2139
+ # resolve "Basket", anchor: "items" do |basket, options|
2140
+ # [:basket, options]
2141
+ # end
2142
+ #
2143
+ # This generates the URL "/basket#items" because when the last item in an
2144
+ # array passed to +polymorphic_url+ is a hash then it's treated as options
2145
+ # to the URL helper that gets called.
2146
+ #
2147
+ # NOTE: The +resolve+ method can't be used inside of a scope block such as
2148
+ # +namespace+ or +scope+ and will raise an error if it detects that it is.
2149
+ def resolve(*args, &block)
2150
+ unless @scope.root?
2151
+ raise RuntimeError, "The resolve method can't be used inside a routes scope block"
2152
+ end
2153
+
2154
+ options = args.extract_options!
2155
+ args = args.flatten(1)
2156
+
2157
+ args.each do |klass|
2158
+ @set.add_polymorphic_mapping(klass, options, &block)
2159
+ end
2160
+ end
2161
+ end
2162
+
1892
2163
  class Scope # :nodoc:
1893
2164
  OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
1894
2165
  :controller, :action, :path_names, :constraints,
1895
- :shallow, :blocks, :defaults, :options]
2166
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
1896
2167
 
1897
2168
  RESOURCE_SCOPES = [:resource, :resources]
1898
2169
  RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1899
2170
 
1900
2171
  attr_reader :parent, :scope_level
1901
2172
 
1902
- def initialize(hash, parent = {}, scope_level = nil)
2173
+ def initialize(hash, parent = NULL, scope_level = nil)
1903
2174
  @hash = hash
1904
2175
  @parent = parent
1905
2176
  @scope_level = scope_level
@@ -1909,6 +2180,14 @@ module ActionDispatch
1909
2180
  scope_level == :nested
1910
2181
  end
1911
2182
 
2183
+ def null?
2184
+ @hash.nil? && @parent.nil?
2185
+ end
2186
+
2187
+ def root?
2188
+ @parent.null?
2189
+ end
2190
+
1912
2191
  def resources?
1913
2192
  scope_level == :resources
1914
2193
  end
@@ -1947,27 +2226,33 @@ module ActionDispatch
1947
2226
  end
1948
2227
 
1949
2228
  def new_level(level)
1950
- self.class.new(self, self, level)
1951
- end
1952
-
1953
- def fetch(key, &block)
1954
- @hash.fetch(key, &block)
2229
+ self.class.new(frame, self, level)
1955
2230
  end
1956
2231
 
1957
2232
  def [](key)
1958
- @hash.fetch(key) { @parent[key] }
2233
+ scope = find { |node| node.frame.key? key }
2234
+ scope && scope.frame[key]
1959
2235
  end
1960
2236
 
1961
- def []=(k,v)
1962
- @hash[k] = v
2237
+ include Enumerable
2238
+
2239
+ def each
2240
+ node = self
2241
+ until node.equal? NULL
2242
+ yield node
2243
+ node = node.parent
2244
+ end
1963
2245
  end
2246
+
2247
+ def frame; @hash; end
2248
+
2249
+ NULL = Scope.new(nil, nil)
1964
2250
  end
1965
2251
 
1966
2252
  def initialize(set) #:nodoc:
1967
2253
  @set = set
1968
- @scope = Scope.new({ :path_names => @set.resources_path_names })
2254
+ @scope = Scope.new(path_names: @set.resources_path_names)
1969
2255
  @concerns = {}
1970
- @nesting = []
1971
2256
  end
1972
2257
 
1973
2258
  include Base
@@ -1976,6 +2261,7 @@ module ActionDispatch
1976
2261
  include Scoping
1977
2262
  include Concerns
1978
2263
  include Resources
2264
+ include CustomUrls
1979
2265
  end
1980
2266
  end
1981
2267
  end