actionpack 4.2.11.1 → 6.1.3.2

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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +291 -489
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -9
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +81 -51
  7. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +64 -17
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +61 -33
  10. data/lib/abstract_controller/collector.rb +9 -13
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +115 -99
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +21 -3
  15. data/lib/abstract_controller/rendering.rb +48 -47
  16. data/lib/abstract_controller/translation.rb +17 -8
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +13 -5
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +150 -0
  21. data/lib/action_controller/base.rb +29 -24
  22. data/lib/action_controller/caching.rb +12 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +17 -19
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +134 -46
  27. data/lib/action_controller/metal/content_security_policy.rb +51 -0
  28. data/lib/action_controller/metal/cookies.rb +6 -4
  29. data/lib/action_controller/metal/data_streaming.rb +30 -50
  30. data/lib/action_controller/metal/default_headers.rb +17 -0
  31. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  32. data/lib/action_controller/metal/etag_with_template_digest.rb +21 -16
  33. data/lib/action_controller/metal/exceptions.rb +63 -15
  34. data/lib/action_controller/metal/flash.rb +9 -8
  35. data/lib/action_controller/metal/head.rb +26 -21
  36. data/lib/action_controller/metal/helpers.rb +37 -18
  37. data/lib/action_controller/metal/http_authentication.rb +81 -73
  38. data/lib/action_controller/metal/implicit_render.rb +53 -9
  39. data/lib/action_controller/metal/instrumentation.rb +32 -35
  40. data/lib/action_controller/metal/live.rb +102 -120
  41. data/lib/action_controller/metal/logging.rb +20 -0
  42. data/lib/action_controller/metal/mime_responds.rb +49 -47
  43. data/lib/action_controller/metal/parameter_encoding.rb +82 -0
  44. data/lib/action_controller/metal/params_wrapper.rb +83 -66
  45. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  46. data/lib/action_controller/metal/redirecting.rb +53 -32
  47. data/lib/action_controller/metal/renderers.rb +87 -44
  48. data/lib/action_controller/metal/rendering.rb +77 -50
  49. data/lib/action_controller/metal/request_forgery_protection.rb +267 -103
  50. data/lib/action_controller/metal/rescue.rb +10 -17
  51. data/lib/action_controller/metal/streaming.rb +12 -11
  52. data/lib/action_controller/metal/strong_parameters.rb +714 -186
  53. data/lib/action_controller/metal/testing.rb +2 -17
  54. data/lib/action_controller/metal/url_for.rb +19 -10
  55. data/lib/action_controller/metal.rb +104 -87
  56. data/lib/action_controller/railtie.rb +28 -10
  57. data/lib/action_controller/railties/helpers.rb +3 -1
  58. data/lib/action_controller/renderer.rb +141 -0
  59. data/lib/action_controller/template_assertions.rb +11 -0
  60. data/lib/action_controller/test_case.rb +296 -422
  61. data/lib/action_controller.rb +34 -23
  62. data/lib/action_dispatch/http/cache.rb +107 -56
  63. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  64. data/lib/action_dispatch/http/content_security_policy.rb +286 -0
  65. data/lib/action_dispatch/http/filter_parameters.rb +32 -25
  66. data/lib/action_dispatch/http/filter_redirect.rb +10 -12
  67. data/lib/action_dispatch/http/headers.rb +55 -22
  68. data/lib/action_dispatch/http/mime_negotiation.rb +79 -51
  69. data/lib/action_dispatch/http/mime_type.rb +153 -121
  70. data/lib/action_dispatch/http/mime_types.rb +20 -6
  71. data/lib/action_dispatch/http/parameters.rb +90 -40
  72. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  73. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  74. data/lib/action_dispatch/http/request.rb +226 -121
  75. data/lib/action_dispatch/http/response.rb +248 -113
  76. data/lib/action_dispatch/http/upload.rb +21 -7
  77. data/lib/action_dispatch/http/url.rb +182 -100
  78. data/lib/action_dispatch/journey/formatter.rb +90 -43
  79. data/lib/action_dispatch/journey/gtg/builder.rb +28 -41
  80. data/lib/action_dispatch/journey/gtg/simulator.rb +11 -16
  81. data/lib/action_dispatch/journey/gtg/transition_table.rb +23 -21
  82. data/lib/action_dispatch/journey/nfa/dot.rb +3 -14
  83. data/lib/action_dispatch/journey/nodes/node.rb +29 -15
  84. data/lib/action_dispatch/journey/parser.rb +17 -16
  85. data/lib/action_dispatch/journey/parser.y +4 -3
  86. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  87. data/lib/action_dispatch/journey/path/pattern.rb +58 -54
  88. data/lib/action_dispatch/journey/route.rb +100 -32
  89. data/lib/action_dispatch/journey/router/utils.rb +29 -18
  90. data/lib/action_dispatch/journey/router.rb +55 -51
  91. data/lib/action_dispatch/journey/routes.rb +17 -17
  92. data/lib/action_dispatch/journey/scanner.rb +26 -17
  93. data/lib/action_dispatch/journey/visitors.rb +98 -54
  94. data/lib/action_dispatch/journey.rb +5 -5
  95. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  96. data/lib/action_dispatch/middleware/callbacks.rb +3 -6
  97. data/lib/action_dispatch/middleware/cookies.rb +347 -217
  98. data/lib/action_dispatch/middleware/debug_exceptions.rb +135 -63
  99. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  100. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  101. data/lib/action_dispatch/middleware/exception_wrapper.rb +115 -71
  102. data/lib/action_dispatch/middleware/executor.rb +21 -0
  103. data/lib/action_dispatch/middleware/flash.rb +78 -54
  104. data/lib/action_dispatch/middleware/host_authorization.rb +130 -0
  105. data/lib/action_dispatch/middleware/public_exceptions.rb +32 -27
  106. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  107. data/lib/action_dispatch/middleware/remote_ip.rb +53 -45
  108. data/lib/action_dispatch/middleware/request_id.rb +17 -10
  109. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -26
  110. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  111. data/lib/action_dispatch/middleware/session/cookie_store.rb +74 -75
  112. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  113. data/lib/action_dispatch/middleware/show_exceptions.rb +28 -23
  114. data/lib/action_dispatch/middleware/ssl.rb +118 -35
  115. data/lib/action_dispatch/middleware/stack.rb +82 -41
  116. data/lib/action_dispatch/middleware/static.rb +156 -89
  117. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -14
  121. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +4 -2
  123. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  125. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +23 -4
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  129. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +105 -8
  132. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  135. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  136. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  137. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  138. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  139. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  140. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +87 -64
  141. data/lib/action_dispatch/railtie.rb +27 -13
  142. data/lib/action_dispatch/request/session.rb +109 -61
  143. data/lib/action_dispatch/request/utils.rb +90 -23
  144. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  145. data/lib/action_dispatch/routing/inspector.rb +141 -102
  146. data/lib/action_dispatch/routing/mapper.rb +811 -473
  147. data/lib/action_dispatch/routing/polymorphic_routes.rb +167 -143
  148. data/lib/action_dispatch/routing/redirection.rb +37 -27
  149. data/lib/action_dispatch/routing/route_set.rb +363 -331
  150. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  151. data/lib/action_dispatch/routing/url_for.rb +66 -26
  152. data/lib/action_dispatch/routing.rb +36 -36
  153. data/lib/action_dispatch/system_test_case.rb +190 -0
  154. data/lib/action_dispatch/system_testing/browser.rb +86 -0
  155. data/lib/action_dispatch/system_testing/driver.rb +67 -0
  156. data/lib/action_dispatch/system_testing/server.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +138 -0
  158. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +29 -0
  159. data/lib/action_dispatch/testing/assertion_response.rb +46 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +44 -22
  161. data/lib/action_dispatch/testing/assertions/routing.rb +47 -31
  162. data/lib/action_dispatch/testing/assertions.rb +6 -4
  163. data/lib/action_dispatch/testing/integration.rb +391 -220
  164. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  165. data/lib/action_dispatch/testing/test_process.rb +53 -22
  166. data/lib/action_dispatch/testing/test_request.rb +27 -34
  167. data/lib/action_dispatch/testing/test_response.rb +11 -11
  168. data/lib/action_dispatch.rb +35 -21
  169. data/lib/action_pack/gem_version.rb +6 -4
  170. data/lib/action_pack/version.rb +3 -1
  171. data/lib/action_pack.rb +4 -2
  172. metadata +78 -48
  173. data/lib/action_controller/metal/force_ssl.rb +0 -97
  174. data/lib/action_controller/metal/hide_actions.rb +0 -40
  175. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  176. data/lib/action_controller/middleware.rb +0 -39
  177. data/lib/action_controller/model_naming.rb +0 -12
  178. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  179. data/lib/action_dispatch/journey/backwards.rb +0 -5
  180. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  181. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  182. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  183. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  184. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  185. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  186. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  187. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,39 +1,40 @@
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 "active_support/core_ext/symbol/starts_ends_with"
8
+ require "action_dispatch/routing/redirection"
9
+ require "action_dispatch/routing/endpoint"
12
10
 
13
11
  module ActionDispatch
14
12
  module Routing
15
13
  class Mapper
16
14
  URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
17
15
 
18
- class Constraints < Endpoint #:nodoc:
16
+ class Constraints < Routing::Endpoint #:nodoc:
19
17
  attr_reader :app, :constraints
20
18
 
21
- def initialize(app, constraints, dispatcher_p)
22
- # Unwrap Constraints objects. I don't actually think it's possible
19
+ SERVE = ->(app, req) { app.serve req }
20
+ CALL = ->(app, req) { app.call req.env }
21
+
22
+ def initialize(app, constraints, strategy)
23
+ # Unwrap Constraints objects. I don't actually think it's possible
23
24
  # to pass a Constraints object to this constructor, but there were
24
- # multiple places that kept testing children of this object. I
25
+ # multiple places that kept testing children of this object. I
25
26
  # *think* they were just being defensive, but I have no idea.
26
27
  if app.is_a?(self.class)
27
28
  constraints += app.constraints
28
29
  app = app.app
29
30
  end
30
31
 
31
- @dispatcher = dispatcher_p
32
+ @strategy = strategy
32
33
 
33
34
  @app, @constraints, = app, constraints
34
35
  end
35
36
 
36
- def dispatcher?; @dispatcher; end
37
+ def dispatcher?; @strategy == SERVE; end
37
38
 
38
39
  def matches?(req)
39
40
  @constraints.all? do |constraint|
@@ -43,18 +44,26 @@ module ActionDispatch
43
44
  end
44
45
 
45
46
  def serve(req)
46
- return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
47
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
47
48
 
48
- if dispatcher?
49
- @app.serve req
50
- else
51
- @app.call req.env
52
- end
49
+ @strategy.call @app, req
53
50
  end
54
51
 
55
52
  private
56
53
  def constraint_args(constraint, request)
57
- constraint.arity == 1 ? [request] : [request.path_parameters, request]
54
+ arity = if constraint.respond_to?(:arity)
55
+ constraint.arity
56
+ else
57
+ constraint.method(:call).arity
58
+ end
59
+
60
+ if arity < 1
61
+ []
62
+ elsif arity == 1
63
+ [request]
64
+ else
65
+ [request.path_parameters, request]
66
+ end
58
67
  end
59
68
  end
60
69
 
@@ -62,101 +71,170 @@ module ActionDispatch
62
71
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
63
72
  OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
64
73
 
65
- attr_reader :requirements, :conditions, :defaults
66
- attr_reader :to, :default_controller, :default_action, :as, :anchor
74
+ attr_reader :requirements, :defaults, :to, :default_controller,
75
+ :default_action, :required_defaults, :ast, :scope_options
67
76
 
68
- def self.build(scope, set, path, as, options)
69
- options = scope[:options].merge(options) if scope[:options]
77
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
78
+ scope_params = {
79
+ blocks: scope[:blocks] || [],
80
+ constraints: scope[:constraints] || {},
81
+ defaults: (scope[:defaults] || {}).dup,
82
+ module: scope[:module],
83
+ options: scope[:options] || {}
84
+ }
70
85
 
71
- options.delete :only
72
- options.delete :except
73
- options.delete :shallow_path
74
- options.delete :shallow_prefix
75
- options.delete :shallow
86
+ new set: set, ast: ast, controller: controller, default_action: default_action,
87
+ to: to, formatted: formatted, via: via, options_constraints: options_constraints,
88
+ anchor: anchor, scope_params: scope_params, options: scope_params[:options].merge(options)
89
+ end
90
+
91
+ def self.check_via(via)
92
+ if via.empty?
93
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
94
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
95
+ "If you want to expose your action to GET, use `get` in the router:\n" \
96
+ " Instead of: match \"controller#action\"\n" \
97
+ " Do: get \"controller#action\""
98
+ raise ArgumentError, msg
99
+ end
100
+ via
101
+ end
102
+
103
+ def self.normalize_path(path, format)
104
+ path = Mapper.normalize_path(path)
76
105
 
77
- defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
106
+ if format == true
107
+ "#{path}.:format"
108
+ elsif optional_format?(path, format)
109
+ "#{path}(.:format)"
110
+ else
111
+ path
112
+ end
113
+ end
78
114
 
79
- new scope, set, path, defaults, as, options
115
+ def self.optional_format?(path, format)
116
+ format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
80
117
  end
81
118
 
82
- def initialize(scope, set, path, defaults, as, options)
83
- @requirements, @conditions = {}, {}
84
- @defaults = defaults
85
- @set = set
119
+ def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
120
+ @defaults = scope_params[:defaults]
121
+ @set = set
122
+ @to = intern(to)
123
+ @default_controller = intern(controller)
124
+ @default_action = intern(default_action)
125
+ @ast = ast
126
+ @anchor = anchor
127
+ @via = via
128
+ @internal = options.delete(:internal)
129
+ @scope_options = scope_params[:options]
130
+
131
+ path_params = []
132
+ wildcard_options = {}
133
+ ast.each do |node|
134
+ if node.symbol?
135
+ path_params << node.to_sym
136
+ elsif formatted != false && node.star?
137
+ # Add a constraint for wildcard route to make it non-greedy and match the
138
+ # optional format part of the route by default.
139
+ wildcard_options[node.name.to_sym] ||= /.+?/
140
+ elsif node.cat?
141
+ alter_regex_for_custom_routes(node)
142
+ end
143
+ end
86
144
 
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
145
+ options = wildcard_options.merge!(options)
92
146
 
93
- formatted = options.delete :format
94
- via = Array(options.delete(:via) { [] })
95
- options_constraints = options.delete :constraints
147
+ options = normalize_options!(options, path_params, scope_params[:module])
96
148
 
97
- path = normalize_path! path, formatted
98
- ast = path_ast path
99
- path_params = path_params ast
149
+ split_options = constraints(options, path_params)
100
150
 
101
- options = normalize_options!(options, formatted, path_params, ast, scope[:module])
151
+ constraints = scope_params[:constraints].merge Hash[split_options[:constraints] || []]
102
152
 
153
+ if options_constraints.is_a?(Hash)
154
+ @defaults = Hash[options_constraints.find_all { |key, default|
155
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
156
+ }].merge @defaults
157
+ @blocks = scope_params[:blocks]
158
+ constraints.merge! options_constraints
159
+ else
160
+ @blocks = blocks(options_constraints)
161
+ end
103
162
 
104
- split_constraints(path_params, scope[:constraints]) if scope[:constraints]
105
- constraints = constraints(options, path_params)
163
+ requirements, conditions = split_constraints path_params, constraints
164
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
106
165
 
107
- split_constraints path_params, constraints
166
+ formats = normalize_format(formatted)
108
167
 
109
- @blocks = blocks(options_constraints, scope[:blocks])
168
+ @requirements = formats[:requirements].merge Hash[requirements]
169
+ @conditions = Hash[conditions]
170
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
110
171
 
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
172
+ if path_params.include?(:action) && !@requirements.key?(:action)
173
+ @defaults[:action] ||= "index"
118
174
  end
119
175
 
120
- normalize_format!(formatted)
176
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
177
+ end
121
178
 
122
- @conditions[:path_info] = path
123
- @conditions[:parsed_path_info] = ast
179
+ def make_route(name, precedence)
180
+ Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
181
+ required_defaults: required_defaults, defaults: defaults,
182
+ request_method_match: request_method, precedence: precedence,
183
+ scope_options: scope_options, internal: @internal)
184
+ end
124
185
 
125
- add_request_method(via, @conditions)
126
- normalize_defaults!(options)
186
+ def application
187
+ app(@blocks)
127
188
  end
128
189
 
129
- def to_route
130
- [ app(@blocks), conditions, requirements, defaults, as, anchor ]
190
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
191
+
192
+ def path
193
+ Journey::Path::Pattern.new(@ast, requirements, JOINED_SEPARATORS, @anchor)
131
194
  end
132
195
 
133
- private
196
+ def conditions
197
+ build_conditions @conditions, @set.request_class
198
+ end
134
199
 
135
- def normalize_path!(path, format)
136
- path = Mapper.normalize_path(path)
200
+ def build_conditions(current_conditions, request_class)
201
+ conditions = current_conditions.dup
137
202
 
138
- if format == true
139
- "#{path}.:format"
140
- elsif optional_format?(path, format)
141
- "#{path}(.:format)"
142
- else
143
- path
144
- end
203
+ conditions.keep_if do |k, _|
204
+ request_class.public_method_defined?(k)
145
205
  end
206
+ end
207
+ private :build_conditions
146
208
 
147
- def optional_format?(path, format)
148
- format != false && path !~ OPTIONAL_FORMAT_REGEX
149
- end
209
+ def request_method
210
+ @via.map { |x| Journey::Route.verb_matcher(x) }
211
+ end
212
+ private :request_method
150
213
 
151
- def normalize_options!(options, formatted, path_params, path_ast, modyoule)
152
- # Add a constraint for wildcard route to make it non-greedy and match the
153
- # optional format part of the route by default
154
- if formatted != false
155
- path_ast.grep(Journey::Nodes::Star) do |node|
156
- options[node.name.to_sym] ||= /.+?/
157
- end
214
+ private
215
+ # Find all the symbol nodes that are adjacent to literal nodes and alter
216
+ # the regexp so that Journey will partition them into custom routes.
217
+ def alter_regex_for_custom_routes(node)
218
+ if node.left.literal? && node.right.symbol?
219
+ symbol = node.right
220
+ elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
221
+ symbol = node.right.left
222
+ elsif node.left.symbol? && node.right.literal?
223
+ symbol = node.left
224
+ elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
225
+ symbol = node.left
158
226
  end
159
227
 
228
+ if symbol
229
+ symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
230
+ end
231
+ end
232
+
233
+ def intern(object)
234
+ object.is_a?(String) ? -object : object
235
+ end
236
+
237
+ def normalize_options!(options, path_params, modyoule)
160
238
  if path_params.include?(:controller)
161
239
  raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
162
240
 
@@ -167,7 +245,7 @@ module ActionDispatch
167
245
  options[:controller] ||= /.+?/
168
246
  end
169
247
 
170
- if to.respond_to? :call
248
+ if to.respond_to?(:action) || to.respond_to?(:call)
171
249
  options
172
250
  else
173
251
  to_endpoint = split_to to
@@ -181,82 +259,60 @@ module ActionDispatch
181
259
  end
182
260
 
183
261
  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
262
+ constraints.partition do |key, requirement|
263
+ path_params.include?(key) || key == :controller
191
264
  end
192
265
  end
193
266
 
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
203
- end
204
- end
205
-
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}"
267
+ def normalize_format(formatted)
268
+ case formatted
269
+ when true
270
+ { requirements: { format: /.+/ },
271
+ defaults: {} }
272
+ when Regexp
273
+ { requirements: { format: formatted },
274
+ defaults: { format: nil } }
275
+ when String
276
+ { requirements: { format: Regexp.compile(formatted) },
277
+ defaults: { format: formatted } }
278
+ else
279
+ { requirements: {}, defaults: {} }
213
280
  end
214
281
  end
215
282
 
216
- def normalize_defaults!(options)
217
- options.each_pair do |key, default|
218
- unless Regexp === default
219
- @defaults[key] = default
283
+ def verify_regexp_requirements(requirements)
284
+ requirements.each do |requirement|
285
+ if ANCHOR_CHARACTERS_REGEX.match?(requirement.source)
286
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
220
287
  end
221
- end
222
- end
223
288
 
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?"
289
+ if requirement.multiline?
290
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
291
+ end
227
292
  end
228
293
  end
229
294
 
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 }
295
+ def normalize_defaults(options)
296
+ Hash[options.reject { |_, default| Regexp === default }]
243
297
  end
244
298
 
245
299
  def app(blocks)
246
- if to.respond_to?(:call)
247
- Constraints.new(to, blocks, false)
300
+ if to.respond_to?(:action)
301
+ Routing::RouteSet::StaticDispatcher.new to
302
+ elsif to.respond_to?(:call)
303
+ Constraints.new(to, blocks, Constraints::CALL)
248
304
  elsif blocks.any?
249
- Constraints.new(dispatcher(defaults), blocks, true)
305
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
250
306
  else
251
- dispatcher(defaults)
307
+ dispatcher(defaults.key?(:controller))
252
308
  end
253
309
  end
254
310
 
255
311
  def check_controller_and_action(path_params, controller, action)
256
312
  hash = check_part(:controller, controller, path_params, {}) do |part|
257
313
  translate_controller(part) {
258
- message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
259
- message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
314
+ message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems."
315
+ message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
260
316
 
261
317
  raise ArgumentError, message
262
318
  }
@@ -280,22 +336,8 @@ module ActionDispatch
280
336
  end
281
337
 
282
338
  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]
339
+ if /#/.match?(to)
340
+ to.split("#").map!(&:-@)
299
341
  else
300
342
  []
301
343
  end
@@ -303,10 +345,10 @@ module ActionDispatch
303
345
 
304
346
  def add_controller_module(controller, modyoule)
305
347
  if modyoule && !controller.is_a?(Regexp)
306
- if controller =~ %r{\A/}
307
- controller[1..-1]
348
+ if controller&.start_with?("/")
349
+ -controller[1..-1]
308
350
  else
309
- [modyoule, controller].compact.join("/")
351
+ -[modyoule, controller].compact.join("/")
310
352
  end
311
353
  else
312
354
  controller
@@ -315,54 +357,54 @@ module ActionDispatch
315
357
 
316
358
  def translate_controller(controller)
317
359
  return controller if Regexp === controller
318
- return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
360
+ return controller.to_s if /\A[a-z_0-9][a-z_0-9\/]*\z/.match?(controller)
319
361
 
320
362
  yield
321
363
  end
322
364
 
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 || []
365
+ def blocks(callable_constraint)
366
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
367
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
329
368
  end
369
+ [callable_constraint]
330
370
  end
331
371
 
332
372
  def constraints(options, path_params)
333
- constraints = {}
334
- required_defaults = []
335
- options.each_pair do |key, option|
373
+ options.group_by do |key, option|
336
374
  if Regexp === option
337
- constraints[key] = option
375
+ :constraints
338
376
  else
339
- required_defaults << key unless path_params.include?(key)
377
+ if path_params.include?(key)
378
+ :path_params
379
+ else
380
+ :required_defaults
381
+ end
340
382
  end
341
383
  end
342
- @conditions[:required_defaults] = required_defaults
343
- constraints
344
- end
345
-
346
- def path_params(ast)
347
- ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
348
384
  end
349
385
 
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
386
+ def dispatcher(raise_on_name_error)
387
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
357
388
  end
358
389
  end
359
390
 
360
- # Invokes Journey::Router::Utils.normalize_path and ensure that
361
- # (:locale) becomes (/:locale) instead of /(:locale). Except
362
- # for root cases, where the latter is the correct one.
391
+ # Invokes Journey::Router::Utils.normalize_path, then ensures that
392
+ # /(:locale) becomes (/:locale). Except for root cases, where the
393
+ # former is the correct one.
363
394
  def self.normalize_path(path)
364
395
  path = Journey::Router::Utils.normalize_path(path)
365
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
396
+
397
+ # the path for a root URL at this point can be something like
398
+ # "/(/:locale)(/:platform)/(:browser)", and we would want
399
+ # "/(:locale)(/:platform)(/:browser)"
400
+
401
+ # reverse "/(", "/((" etc to "(/", "((/" etc
402
+ path.gsub!(%r{/(\(+)/?}, '\1/')
403
+ # if a path is all optional segments, change the leading "(/" back to
404
+ # "/(" so it evaluates to "/" when interpreted with no options.
405
+ # Unless, however, at least one secondary segment consists of a static
406
+ # part, ex. "(/:locale)(/pages/:page)"
407
+ path.sub!(%r{^(\(+)/}, '/\1') if %r{^(\(+[^)]+\))(\(+/:[^)]+\))*$}.match?(path)
366
408
  path
367
409
  end
368
410
 
@@ -371,24 +413,7 @@ module ActionDispatch
371
413
  end
372
414
 
373
415
  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.
416
+ # Matches a URL pattern to one or more routes.
392
417
  #
393
418
  # You should not use the +match+ method in your router
394
419
  # without specifying an HTTP method.
@@ -398,7 +423,7 @@ module ActionDispatch
398
423
  # # sets :controller, :action and :id in params
399
424
  # match ':controller/:action/:id', via: [:get, :post]
400
425
  #
401
- # Note that +:controller+, +:action+ and +:id+ are interpreted as url
426
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as URL
402
427
  # query parameters and thus available through +params+ in an action.
403
428
  #
404
429
  # If you want to expose your action to GET, use +get+ in the router:
@@ -435,7 +460,7 @@ module ActionDispatch
435
460
  # A pattern can also point to a +Rack+ endpoint i.e. anything that
436
461
  # responds to +call+:
437
462
  #
438
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
463
+ # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
439
464
  # match 'photos/:id', to: PhotoRackApp, via: :get
440
465
  # # Yes, controller actions are just rack endpoints
441
466
  # match 'photos/:id', to: PhotosController.action(:show), via: :get
@@ -447,7 +472,7 @@ module ActionDispatch
447
472
  #
448
473
  # === Options
449
474
  #
450
- # Any options not seen here are passed on as params with the url.
475
+ # Any options not seen here are passed on as params with the URL.
451
476
  #
452
477
  # [:controller]
453
478
  # The route's controller.
@@ -460,6 +485,31 @@ module ActionDispatch
460
485
  # dynamic segment used to generate the routes).
461
486
  # You can access that segment from your controller using
462
487
  # <tt>params[<:param>]</tt>.
488
+ # In your router:
489
+ #
490
+ # resources :users, param: :name
491
+ #
492
+ # The +users+ resource here will have the following routes generated for it:
493
+ #
494
+ # GET /users(.:format)
495
+ # POST /users(.:format)
496
+ # GET /users/new(.:format)
497
+ # GET /users/:name/edit(.:format)
498
+ # GET /users/:name(.:format)
499
+ # PATCH/PUT /users/:name(.:format)
500
+ # DELETE /users/:name(.:format)
501
+ #
502
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
503
+ # model to construct a URL:
504
+ #
505
+ # class User < ActiveRecord::Base
506
+ # def to_param
507
+ # name
508
+ # end
509
+ # end
510
+ #
511
+ # user = User.find_by(name: 'Phusion')
512
+ # user_path(user) # => "/users/Phusion"
463
513
  #
464
514
  # [:path]
465
515
  # The path prefix for the routes.
@@ -487,7 +537,7 @@ module ActionDispatch
487
537
  # +call+ or a string representing a controller's action.
488
538
  #
489
539
  # match 'path', to: 'controller#action', via: :get
490
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
540
+ # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
491
541
  # match 'path', to: RackApp, via: :get
492
542
  #
493
543
  # [:on]
@@ -511,16 +561,16 @@ module ActionDispatch
511
561
  # Constrains parameters with a hash of regular expressions
512
562
  # or an object that responds to <tt>matches?</tt>. In addition, constraints
513
563
  # other than path can also be specified with any object
514
- # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
564
+ # that responds to <tt>===</tt> (e.g. String, Array, Range, etc.).
515
565
  #
516
566
  # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
517
567
  #
518
568
  # match 'json_only', constraints: { format: 'json' }, via: :get
519
569
  #
520
- # class Whitelist
570
+ # class PermitList
521
571
  # def matches?(request) request.remote_ip == '1.2.3.4' end
522
572
  # end
523
- # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
573
+ # match 'path', to: 'c#a', constraints: PermitList.new, via: :get
524
574
  #
525
575
  # See <tt>Scoping#constraints</tt> for more examples with its scope
526
576
  # equivalent.
@@ -543,7 +593,7 @@ module ActionDispatch
543
593
  # [:format]
544
594
  # Allows you to specify the default value for optional +format+
545
595
  # segment or disable it by supplying +false+.
546
- def match(path, options=nil)
596
+ def match(path, options = nil)
547
597
  end
548
598
 
549
599
  # Mount a Rack-based application to be used within the application.
@@ -568,17 +618,20 @@ module ActionDispatch
568
618
  def mount(app, options = nil)
569
619
  if options
570
620
  path = options.delete(:at)
571
- else
572
- unless Hash === app
573
- raise ArgumentError, "must be called with mount point"
574
- end
575
-
621
+ elsif Hash === app
576
622
  options = app
577
623
  app, path = options.find { |k, _| k.respond_to?(:call) }
578
624
  options.delete(app) if app
579
625
  end
580
626
 
581
- raise "A rack application must be specified" unless path
627
+ raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
628
+ raise ArgumentError, <<~MSG unless path
629
+ Must be called with mount point
630
+
631
+ mount SomeRackApp, at: "some_route"
632
+ or
633
+ mount(SomeRackApp => "some_route")
634
+ MSG
582
635
 
583
636
  rails_app = rails_app? app
584
637
  options[:as] ||= app_name(app, rails_app)
@@ -586,7 +639,7 @@ module ActionDispatch
586
639
  target_as = name_for_action(options[:as], path)
587
640
  options[:via] ||= :all
588
641
 
589
- match(path, options.merge(:to => app, :anchor => false, :format => false))
642
+ match(path, options.merge(to: app, anchor: false, format: false))
590
643
 
591
644
  define_generate_prefix(app, target_as) if rails_app
592
645
  self
@@ -605,7 +658,7 @@ module ActionDispatch
605
658
 
606
659
  # Query if the following named route was already defined.
607
660
  def has_named_route?(name)
608
- @set.named_routes.routes[name.to_sym]
661
+ @set.named_routes.key?(name)
609
662
  end
610
663
 
611
664
  private
@@ -625,18 +678,31 @@ module ActionDispatch
625
678
  def define_generate_prefix(app, name)
626
679
  _route = @set.named_routes.get name
627
680
  _routes = @set
628
- app.routes.define_mounted_helper(name)
681
+ _url_helpers = @set.url_helpers
682
+
683
+ script_namer = ->(options) do
684
+ prefix_options = options.slice(*_route.segment_keys)
685
+ prefix_options[:relative_url_root] = ""
686
+
687
+ if options[:_recall]
688
+ prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
689
+ end
690
+
691
+ # We must actually delete prefix segment keys to avoid passing them to next url_for.
692
+ _route.segment_keys.each { |k| options.delete(k) }
693
+ _url_helpers.public_send("#{name}_path", prefix_options)
694
+ end
695
+
696
+ app.routes.define_mounted_helper(name, script_namer)
697
+
629
698
  app.routes.extend Module.new {
630
699
  def optimize_routes_generation?; false; end
700
+
631
701
  define_method :find_script_name do |options|
632
702
  if options.key? :script_name
633
703
  super(options)
634
704
  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)
705
+ script_namer.call(options)
640
706
  end
641
707
  end
642
708
  }
@@ -684,6 +750,14 @@ module ActionDispatch
684
750
  map_method(:delete, args, &block)
685
751
  end
686
752
 
753
+ # Define a route that only recognizes HTTP OPTIONS.
754
+ # For supported arguments, see match[rdoc-ref:Base#match]
755
+ #
756
+ # options 'carrots', to: 'food#carrots'
757
+ def options(*args, &block)
758
+ map_method(:options, args, &block)
759
+ end
760
+
687
761
  private
688
762
  def map_method(method, args, &block)
689
763
  options = args.extract_options!
@@ -782,7 +856,7 @@ module ActionDispatch
782
856
  options = args.extract_options!.dup
783
857
  scope = {}
784
858
 
785
- options[:path] = args.flatten.join('/') if args.any?
859
+ options[:path] = args.flatten.join("/") if args.any?
786
860
  options[:constraints] ||= {}
787
861
 
788
862
  unless nested_scope?
@@ -795,21 +869,30 @@ module ActionDispatch
795
869
  URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
796
870
  end
797
871
 
798
- (options[:defaults] ||= {}).reverse_merge!(defaults)
872
+ options[:defaults] = defaults.merge(options[:defaults] || {})
799
873
  else
800
874
  block, options[:constraints] = options[:constraints], {}
801
875
  end
802
876
 
877
+ if options.key?(:only) || options.key?(:except)
878
+ scope[:action_options] = { only: options.delete(:only),
879
+ except: options.delete(:except) }
880
+ end
881
+
882
+ if options.key? :anchor
883
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
884
+ end
885
+
803
886
  @scope.options.each do |option|
804
887
  if option == :blocks
805
888
  value = block
806
889
  elsif option == :options
807
890
  value = options
808
891
  else
809
- value = options.delete(option)
892
+ value = options.delete(option) { POISON }
810
893
  end
811
894
 
812
- if value
895
+ unless POISON == value
813
896
  scope[option] = send("merge_#{option}_scope", @scope[option], value)
814
897
  end
815
898
  end
@@ -821,14 +904,18 @@ module ActionDispatch
821
904
  @scope = @scope.parent
822
905
  end
823
906
 
907
+ POISON = Object.new # :nodoc:
908
+
824
909
  # Scopes routes to a specific controller
825
910
  #
826
911
  # controller "food" do
827
- # match "bacon", action: "bacon"
912
+ # match "bacon", action: :bacon, via: :get
828
913
  # end
829
- def controller(controller, options={})
830
- options[:controller] = controller
831
- scope(options) { yield }
914
+ def controller(controller)
915
+ @scope = @scope.new(controller: controller)
916
+ yield
917
+ ensure
918
+ @scope = @scope.parent
832
919
  end
833
920
 
834
921
  # Scopes routes to a specific namespace. For example:
@@ -874,13 +961,14 @@ module ActionDispatch
874
961
 
875
962
  defaults = {
876
963
  module: path,
877
- path: options.fetch(:path, path),
878
964
  as: options.fetch(:as, path),
879
965
  shallow_path: options.fetch(:path, path),
880
966
  shallow_prefix: options.fetch(:as, path)
881
967
  }
882
968
 
883
- scope(defaults.merge!(options)) { yield }
969
+ path_scope(options.delete(:path) { path }) do
970
+ scope(defaults.merge!(options)) { yield }
971
+ end
884
972
  end
885
973
 
886
974
  # === Parameter Restriction
@@ -917,7 +1005,7 @@ module ActionDispatch
917
1005
  #
918
1006
  # Requests to routes can be constrained based on specific criteria:
919
1007
  #
920
- # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
1008
+ # constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do
921
1009
  # resources :iphones
922
1010
  # end
923
1011
  #
@@ -927,7 +1015,7 @@ module ActionDispatch
927
1015
  #
928
1016
  # class Iphone
929
1017
  # def self.matches?(request)
930
- # request.env["HTTP_USER_AGENT"] =~ /iPhone/
1018
+ # /iPhone/.match?(request.env["HTTP_USER_AGENT"])
931
1019
  # end
932
1020
  # end
933
1021
  #
@@ -939,7 +1027,7 @@ module ActionDispatch
939
1027
  # resources :iphones
940
1028
  # end
941
1029
  def constraints(constraints = {})
942
- scope(:constraints => constraints) { yield }
1030
+ scope(constraints: constraints) { yield }
943
1031
  end
944
1032
 
945
1033
  # Allows you to set default parameters for a route, such as this:
@@ -948,66 +1036,77 @@ module ActionDispatch
948
1036
  # end
949
1037
  # Using this, the +:id+ parameter here will default to 'home'.
950
1038
  def defaults(defaults = {})
951
- scope(:defaults => defaults) { yield }
1039
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
1040
+ yield
1041
+ ensure
1042
+ @scope = @scope.parent
952
1043
  end
953
1044
 
954
1045
  private
955
- def merge_path_scope(parent, child) #:nodoc:
1046
+ def merge_path_scope(parent, child)
956
1047
  Mapper.normalize_path("#{parent}/#{child}")
957
1048
  end
958
1049
 
959
- def merge_shallow_path_scope(parent, child) #:nodoc:
1050
+ def merge_shallow_path_scope(parent, child)
960
1051
  Mapper.normalize_path("#{parent}/#{child}")
961
1052
  end
962
1053
 
963
- def merge_as_scope(parent, child) #:nodoc:
1054
+ def merge_as_scope(parent, child)
964
1055
  parent ? "#{parent}_#{child}" : child
965
1056
  end
966
1057
 
967
- def merge_shallow_prefix_scope(parent, child) #:nodoc:
1058
+ def merge_shallow_prefix_scope(parent, child)
968
1059
  parent ? "#{parent}_#{child}" : child
969
1060
  end
970
1061
 
971
- def merge_module_scope(parent, child) #:nodoc:
1062
+ def merge_module_scope(parent, child)
972
1063
  parent ? "#{parent}/#{child}" : child
973
1064
  end
974
1065
 
975
- def merge_controller_scope(parent, child) #:nodoc:
1066
+ def merge_controller_scope(parent, child)
1067
+ child
1068
+ end
1069
+
1070
+ def merge_action_scope(parent, child)
976
1071
  child
977
1072
  end
978
1073
 
979
- def merge_action_scope(parent, child) #:nodoc:
1074
+ def merge_via_scope(parent, child)
980
1075
  child
981
1076
  end
982
1077
 
983
- def merge_path_names_scope(parent, child) #:nodoc:
1078
+ def merge_format_scope(parent, child)
1079
+ child
1080
+ end
1081
+
1082
+ def merge_path_names_scope(parent, child)
984
1083
  merge_options_scope(parent, child)
985
1084
  end
986
1085
 
987
- def merge_constraints_scope(parent, child) #:nodoc:
1086
+ def merge_constraints_scope(parent, child)
988
1087
  merge_options_scope(parent, child)
989
1088
  end
990
1089
 
991
- def merge_defaults_scope(parent, child) #:nodoc:
1090
+ def merge_defaults_scope(parent, child)
992
1091
  merge_options_scope(parent, child)
993
1092
  end
994
1093
 
995
- def merge_blocks_scope(parent, child) #:nodoc:
1094
+ def merge_blocks_scope(parent, child)
996
1095
  merged = parent ? parent.dup : []
997
1096
  merged << child if child
998
1097
  merged
999
1098
  end
1000
1099
 
1001
- def merge_options_scope(parent, child) #:nodoc:
1002
- (parent || {}).except(*override_keys(child)).merge!(child)
1100
+ def merge_options_scope(parent, child)
1101
+ (parent || {}).merge(child)
1003
1102
  end
1004
1103
 
1005
- def merge_shallow_scope(parent, child) #:nodoc:
1104
+ def merge_shallow_scope(parent, child)
1006
1105
  child ? true : false
1007
1106
  end
1008
1107
 
1009
- def override_keys(child) #:nodoc:
1010
- child.key?(:only) || child.key?(:except) ? [:only, :except] : []
1108
+ def merge_to_scope(parent, child)
1109
+ child
1011
1110
  end
1012
1111
  end
1013
1112
 
@@ -1058,27 +1157,44 @@ module ActionDispatch
1058
1157
  CANONICAL_ACTIONS = %w(index create new show update destroy)
1059
1158
 
1060
1159
  class Resource #:nodoc:
1061
- attr_reader :controller, :path, :options, :param
1160
+ attr_reader :controller, :path, :param
1161
+
1162
+ def initialize(entities, api_only, shallow, options = {})
1163
+ if options[:param].to_s.include?(":")
1164
+ raise ArgumentError, ":param option can't contain colons"
1165
+ end
1062
1166
 
1063
- def initialize(entities, options = {})
1064
1167
  @name = entities.to_s
1065
1168
  @path = (options[:path] || @name).to_s
1066
1169
  @controller = (options[:controller] || @name).to_s
1067
1170
  @as = options[:as]
1068
1171
  @param = (options[:param] || :id).to_sym
1069
1172
  @options = options
1070
- @shallow = false
1173
+ @shallow = shallow
1174
+ @api_only = api_only
1175
+ @only = options.delete :only
1176
+ @except = options.delete :except
1071
1177
  end
1072
1178
 
1073
1179
  def default_actions
1074
- [:index, :create, :new, :show, :update, :destroy, :edit]
1180
+ if @api_only
1181
+ [:index, :create, :show, :update, :destroy]
1182
+ else
1183
+ [:index, :create, :new, :show, :update, :destroy, :edit]
1184
+ end
1075
1185
  end
1076
1186
 
1077
1187
  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)
1188
+ if @except
1189
+ available_actions - Array(@except).map(&:to_sym)
1190
+ else
1191
+ available_actions
1192
+ end
1193
+ end
1194
+
1195
+ def available_actions
1196
+ if @only
1197
+ Array(@only).map(&:to_sym)
1082
1198
  else
1083
1199
  default_actions
1084
1200
  end
@@ -1105,7 +1221,7 @@ module ActionDispatch
1105
1221
  end
1106
1222
 
1107
1223
  def resource_scope
1108
- { :controller => controller }
1224
+ controller
1109
1225
  end
1110
1226
 
1111
1227
  alias :collection_scope :path
@@ -1128,17 +1244,15 @@ module ActionDispatch
1128
1244
  "#{path}/:#{nested_param}"
1129
1245
  end
1130
1246
 
1131
- def shallow=(value)
1132
- @shallow = value
1133
- end
1134
-
1135
1247
  def shallow?
1136
1248
  @shallow
1137
1249
  end
1250
+
1251
+ def singleton?; false; end
1138
1252
  end
1139
1253
 
1140
1254
  class SingletonResource < Resource #:nodoc:
1141
- def initialize(entities, options)
1255
+ def initialize(entities, api_only, shallow, options)
1142
1256
  super
1143
1257
  @as = nil
1144
1258
  @controller = (options[:controller] || plural).to_s
@@ -1146,7 +1260,11 @@ module ActionDispatch
1146
1260
  end
1147
1261
 
1148
1262
  def default_actions
1149
- [:show, :create, :update, :destroy, :new, :edit]
1263
+ if @api_only
1264
+ [:show, :create, :update, :destroy]
1265
+ else
1266
+ [:show, :create, :update, :destroy, :new, :edit]
1267
+ end
1150
1268
  end
1151
1269
 
1152
1270
  def plural
@@ -1162,6 +1280,8 @@ module ActionDispatch
1162
1280
 
1163
1281
  alias :member_scope :path
1164
1282
  alias :nested_scope :path
1283
+
1284
+ def singleton?; true; end
1165
1285
  end
1166
1286
 
1167
1287
  def resources_path_names(options)
@@ -1176,19 +1296,19 @@ module ActionDispatch
1176
1296
  #
1177
1297
  # resource :profile
1178
1298
  #
1179
- # creates six different routes in your application, all mapping to
1299
+ # This creates six different routes in your application, all mapping to
1180
1300
  # the +Profiles+ controller (note that the controller is named after
1181
1301
  # the plural):
1182
1302
  #
1183
1303
  # GET /profile/new
1184
- # POST /profile
1185
1304
  # GET /profile
1186
1305
  # GET /profile/edit
1187
1306
  # PATCH/PUT /profile
1188
1307
  # DELETE /profile
1308
+ # POST /profile
1189
1309
  #
1190
1310
  # === Options
1191
- # Takes same options as +resources+.
1311
+ # Takes same options as resources[rdoc-ref:#resources]
1192
1312
  def resource(*resources, &block)
1193
1313
  options = resources.extract_options!.dup
1194
1314
 
@@ -1196,20 +1316,23 @@ module ActionDispatch
1196
1316
  return self
1197
1317
  end
1198
1318
 
1199
- resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1200
- yield if block_given?
1319
+ with_scope_level(:resource) do
1320
+ options = apply_action_options options
1321
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
1322
+ yield if block_given?
1201
1323
 
1202
- concerns(options[:concerns]) if options[:concerns]
1324
+ concerns(options[:concerns]) if options[:concerns]
1203
1325
 
1204
- collection do
1205
- post :create
1206
- end if parent_resource.actions.include?(:create)
1326
+ new do
1327
+ get :new
1328
+ end if parent_resource.actions.include?(:new)
1207
1329
 
1208
- new do
1209
- get :new
1210
- end if parent_resource.actions.include?(:new)
1330
+ set_member_mappings_for_resource
1211
1331
 
1212
- set_member_mappings_for_resource
1332
+ collection do
1333
+ post :create
1334
+ end if parent_resource.actions.include?(:create)
1335
+ end
1213
1336
  end
1214
1337
 
1215
1338
  self
@@ -1250,7 +1373,7 @@ module ActionDispatch
1250
1373
  # DELETE /photos/:photo_id/comments/:id
1251
1374
  #
1252
1375
  # === Options
1253
- # Takes same options as <tt>Base#match</tt> as well as:
1376
+ # Takes same options as match[rdoc-ref:Base#match] as well as:
1254
1377
  #
1255
1378
  # [:path_names]
1256
1379
  # Allows you to change the segment component of the +edit+ and +new+ actions.
@@ -1258,14 +1381,14 @@ module ActionDispatch
1258
1381
  #
1259
1382
  # resources :posts, path_names: { new: "brand_new" }
1260
1383
  #
1261
- # The above example will now change /posts/new to /posts/brand_new
1384
+ # The above example will now change /posts/new to /posts/brand_new.
1262
1385
  #
1263
1386
  # [:path]
1264
1387
  # Allows you to change the path prefix for the resource.
1265
1388
  #
1266
1389
  # resources :posts, path: 'postings'
1267
1390
  #
1268
- # The resource and all segments will now route to /postings instead of /posts
1391
+ # The resource and all segments will now route to /postings instead of /posts.
1269
1392
  #
1270
1393
  # [:only]
1271
1394
  # Only generate routes for the given actions.
@@ -1298,6 +1421,8 @@ module ActionDispatch
1298
1421
  # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
1299
1422
  # to be shortened to just <tt>/comments/1234</tt>.
1300
1423
  #
1424
+ # Set <tt>shallow: false</tt> on a child resource to ignore a parent's shallow parameter.
1425
+ #
1301
1426
  # [:shallow_path]
1302
1427
  # Prefixes nested shallow routes with the specified path.
1303
1428
  #
@@ -1340,6 +1465,9 @@ module ActionDispatch
1340
1465
  # Allows you to specify the default value for optional +format+
1341
1466
  # segment or disable it by supplying +false+.
1342
1467
  #
1468
+ # [:param]
1469
+ # Allows you to override the default param name of +:id+ in the URL.
1470
+ #
1343
1471
  # === Examples
1344
1472
  #
1345
1473
  # # routes call <tt>Admin::PostsController</tt>
@@ -1354,21 +1482,24 @@ module ActionDispatch
1354
1482
  return self
1355
1483
  end
1356
1484
 
1357
- resource_scope(:resources, Resource.new(resources.pop, options)) do
1358
- yield if block_given?
1485
+ with_scope_level(:resources) do
1486
+ options = apply_action_options options
1487
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
1488
+ yield if block_given?
1359
1489
 
1360
- concerns(options[:concerns]) if options[:concerns]
1490
+ concerns(options[:concerns]) if options[:concerns]
1361
1491
 
1362
- collection do
1363
- get :index if parent_resource.actions.include?(:index)
1364
- post :create if parent_resource.actions.include?(:create)
1365
- end
1492
+ collection do
1493
+ get :index if parent_resource.actions.include?(:index)
1494
+ post :create if parent_resource.actions.include?(:create)
1495
+ end
1366
1496
 
1367
- new do
1368
- get :new
1369
- end if parent_resource.actions.include?(:new)
1497
+ new do
1498
+ get :new
1499
+ end if parent_resource.actions.include?(:new)
1370
1500
 
1371
- set_member_mappings_for_resource
1501
+ set_member_mappings_for_resource
1502
+ end
1372
1503
  end
1373
1504
 
1374
1505
  self
@@ -1392,7 +1523,7 @@ module ActionDispatch
1392
1523
  end
1393
1524
 
1394
1525
  with_scope_level(:collection) do
1395
- scope(parent_resource.collection_scope) do
1526
+ path_scope(parent_resource.collection_scope) do
1396
1527
  yield
1397
1528
  end
1398
1529
  end
@@ -1416,9 +1547,11 @@ module ActionDispatch
1416
1547
 
1417
1548
  with_scope_level(:member) do
1418
1549
  if shallow?
1419
- shallow_scope(parent_resource.member_scope) { yield }
1550
+ shallow_scope {
1551
+ path_scope(parent_resource.member_scope) { yield }
1552
+ }
1420
1553
  else
1421
- scope(parent_resource.member_scope) { yield }
1554
+ path_scope(parent_resource.member_scope) { yield }
1422
1555
  end
1423
1556
  end
1424
1557
  end
@@ -1429,7 +1562,7 @@ module ActionDispatch
1429
1562
  end
1430
1563
 
1431
1564
  with_scope_level(:new) do
1432
- scope(parent_resource.new_scope(action_path(:new))) do
1565
+ path_scope(parent_resource.new_scope(action_path(:new))) do
1433
1566
  yield
1434
1567
  end
1435
1568
  end
@@ -1442,14 +1575,20 @@ module ActionDispatch
1442
1575
 
1443
1576
  with_scope_level(:nested) do
1444
1577
  if shallow? && shallow_nesting_depth >= 1
1445
- shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1578
+ shallow_scope do
1579
+ path_scope(parent_resource.nested_scope) do
1580
+ scope(nested_options) { yield }
1581
+ end
1582
+ end
1446
1583
  else
1447
- scope(parent_resource.nested_scope, nested_options) { yield }
1584
+ path_scope(parent_resource.nested_scope) do
1585
+ scope(nested_options) { yield }
1586
+ end
1448
1587
  end
1449
1588
  end
1450
1589
  end
1451
1590
 
1452
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
1591
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
1453
1592
  def namespace(path, options = {})
1454
1593
  if resource_scope?
1455
1594
  nested { super }
@@ -1459,28 +1598,50 @@ module ActionDispatch
1459
1598
  end
1460
1599
 
1461
1600
  def shallow
1462
- scope(:shallow => true) do
1463
- yield
1464
- end
1601
+ @scope = @scope.new(shallow: true)
1602
+ yield
1603
+ ensure
1604
+ @scope = @scope.parent
1465
1605
  end
1466
1606
 
1467
1607
  def shallow?
1468
- parent_resource.instance_of?(Resource) && @scope[:shallow]
1608
+ !parent_resource.singleton? && @scope[:shallow]
1469
1609
  end
1470
1610
 
1471
- # match 'path' => 'controller#action'
1472
- # match 'path', to: 'controller#action'
1473
- # match 'path', 'otherpath', on: :member, via: :get
1474
- def match(path, *rest)
1611
+ def draw(name)
1612
+ path = @draw_paths.find do |_path|
1613
+ File.exist? "#{_path}/#{name}.rb"
1614
+ end
1615
+
1616
+ unless path
1617
+ msg = "Your router tried to #draw the external file #{name}.rb,\n" \
1618
+ "but the file was not found in:\n\n"
1619
+ msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n")
1620
+ raise ArgumentError, msg
1621
+ end
1622
+
1623
+ route_path = "#{path}/#{name}.rb"
1624
+ instance_eval(File.read(route_path), route_path.to_s)
1625
+ end
1626
+
1627
+ # Matches a URL pattern to one or more routes.
1628
+ # For more information, see match[rdoc-ref:Base#match].
1629
+ #
1630
+ # match 'path' => 'controller#action', via: :patch
1631
+ # match 'path', to: 'controller#action', via: :post
1632
+ # match 'path', 'otherpath', on: :member, via: :get
1633
+ def match(path, *rest, &block)
1475
1634
  if rest.empty? && Hash === path
1476
1635
  options = path
1477
1636
  path, to = options.find { |name, _value| name.is_a?(String) }
1478
1637
 
1638
+ raise ArgumentError, "Route path not specified" if path.nil?
1639
+
1479
1640
  case to
1480
1641
  when Symbol
1481
1642
  options[:action] = to
1482
1643
  when String
1483
- if to =~ /#/
1644
+ if /#/.match?(to)
1484
1645
  options[:to] = to
1485
1646
  else
1486
1647
  options[:controller] = to
@@ -1496,77 +1657,30 @@ module ActionDispatch
1496
1657
  paths = [path] + rest
1497
1658
  end
1498
1659
 
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) }
1531
- 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
1540
- end
1541
- end
1542
-
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?("/")
1660
+ if options.key?(:defaults)
1661
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
1551
1662
  else
1552
- action = nil
1663
+ map_match(paths, options, &block)
1553
1664
  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
1665
  end
1565
1666
 
1566
- def root(path, options={})
1667
+ # You can specify what Rails should route "/" to with the root method:
1668
+ #
1669
+ # root to: 'pages#main'
1670
+ #
1671
+ # For options, see +match+, as +root+ uses it internally.
1672
+ #
1673
+ # You can also pass a string which will expand
1674
+ #
1675
+ # root 'pages#main'
1676
+ #
1677
+ # You should put the root route at the top of <tt>config/routes.rb</tt>,
1678
+ # because this means it will be matched first. As this is the most popular route
1679
+ # of most Rails applications, this is beneficial.
1680
+ def root(path, options = {})
1567
1681
  if path.is_a?(String)
1568
1682
  options[:to] = path
1569
- elsif path.is_a?(Hash) and options.empty?
1683
+ elsif path.is_a?(Hash) && options.empty?
1570
1684
  options = path
1571
1685
  else
1572
1686
  raise ArgumentError, "must be called with a path and/or options"
@@ -1574,36 +1688,36 @@ module ActionDispatch
1574
1688
 
1575
1689
  if @scope.resources?
1576
1690
  with_scope_level(:root) do
1577
- scope(parent_resource.path) do
1578
- super(options)
1691
+ path_scope(parent_resource.path) do
1692
+ match_root_route(options)
1579
1693
  end
1580
1694
  end
1581
1695
  else
1582
- super(options)
1696
+ match_root_route(options)
1583
1697
  end
1584
1698
  end
1585
1699
 
1586
- protected
1587
-
1588
- def parent_resource #:nodoc:
1700
+ private
1701
+ def parent_resource
1589
1702
  @scope[:scope_level_resource]
1590
1703
  end
1591
1704
 
1592
- def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1705
+ def apply_common_behavior_for(method, resources, options, &block)
1593
1706
  if resources.length > 1
1594
- resources.each { |r| send(method, r, options, &block) }
1707
+ resources.each { |r| public_send(method, r, options, &block) }
1595
1708
  return true
1596
1709
  end
1597
1710
 
1598
- if options.delete(:shallow)
1711
+ if options[:shallow]
1712
+ options.delete(:shallow)
1599
1713
  shallow do
1600
- send(method, resources.pop, options, &block)
1714
+ public_send(method, resources.pop, options, &block)
1601
1715
  end
1602
1716
  return true
1603
1717
  end
1604
1718
 
1605
1719
  if resource_scope?
1606
- nested { send(method, resources.pop, options, &block) }
1720
+ nested { public_send(method, resources.pop, options, &block) }
1607
1721
  return true
1608
1722
  end
1609
1723
 
@@ -1614,76 +1728,56 @@ module ActionDispatch
1614
1728
  scope_options = options.slice!(*RESOURCE_OPTIONS)
1615
1729
  unless scope_options.empty?
1616
1730
  scope(scope_options) do
1617
- send(method, resources.pop, options, &block)
1731
+ public_send(method, resources.pop, options, &block)
1618
1732
  end
1619
1733
  return true
1620
1734
  end
1621
1735
 
1622
- unless action_options?(options)
1623
- options.merge!(scope_action_options) if scope_action_options?
1624
- end
1625
-
1626
1736
  false
1627
1737
  end
1628
1738
 
1629
- def action_options?(options) #:nodoc:
1630
- options[:only] || options[:except]
1739
+ def apply_action_options(options)
1740
+ return options if action_options? options
1741
+ options.merge scope_action_options
1631
1742
  end
1632
1743
 
1633
- def scope_action_options? #:nodoc:
1634
- @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1744
+ def action_options?(options)
1745
+ options[:only] || options[:except]
1635
1746
  end
1636
1747
 
1637
- def scope_action_options #:nodoc:
1638
- @scope[:options].slice(:only, :except)
1748
+ def scope_action_options
1749
+ @scope[:action_options] || {}
1639
1750
  end
1640
1751
 
1641
- def resource_scope? #:nodoc:
1752
+ def resource_scope?
1642
1753
  @scope.resource_scope?
1643
1754
  end
1644
1755
 
1645
- def resource_method_scope? #:nodoc:
1756
+ def resource_method_scope?
1646
1757
  @scope.resource_method_scope?
1647
1758
  end
1648
1759
 
1649
- def nested_scope? #:nodoc:
1760
+ def nested_scope?
1650
1761
  @scope.nested?
1651
1762
  end
1652
1763
 
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)
1764
+ def with_scope_level(kind) # :doc:
1666
1765
  @scope = @scope.new_level(kind)
1667
1766
  yield
1668
1767
  ensure
1669
1768
  @scope = @scope.parent
1670
1769
  end
1671
1770
 
1672
- def resource_scope(kind, resource) #:nodoc:
1673
- resource.shallow = @scope[:shallow]
1674
- @scope = @scope.new(:scope_level_resource => resource)
1675
- @nesting.push(resource)
1771
+ def resource_scope(resource)
1772
+ @scope = @scope.new(scope_level_resource: resource)
1676
1773
 
1677
- with_scope_level(kind) do
1678
- scope(parent_resource.resource_scope) { yield }
1679
- end
1774
+ controller(resource.resource_scope) { yield }
1680
1775
  ensure
1681
- @nesting.pop
1682
1776
  @scope = @scope.parent
1683
1777
  end
1684
1778
 
1685
- def nested_options #:nodoc:
1686
- options = { :as => parent_resource.member_name }
1779
+ def nested_options
1780
+ options = { as: parent_resource.member_name }
1687
1781
  options[:constraints] = {
1688
1782
  parent_resource.nested_param => param_constraint
1689
1783
  } if param_constraint?
@@ -1691,62 +1785,61 @@ module ActionDispatch
1691
1785
  options
1692
1786
  end
1693
1787
 
1694
- def nesting_depth #:nodoc:
1695
- @nesting.size
1788
+ def shallow_nesting_depth
1789
+ @scope.find_all { |node|
1790
+ node.frame[:scope_level_resource]
1791
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
1696
1792
  end
1697
1793
 
1698
- def shallow_nesting_depth #:nodoc:
1699
- @nesting.select(&:shallow?).size
1700
- end
1701
-
1702
- def param_constraint? #:nodoc:
1794
+ def param_constraint?
1703
1795
  @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1704
1796
  end
1705
1797
 
1706
- def param_constraint #:nodoc:
1798
+ def param_constraint
1707
1799
  @scope[:constraints][parent_resource.param]
1708
1800
  end
1709
1801
 
1710
- def canonical_action?(action) #:nodoc:
1802
+ def canonical_action?(action)
1711
1803
  resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1712
1804
  end
1713
1805
 
1714
- def shallow_scope(path, options = {}) #:nodoc:
1715
- scope = { :as => @scope[:shallow_prefix],
1716
- :path => @scope[:shallow_path] }
1806
+ def shallow_scope
1807
+ scope = { as: @scope[:shallow_prefix],
1808
+ path: @scope[:shallow_path] }
1717
1809
  @scope = @scope.new scope
1718
1810
 
1719
- scope(path, options) { yield }
1811
+ yield
1720
1812
  ensure
1721
1813
  @scope = @scope.parent
1722
1814
  end
1723
1815
 
1724
- def path_for_action(action, path) #:nodoc:
1725
- if path.blank? && canonical_action?(action)
1816
+ def path_for_action(action, path)
1817
+ return "#{@scope[:path]}/#{path}" if path
1818
+
1819
+ if canonical_action?(action)
1726
1820
  @scope[:path].to_s
1727
1821
  else
1728
- "#{@scope[:path]}/#{action_path(action, path)}"
1822
+ "#{@scope[:path]}/#{action_path(action)}"
1729
1823
  end
1730
1824
  end
1731
1825
 
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
1826
+ def action_path(name)
1827
+ @scope[:path_names][name.to_sym] || name
1735
1828
  end
1736
1829
 
1737
- def prefix_name_for_action(as, action) #:nodoc:
1830
+ def prefix_name_for_action(as, action)
1738
1831
  if as
1739
1832
  prefix = as
1740
1833
  elsif !canonical_action?(action)
1741
1834
  prefix = action
1742
1835
  end
1743
1836
 
1744
- if prefix && prefix != '/' && !prefix.empty?
1745
- Mapper.normalize_name prefix.to_s.tr('-', '_')
1837
+ if prefix && prefix != "/" && !prefix.empty?
1838
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
1746
1839
  end
1747
1840
  end
1748
1841
 
1749
- def name_for_action(as, action) #:nodoc:
1842
+ def name_for_action(as, action)
1750
1843
  prefix = prefix_name_for_action(as, action)
1751
1844
  name_prefix = @scope[:as]
1752
1845
 
@@ -1757,21 +1850,22 @@ module ActionDispatch
1757
1850
  member_name = parent_resource.member_name
1758
1851
  end
1759
1852
 
1760
- name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1853
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1854
+ candidate = action_name.select(&:present?).join("_")
1761
1855
 
1762
- if candidate = name.compact.join("_").presence
1856
+ unless candidate.empty?
1763
1857
  # If a name was not explicitly given, we check if it is valid
1764
1858
  # and return nil in case it isn't. Otherwise, we pass the invalid name
1765
1859
  # forward so the underlying router engine treats it and raises an exception.
1766
1860
  if as.nil?
1767
- candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
1861
+ candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
1768
1862
  else
1769
1863
  candidate
1770
1864
  end
1771
1865
  end
1772
1866
  end
1773
1867
 
1774
- def set_member_mappings_for_resource
1868
+ def set_member_mappings_for_resource # :doc:
1775
1869
  member do
1776
1870
  get :edit if parent_resource.actions.include?(:edit)
1777
1871
  get :show if parent_resource.actions.include?(:show)
@@ -1782,6 +1876,120 @@ module ActionDispatch
1782
1876
  delete :destroy if parent_resource.actions.include?(:destroy)
1783
1877
  end
1784
1878
  end
1879
+
1880
+ def api_only? # :doc:
1881
+ @set.api_only?
1882
+ end
1883
+
1884
+ def path_scope(path)
1885
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
1886
+ yield
1887
+ ensure
1888
+ @scope = @scope.parent
1889
+ end
1890
+
1891
+ def map_match(paths, options)
1892
+ if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
1893
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1894
+ end
1895
+
1896
+ if @scope[:to]
1897
+ options[:to] ||= @scope[:to]
1898
+ end
1899
+
1900
+ if @scope[:controller] && @scope[:action]
1901
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
1902
+ end
1903
+
1904
+ controller = options.delete(:controller) || @scope[:controller]
1905
+ option_path = options.delete :path
1906
+ to = options.delete :to
1907
+ via = Mapping.check_via Array(options.delete(:via) {
1908
+ @scope[:via]
1909
+ })
1910
+ formatted = options.delete(:format) { @scope[:format] }
1911
+ anchor = options.delete(:anchor) { true }
1912
+ options_constraints = options.delete(:constraints) || {}
1913
+
1914
+ path_types = paths.group_by(&:class)
1915
+ (path_types[String] || []).each do |_path|
1916
+ route_options = options.dup
1917
+ if _path && option_path
1918
+ raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
1919
+ end
1920
+ to = get_to_from_path(_path, to, route_options[:action])
1921
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
1922
+ end
1923
+
1924
+ (path_types[Symbol] || []).each do |action|
1925
+ route_options = options.dup
1926
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
1927
+ end
1928
+
1929
+ self
1930
+ end
1931
+
1932
+ def get_to_from_path(path, to, action)
1933
+ return to if to || action
1934
+
1935
+ path_without_format = path.sub(/\(\.:format\)$/, "")
1936
+ if using_match_shorthand?(path_without_format)
1937
+ path_without_format.delete_prefix("/").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
1938
+ else
1939
+ nil
1940
+ end
1941
+ end
1942
+
1943
+ def using_match_shorthand?(path)
1944
+ %r{^/?[-\w]+/[-\w/]+$}.match?(path)
1945
+ end
1946
+
1947
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
1948
+ if on = options.delete(:on)
1949
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1950
+ else
1951
+ case @scope.scope_level
1952
+ when :resources
1953
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1954
+ when :resource
1955
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1956
+ else
1957
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
1958
+ end
1959
+ end
1960
+ end
1961
+
1962
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
1963
+ path = path_for_action(action, _path)
1964
+ raise ArgumentError, "path is required" if path.blank?
1965
+
1966
+ action = action.to_s
1967
+
1968
+ default_action = options.delete(:action) || @scope[:action]
1969
+
1970
+ if /^[\w\-\/]+$/.match?(action)
1971
+ default_action ||= action.tr("-", "_") unless action.include?("/")
1972
+ else
1973
+ action = nil
1974
+ end
1975
+
1976
+ as = if !options.fetch(:as, true) # if it's set to nil or false
1977
+ options.delete(:as)
1978
+ else
1979
+ name_for_action(options.delete(:as), action)
1980
+ end
1981
+
1982
+ path = Mapping.normalize_path URI::DEFAULT_PARSER.escape(path), formatted
1983
+ ast = Journey::Parser.parse path
1984
+
1985
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
1986
+ @set.add_route(mapping, as)
1987
+ end
1988
+
1989
+ def match_root_route(options)
1990
+ args = ["/", { as: :root, via: :get }.merge(options)]
1991
+ match(*args)
1992
+ end
1785
1993
  end
1786
1994
 
1787
1995
  # Routing Concerns allow you to declare common routes that can be reused
@@ -1872,7 +2080,7 @@ module ActionDispatch
1872
2080
  # concerns :commentable
1873
2081
  # end
1874
2082
  #
1875
- # concerns also work in any routes helper that you want to use:
2083
+ # Concerns also work in any routes helper that you want to use:
1876
2084
  #
1877
2085
  # namespace :posts do
1878
2086
  # concerns :commentable
@@ -1889,17 +2097,131 @@ module ActionDispatch
1889
2097
  end
1890
2098
  end
1891
2099
 
2100
+ module CustomUrls
2101
+ # Define custom URL helpers that will be added to the application's
2102
+ # routes. This allows you to override and/or replace the default behavior
2103
+ # of routing helpers, e.g:
2104
+ #
2105
+ # direct :homepage do
2106
+ # "https://rubyonrails.org"
2107
+ # end
2108
+ #
2109
+ # direct :commentable do |model|
2110
+ # [ model, anchor: model.dom_id ]
2111
+ # end
2112
+ #
2113
+ # direct :main do
2114
+ # { controller: "pages", action: "index", subdomain: "www" }
2115
+ # end
2116
+ #
2117
+ # The return value from the block passed to +direct+ must be a valid set of
2118
+ # arguments for +url_for+ which will actually build the URL string. This can
2119
+ # be one of the following:
2120
+ #
2121
+ # * A string, which is treated as a generated URL
2122
+ # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt>
2123
+ # * An array, which is passed to +polymorphic_url+
2124
+ # * An Active Model instance
2125
+ # * An Active Model class
2126
+ #
2127
+ # NOTE: Other URL helpers can be called in the block but be careful not to invoke
2128
+ # your custom URL helper again otherwise it will result in a stack overflow error.
2129
+ #
2130
+ # You can also specify default options that will be passed through to
2131
+ # your URL helper definition, e.g:
2132
+ #
2133
+ # direct :browse, page: 1, size: 10 do |options|
2134
+ # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
2135
+ # end
2136
+ #
2137
+ # In this instance the +params+ object comes from the context in which the
2138
+ # block is executed, e.g. generating a URL inside a controller action or a view.
2139
+ # If the block is executed where there isn't a +params+ object such as this:
2140
+ #
2141
+ # Rails.application.routes.url_helpers.browse_path
2142
+ #
2143
+ # then it will raise a +NameError+. Because of this you need to be aware of the
2144
+ # context in which you will use your custom URL helper when defining it.
2145
+ #
2146
+ # NOTE: The +direct+ method can't be used inside of a scope block such as
2147
+ # +namespace+ or +scope+ and will raise an error if it detects that it is.
2148
+ def direct(name, options = {}, &block)
2149
+ unless @scope.root?
2150
+ raise RuntimeError, "The direct method can't be used inside a routes scope block"
2151
+ end
2152
+
2153
+ @set.add_url_helper(name, options, &block)
2154
+ end
2155
+
2156
+ # Define custom polymorphic mappings of models to URLs. This alters the
2157
+ # behavior of +polymorphic_url+ and consequently the behavior of
2158
+ # +link_to+ and +form_for+ when passed a model instance, e.g:
2159
+ #
2160
+ # resource :basket
2161
+ #
2162
+ # resolve "Basket" do
2163
+ # [:basket]
2164
+ # end
2165
+ #
2166
+ # This will now generate "/basket" when a +Basket+ instance is passed to
2167
+ # +link_to+ or +form_for+ instead of the standard "/baskets/:id".
2168
+ #
2169
+ # NOTE: This custom behavior only applies to simple polymorphic URLs where
2170
+ # a single model instance is passed and not more complicated forms, e.g:
2171
+ #
2172
+ # # config/routes.rb
2173
+ # resource :profile
2174
+ # namespace :admin do
2175
+ # resources :users
2176
+ # end
2177
+ #
2178
+ # resolve("User") { [:profile] }
2179
+ #
2180
+ # # app/views/application/_menu.html.erb
2181
+ # link_to "Profile", @current_user
2182
+ # link_to "Profile", [:admin, @current_user]
2183
+ #
2184
+ # The first +link_to+ will generate "/profile" but the second will generate
2185
+ # the standard polymorphic URL of "/admin/users/1".
2186
+ #
2187
+ # You can pass options to a polymorphic mapping - the arity for the block
2188
+ # needs to be two as the instance is passed as the first argument, e.g:
2189
+ #
2190
+ # resolve "Basket", anchor: "items" do |basket, options|
2191
+ # [:basket, options]
2192
+ # end
2193
+ #
2194
+ # This generates the URL "/basket#items" because when the last item in an
2195
+ # array passed to +polymorphic_url+ is a hash then it's treated as options
2196
+ # to the URL helper that gets called.
2197
+ #
2198
+ # NOTE: The +resolve+ method can't be used inside of a scope block such as
2199
+ # +namespace+ or +scope+ and will raise an error if it detects that it is.
2200
+ def resolve(*args, &block)
2201
+ unless @scope.root?
2202
+ raise RuntimeError, "The resolve method can't be used inside a routes scope block"
2203
+ end
2204
+
2205
+ options = args.extract_options!
2206
+ args = args.flatten(1)
2207
+
2208
+ args.each do |klass|
2209
+ @set.add_polymorphic_mapping(klass, options, &block)
2210
+ end
2211
+ end
2212
+ end
2213
+
1892
2214
  class Scope # :nodoc:
1893
2215
  OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
1894
2216
  :controller, :action, :path_names, :constraints,
1895
- :shallow, :blocks, :defaults, :options]
2217
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
1896
2218
 
1897
2219
  RESOURCE_SCOPES = [:resource, :resources]
1898
2220
  RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1899
2221
 
1900
2222
  attr_reader :parent, :scope_level
1901
2223
 
1902
- def initialize(hash, parent = {}, scope_level = nil)
2224
+ def initialize(hash, parent = NULL, scope_level = nil)
1903
2225
  @hash = hash
1904
2226
  @parent = parent
1905
2227
  @scope_level = scope_level
@@ -1909,6 +2231,14 @@ module ActionDispatch
1909
2231
  scope_level == :nested
1910
2232
  end
1911
2233
 
2234
+ def null?
2235
+ @hash.nil? && @parent.nil?
2236
+ end
2237
+
2238
+ def root?
2239
+ @parent.null?
2240
+ end
2241
+
1912
2242
  def resources?
1913
2243
  scope_level == :resources
1914
2244
  end
@@ -1947,27 +2277,34 @@ module ActionDispatch
1947
2277
  end
1948
2278
 
1949
2279
  def new_level(level)
1950
- self.class.new(self, self, level)
1951
- end
1952
-
1953
- def fetch(key, &block)
1954
- @hash.fetch(key, &block)
2280
+ self.class.new(frame, self, level)
1955
2281
  end
1956
2282
 
1957
2283
  def [](key)
1958
- @hash.fetch(key) { @parent[key] }
2284
+ scope = find { |node| node.frame.key? key }
2285
+ scope && scope.frame[key]
1959
2286
  end
1960
2287
 
1961
- def []=(k,v)
1962
- @hash[k] = v
2288
+ include Enumerable
2289
+
2290
+ def each
2291
+ node = self
2292
+ until node.equal? NULL
2293
+ yield node
2294
+ node = node.parent
2295
+ end
1963
2296
  end
2297
+
2298
+ def frame; @hash; end
2299
+
2300
+ NULL = Scope.new(nil, nil)
1964
2301
  end
1965
2302
 
1966
2303
  def initialize(set) #:nodoc:
1967
2304
  @set = set
1968
- @scope = Scope.new({ :path_names => @set.resources_path_names })
2305
+ @draw_paths = set.draw_paths
2306
+ @scope = Scope.new(path_names: @set.resources_path_names)
1969
2307
  @concerns = {}
1970
- @nesting = []
1971
2308
  end
1972
2309
 
1973
2310
  include Base
@@ -1976,6 +2313,7 @@ module ActionDispatch
1976
2313
  include Scoping
1977
2314
  include Concerns
1978
2315
  include Resources
2316
+ include CustomUrls
1979
2317
  end
1980
2318
  end
1981
2319
  end