actionpack 4.2.10 → 7.2.0.rc1

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 (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,39 +1,53 @@
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
+ # :markup: markdown
4
+
5
+ require "active_support/core_ext/hash/slice"
6
+ require "active_support/core_ext/enumerable"
7
+ require "active_support/core_ext/array/extract_options"
8
+ require "active_support/core_ext/regexp"
9
+ require "action_dispatch/routing/redirection"
10
+ require "action_dispatch/routing/endpoint"
12
11
 
13
12
  module ActionDispatch
14
13
  module Routing
15
14
  class Mapper
15
+ class BacktraceCleaner < ActiveSupport::BacktraceCleaner # :nodoc:
16
+ def initialize
17
+ super
18
+ remove_silencers!
19
+ add_core_silencer
20
+ add_stdlib_silencer
21
+ end
22
+ end
23
+
16
24
  URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
17
25
 
18
- class Constraints < Endpoint #:nodoc:
26
+ cattr_accessor :route_source_locations, instance_accessor: false, default: false
27
+ cattr_accessor :backtrace_cleaner, instance_accessor: false, default: BacktraceCleaner.new
28
+
29
+ class Constraints < Routing::Endpoint # :nodoc:
19
30
  attr_reader :app, :constraints
20
31
 
21
- def initialize(app, constraints, dispatcher_p)
22
- # Unwrap Constraints objects. I don't actually think it's possible
23
- # to pass a Constraints object to this constructor, but there were
24
- # multiple places that kept testing children of this object. I
25
- # *think* they were just being defensive, but I have no idea.
32
+ SERVE = ->(app, req) { app.serve req }
33
+ CALL = ->(app, req) { app.call req.env }
34
+
35
+ def initialize(app, constraints, strategy)
36
+ # Unwrap Constraints objects. I don't actually think it's possible to pass a
37
+ # Constraints object to this constructor, but there were multiple places that
38
+ # kept testing children of this object. I **think** they were just being
39
+ # defensive, but I have no idea.
26
40
  if app.is_a?(self.class)
27
41
  constraints += app.constraints
28
42
  app = app.app
29
43
  end
30
44
 
31
- @dispatcher = dispatcher_p
45
+ @strategy = strategy
32
46
 
33
47
  @app, @constraints, = app, constraints
34
48
  end
35
49
 
36
- def dispatcher?; @dispatcher; end
50
+ def dispatcher?; @strategy == SERVE; end
37
51
 
38
52
  def matches?(req)
39
53
  @constraints.all? do |constraint|
@@ -43,136 +57,192 @@ module ActionDispatch
43
57
  end
44
58
 
45
59
  def serve(req)
46
- return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
60
+ return [ 404, { Constants::X_CASCADE => "pass" }, [] ] unless matches?(req)
47
61
 
48
- if dispatcher?
49
- @app.serve req
50
- else
51
- @app.call req.env
52
- end
62
+ @strategy.call @app, req
53
63
  end
54
64
 
55
65
  private
56
66
  def constraint_args(constraint, request)
57
- constraint.arity == 1 ? [request] : [request.path_parameters, request]
67
+ arity = if constraint.respond_to?(:arity)
68
+ constraint.arity
69
+ else
70
+ constraint.method(:call).arity
71
+ end
72
+
73
+ if arity < 1
74
+ []
75
+ elsif arity == 1
76
+ [request]
77
+ else
78
+ [request.path_parameters, request]
79
+ end
58
80
  end
59
81
  end
60
82
 
61
- class Mapping #:nodoc:
83
+ class Mapping # :nodoc:
62
84
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
63
85
  OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
64
86
 
65
- attr_reader :requirements, :conditions, :defaults
66
- attr_reader :to, :default_controller, :default_action, :as, :anchor
87
+ attr_reader :path, :requirements, :defaults, :to, :default_controller,
88
+ :default_action, :required_defaults, :ast, :scope_options
89
+
90
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
91
+ scope_params = {
92
+ blocks: scope[:blocks] || [],
93
+ constraints: scope[:constraints] || {},
94
+ defaults: (scope[:defaults] || {}).dup,
95
+ module: scope[:module],
96
+ options: scope[:options] || {}
97
+ }
98
+
99
+ new set: set, ast: ast, controller: controller, default_action: default_action,
100
+ to: to, formatted: formatted, via: via, options_constraints: options_constraints,
101
+ anchor: anchor, scope_params: scope_params, options: scope_params[:options].merge(options)
102
+ end
67
103
 
68
- def self.build(scope, set, path, as, options)
69
- options = scope[:options].merge(options) if scope[:options]
104
+ def self.check_via(via)
105
+ if via.empty?
106
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
107
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
108
+ "If you want to expose your action to GET, use `get` in the router:\n" \
109
+ " Instead of: match \"controller#action\"\n" \
110
+ " Do: get \"controller#action\""
111
+ raise ArgumentError, msg
112
+ end
113
+ via
114
+ end
70
115
 
71
- options.delete :only
72
- options.delete :except
73
- options.delete :shallow_path
74
- options.delete :shallow_prefix
75
- options.delete :shallow
116
+ def self.normalize_path(path, format)
117
+ path = Mapper.normalize_path(path)
76
118
 
77
- defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
119
+ if format == true
120
+ "#{path}.:format"
121
+ elsif optional_format?(path, format)
122
+ "#{path}(.:format)"
123
+ else
124
+ path
125
+ end
126
+ end
78
127
 
79
- new scope, set, path, defaults, as, options
128
+ def self.optional_format?(path, format)
129
+ format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
80
130
  end
81
131
 
82
- def initialize(scope, set, path, defaults, as, options)
83
- @requirements, @conditions = {}, {}
84
- @defaults = defaults
85
- @set = set
132
+ def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
133
+ @defaults = scope_params[:defaults]
134
+ @set = set
135
+ @to = intern(to)
136
+ @default_controller = intern(controller)
137
+ @default_action = intern(default_action)
138
+ @anchor = anchor
139
+ @via = via
140
+ @internal = options.delete(:internal)
141
+ @scope_options = scope_params[:options]
142
+ ast = Journey::Ast.new(ast, formatted)
86
143
 
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
144
+ options = ast.wildcard_options.merge!(options)
92
145
 
93
- formatted = options.delete :format
94
- via = Array(options.delete(:via) { [] })
95
- options_constraints = options.delete :constraints
146
+ options = normalize_options!(options, ast.path_params, scope_params[:module])
96
147
 
97
- path = normalize_path! path, formatted
98
- ast = path_ast path
99
- path_params = path_params ast
148
+ split_options = constraints(options, ast.path_params)
100
149
 
101
- options = normalize_options!(options, formatted, path_params, ast, scope[:module])
150
+ constraints = scope_params[:constraints].merge Hash[split_options[:constraints] || []]
102
151
 
152
+ if options_constraints.is_a?(Hash)
153
+ @defaults = Hash[options_constraints.find_all { |key, default|
154
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
155
+ }].merge @defaults
156
+ @blocks = scope_params[:blocks]
157
+ constraints.merge! options_constraints
158
+ else
159
+ @blocks = blocks(options_constraints)
160
+ end
103
161
 
104
- split_constraints(path_params, scope[:constraints]) if scope[:constraints]
105
- constraints = constraints(options, path_params)
162
+ requirements, conditions = split_constraints ast.path_params, constraints
163
+ verify_regexp_requirements requirements, ast.wildcard_options
106
164
 
107
- split_constraints path_params, constraints
165
+ formats = normalize_format(formatted)
108
166
 
109
- @blocks = blocks(options_constraints, scope[:blocks])
167
+ @requirements = formats[:requirements].merge Hash[requirements]
168
+ @conditions = Hash[conditions]
169
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
110
170
 
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
171
+ if ast.path_params.include?(:action) && !@requirements.key?(:action)
172
+ @defaults[:action] ||= "index"
118
173
  end
119
174
 
120
- normalize_format!(formatted)
175
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
121
176
 
122
- @conditions[:path_info] = path
123
- @conditions[:parsed_path_info] = ast
177
+ ast.requirements = @requirements
178
+ @path = Journey::Path::Pattern.new(ast, @requirements, JOINED_SEPARATORS, @anchor)
179
+ end
180
+
181
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
124
182
 
125
- add_request_method(via, @conditions)
126
- normalize_defaults!(options)
183
+ def make_route(name, precedence)
184
+ Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
185
+ required_defaults: required_defaults, defaults: defaults,
186
+ request_method_match: request_method, precedence: precedence,
187
+ scope_options: scope_options, internal: @internal, source_location: route_source_location)
127
188
  end
128
189
 
129
- def to_route
130
- [ app(@blocks), conditions, requirements, defaults, as, anchor ]
190
+ def application
191
+ app(@blocks)
131
192
  end
132
193
 
133
- private
194
+ def conditions
195
+ build_conditions @conditions, @set.request_class
196
+ end
134
197
 
135
- def normalize_path!(path, format)
136
- path = Mapper.normalize_path(path)
198
+ def build_conditions(current_conditions, request_class)
199
+ conditions = current_conditions.dup
137
200
 
138
- if format == true
139
- "#{path}.:format"
140
- elsif optional_format?(path, format)
141
- "#{path}(.:format)"
142
- else
143
- path
144
- end
201
+ conditions.keep_if do |k, _|
202
+ request_class.public_method_defined?(k)
145
203
  end
204
+ end
205
+ private :build_conditions
146
206
 
147
- def optional_format?(path, format)
148
- format != false && path !~ OPTIONAL_FORMAT_REGEX
149
- end
207
+ def request_method
208
+ @via.map { |x| Journey::Route.verb_matcher(x) }
209
+ end
210
+ private :request_method
150
211
 
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
158
- end
212
+ private
213
+ def intern(object)
214
+ object.is_a?(String) ? -object : object
215
+ end
159
216
 
217
+ def normalize_options!(options, path_params, modyoule)
160
218
  if path_params.include?(:controller)
161
219
  raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
162
220
 
163
221
  # Add a default constraint for :controller path segments that matches namespaced
164
222
  # controllers with default routes like :controller/:action/:id(.:format), e.g:
165
223
  # GET /admin/products/show/1
166
- # => { controller: 'admin/products', action: 'show', id: '1' }
224
+ # # > { controller: 'admin/products', action: 'show', id: '1' }
167
225
  options[:controller] ||= /.+?/
168
226
  end
169
227
 
170
- if to.respond_to? :call
228
+ if to.respond_to?(:action) || to.respond_to?(:call)
171
229
  options
172
230
  else
173
- to_endpoint = split_to to
174
- controller = to_endpoint[0] || default_controller
175
- action = to_endpoint[1] || default_action
231
+ if to.nil?
232
+ controller = default_controller
233
+ action = default_action
234
+ elsif to.is_a?(String)
235
+ if to.include?("#")
236
+ to_endpoint = to.split("#").map!(&:-@)
237
+ controller = to_endpoint[0]
238
+ action = to_endpoint[1]
239
+ else
240
+ controller = default_controller
241
+ action = to
242
+ end
243
+ else
244
+ raise ArgumentError, ":to must respond to `action` or `call`, or it must be a String that includes '#', or the controller should be implicit"
245
+ end
176
246
 
177
247
  controller = add_controller_module(controller, modyoule)
178
248
 
@@ -181,82 +251,64 @@ module ActionDispatch
181
251
  end
182
252
 
183
253
  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
254
+ constraints.partition do |key, requirement|
255
+ path_params.include?(key) || key == :controller
191
256
  end
192
257
  end
193
258
 
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
+ def normalize_format(formatted)
260
+ case formatted
261
+ when true
262
+ { requirements: { format: /.+/ },
263
+ defaults: {} }
264
+ when Regexp
265
+ { requirements: { format: formatted },
266
+ defaults: { format: nil } }
267
+ when String
268
+ { requirements: { format: Regexp.compile(formatted) },
269
+ defaults: { format: formatted } }
270
+ else
271
+ { requirements: {}, defaults: {} }
203
272
  end
204
273
  end
205
274
 
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}"
213
- end
214
- end
275
+ def verify_regexp_requirements(requirements, wildcard_options)
276
+ requirements.each do |requirement, regex|
277
+ next unless regex.is_a? Regexp
215
278
 
216
- def normalize_defaults!(options)
217
- options.each_pair do |key, default|
218
- unless Regexp === default
219
- @defaults[key] = default
279
+ if ANCHOR_CHARACTERS_REGEX.match?(regex.source)
280
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
220
281
  end
221
- end
222
- end
223
-
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?"
227
- end
228
- end
229
282
 
230
- def add_request_method(via, conditions)
231
- return if via == [:all]
283
+ if regex.multiline?
284
+ next if wildcard_options.key?(requirement)
232
285
 
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
286
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{regex.inspect}"
287
+ end
240
288
  end
289
+ end
241
290
 
242
- conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
291
+ def normalize_defaults(options)
292
+ Hash[options.reject { |_, default| Regexp === default }]
243
293
  end
244
294
 
245
295
  def app(blocks)
246
- if to.respond_to?(:call)
247
- Constraints.new(to, blocks, false)
296
+ if to.respond_to?(:action)
297
+ Routing::RouteSet::StaticDispatcher.new to
298
+ elsif to.respond_to?(:call)
299
+ Constraints.new(to, blocks, Constraints::CALL)
248
300
  elsif blocks.any?
249
- Constraints.new(dispatcher(defaults), blocks, true)
301
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
250
302
  else
251
- dispatcher(defaults)
303
+ dispatcher(defaults.key?(:controller))
252
304
  end
253
305
  end
254
306
 
255
307
  def check_controller_and_action(path_params, controller, action)
256
308
  hash = check_part(:controller, controller, path_params, {}) do |part|
257
309
  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"
310
+ message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems."
311
+ message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
260
312
 
261
313
  raise ArgumentError, message
262
314
  }
@@ -279,34 +331,12 @@ module ActionDispatch
279
331
  hash
280
332
  end
281
333
 
282
- 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]
299
- else
300
- []
301
- end
302
- end
303
-
304
334
  def add_controller_module(controller, modyoule)
305
335
  if modyoule && !controller.is_a?(Regexp)
306
- if controller =~ %r{\A/}
307
- controller[1..-1]
336
+ if controller&.start_with?("/")
337
+ -controller[1..-1]
308
338
  else
309
- [modyoule, controller].compact.join("/")
339
+ -[modyoule, controller].compact.join("/")
310
340
  end
311
341
  else
312
342
  controller
@@ -315,54 +345,84 @@ module ActionDispatch
315
345
 
316
346
  def translate_controller(controller)
317
347
  return controller if Regexp === controller
318
- return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
348
+ return controller.to_s if /\A[a-z_0-9][a-z_0-9\/]*\z/.match?(controller)
319
349
 
320
350
  yield
321
351
  end
322
352
 
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 || []
353
+ def blocks(callable_constraint)
354
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
355
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
329
356
  end
357
+ [callable_constraint]
330
358
  end
331
359
 
332
360
  def constraints(options, path_params)
333
- constraints = {}
334
- required_defaults = []
335
- options.each_pair do |key, option|
361
+ options.group_by do |key, option|
336
362
  if Regexp === option
337
- constraints[key] = option
363
+ :constraints
338
364
  else
339
- required_defaults << key unless path_params.include?(key)
365
+ if path_params.include?(key)
366
+ :path_params
367
+ else
368
+ :required_defaults
369
+ end
340
370
  end
341
371
  end
342
- @conditions[:required_defaults] = required_defaults
343
- constraints
344
372
  end
345
373
 
346
- def path_params(ast)
347
- ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
374
+ def dispatcher(raise_on_name_error)
375
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
348
376
  end
349
377
 
350
- def path_ast(path)
351
- parser = Journey::Parser.new
352
- parser.parse path
353
- end
378
+ if Thread.respond_to?(:each_caller_location)
379
+ def route_source_location
380
+ if Mapper.route_source_locations
381
+ action_dispatch_dir = File.expand_path("..", __dir__)
382
+ Thread.each_caller_location do |location|
383
+ next if location.path.start_with?(action_dispatch_dir)
384
+
385
+ cleaned_path = Mapper.backtrace_cleaner.clean_frame(location.path)
386
+ next if cleaned_path.nil?
387
+
388
+ return "#{cleaned_path}:#{location.lineno}"
389
+ end
390
+ nil
391
+ end
392
+ end
393
+ else
394
+ def route_source_location
395
+ if Mapper.route_source_locations
396
+ action_dispatch_dir = File.expand_path("..", __dir__)
397
+ caller_locations.each do |location|
398
+ next if location.path.start_with?(action_dispatch_dir)
399
+
400
+ cleaned_path = Mapper.backtrace_cleaner.clean_frame(location.path)
401
+ next if cleaned_path.nil?
354
402
 
355
- def dispatcher(defaults)
356
- @set.dispatcher defaults
403
+ return "#{cleaned_path}:#{location.lineno}"
404
+ end
405
+ nil
406
+ end
407
+ end
357
408
  end
358
409
  end
359
410
 
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.
411
+ # Invokes Journey::Router::Utils.normalize_path, then ensures that /(:locale)
412
+ # becomes (/:locale). Except for root cases, where the former is the correct
413
+ # one.
363
414
  def self.normalize_path(path)
364
415
  path = Journey::Router::Utils.normalize_path(path)
365
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
416
+
417
+ # the path for a root URL at this point can be something like
418
+ # "/(/:locale)(/:platform)/(:browser)", and we would want
419
+ # "/(:locale)(/:platform)(/:browser)" reverse "/(", "/((" etc to "(/", "((/" etc
420
+ path.gsub!(%r{/(\(+)/?}, '\1/')
421
+ # if a path is all optional segments, change the leading "(/" back to "/(" so it
422
+ # evaluates to "/" when interpreted with no options. Unless, however, at least
423
+ # one secondary segment consists of a static part, ex.
424
+ # "(/:locale)(/pages/:page)"
425
+ path.sub!(%r{^(\(+)/}, '/\1') if %r{^(\(+[^)]+\))(\(+/:[^)]+\))*$}.match?(path)
366
426
  path
367
427
  end
368
428
 
@@ -371,214 +431,221 @@ module ActionDispatch
371
431
  end
372
432
 
373
433
  module Base
374
- # You can specify what Rails should route "/" to with the root method:
434
+ # Matches a URL pattern to one or more routes.
375
435
  #
376
- # root to: 'pages#main'
436
+ # You should not use the `match` method in your router without specifying an
437
+ # HTTP method.
377
438
  #
378
- # For options, see +match+, as +root+ uses it internally.
439
+ # If you want to expose your action to both GET and POST, use:
379
440
  #
380
- # You can also pass a string which will expand
441
+ # # sets :controller, :action, and :id in params
442
+ # match ':controller/:action/:id', via: [:get, :post]
381
443
  #
382
- # root 'pages#main'
444
+ # Note that `:controller`, `:action`, and `:id` are interpreted as URL query
445
+ # parameters and thus available through `params` in an action.
383
446
  #
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.
447
+ # If you want to expose your action to GET, use `get` in the router:
392
448
  #
393
- # You should not use the +match+ method in your router
394
- # without specifying an HTTP method.
449
+ # Instead of:
395
450
  #
396
- # If you want to expose your action to both GET and POST, use:
451
+ # match ":controller/:action/:id"
452
+ #
453
+ # Do:
397
454
  #
398
- # # sets :controller, :action and :id in params
399
- # match ':controller/:action/:id', via: [:get, :post]
455
+ # get ":controller/:action/:id"
400
456
  #
401
- # Note that +:controller+, +:action+ and +:id+ are interpreted as url
402
- # query parameters and thus available through +params+ in an action.
457
+ # Two of these symbols are special, `:controller` maps to the controller and
458
+ # `:action` to the controller's action. A pattern can also map wildcard segments
459
+ # (globs) to params:
403
460
  #
404
- # If you want to expose your action to GET, use +get+ in the router:
461
+ # get 'songs/*category/:title', to: 'songs#show'
405
462
  #
406
- # Instead of:
463
+ # # 'songs/rock/classic/stairway-to-heaven' sets
464
+ # # params[:category] = 'rock/classic'
465
+ # # params[:title] = 'stairway-to-heaven'
407
466
  #
408
- # match ":controller/:action/:id"
467
+ # To match a wildcard parameter, it must have a name assigned to it. Without a
468
+ # variable name to attach the glob parameter to, the route can't be parsed.
409
469
  #
410
- # Do:
470
+ # When a pattern points to an internal route, the route's `:action` and
471
+ # `:controller` should be set in options or hash shorthand. Examples:
411
472
  #
412
- # get ":controller/:action/:id"
473
+ # match 'photos/:id' => 'photos#show', via: :get
474
+ # match 'photos/:id', to: 'photos#show', via: :get
475
+ # match 'photos/:id', controller: 'photos', action: 'show', via: :get
413
476
  #
414
- # Two of these symbols are special, +:controller+ maps to the controller
415
- # and +:action+ to the controller's action. A pattern can also map
416
- # wildcard segments (globs) to params:
477
+ # A pattern can also point to a `Rack` endpoint i.e. anything that responds to
478
+ # `call`:
417
479
  #
418
- # get 'songs/*category/:title', to: 'songs#show'
480
+ # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
481
+ # match 'photos/:id', to: PhotoRackApp, via: :get
482
+ # # Yes, controller actions are just rack endpoints
483
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
419
484
  #
420
- # # 'songs/rock/classic/stairway-to-heaven' sets
421
- # # params[:category] = 'rock/classic'
422
- # # params[:title] = 'stairway-to-heaven'
485
+ # Because requesting various HTTP verbs with a single action has security
486
+ # implications, you must either specify the actions in the via options or use
487
+ # one of the [HttpHelpers](rdoc-ref:HttpHelpers) instead `match`
423
488
  #
424
- # To match a wildcard parameter, it must have a name assigned to it.
425
- # Without a variable name to attach the glob parameter to, the route
426
- # can't be parsed.
489
+ # ### Options
427
490
  #
428
- # When a pattern points to an internal route, the route's +:action+ and
429
- # +:controller+ should be set in options or hash shorthand. Examples:
491
+ # Any options not seen here are passed on as params with the URL.
430
492
  #
431
- # match 'photos/:id' => 'photos#show', via: :get
432
- # match 'photos/:id', to: 'photos#show', via: :get
433
- # match 'photos/:id', controller: 'photos', action: 'show', via: :get
493
+ # :controller
494
+ # : The route's controller.
434
495
  #
435
- # A pattern can also point to a +Rack+ endpoint i.e. anything that
436
- # responds to +call+:
496
+ # :action
497
+ # : The route's action.
437
498
  #
438
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
439
- # match 'photos/:id', to: PhotoRackApp, via: :get
440
- # # Yes, controller actions are just rack endpoints
441
- # match 'photos/:id', to: PhotosController.action(:show), via: :get
499
+ # :param
500
+ # : Overrides the default resource identifier `:id` (name of the dynamic
501
+ # segment used to generate the routes). You can access that segment from
502
+ # your controller using `params[<:param>]`. In your router:
442
503
  #
443
- # Because requesting various HTTP verbs with a single action has security
444
- # implications, you must either specify the actions in
445
- # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
446
- # instead +match+
504
+ # resources :users, param: :name
447
505
  #
448
- # === Options
506
+ # The `users` resource here will have the following routes generated for it:
449
507
  #
450
- # Any options not seen here are passed on as params with the url.
508
+ # GET /users(.:format)
509
+ # POST /users(.:format)
510
+ # GET /users/new(.:format)
511
+ # GET /users/:name/edit(.:format)
512
+ # GET /users/:name(.:format)
513
+ # PATCH/PUT /users/:name(.:format)
514
+ # DELETE /users/:name(.:format)
451
515
  #
452
- # [:controller]
453
- # The route's controller.
516
+ # You can override `ActiveRecord::Base#to_param` of a related model to
517
+ # construct a URL:
454
518
  #
455
- # [:action]
456
- # The route's action.
519
+ # class User < ActiveRecord::Base
520
+ # def to_param
521
+ # name
522
+ # end
523
+ # end
457
524
  #
458
- # [:param]
459
- # Overrides the default resource identifier +:id+ (name of the
460
- # dynamic segment used to generate the routes).
461
- # You can access that segment from your controller using
462
- # <tt>params[<:param>]</tt>.
525
+ # user = User.find_by(name: 'Phusion')
526
+ # user_path(user) # => "/users/Phusion"
463
527
  #
464
- # [:path]
465
- # The path prefix for the routes.
528
+ # :path
529
+ # : The path prefix for the routes.
466
530
  #
467
- # [:module]
468
- # The namespace for :controller.
531
+ # :module
532
+ # : The namespace for :controller.
469
533
  #
470
- # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
471
- # # => Sekret::PostsController
534
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
535
+ # # => Sekret::PostsController
472
536
  #
473
- # See <tt>Scoping#namespace</tt> for its scope equivalent.
537
+ # See `Scoping#namespace` for its scope equivalent.
474
538
  #
475
- # [:as]
476
- # The name used to generate routing helpers.
539
+ # :as
540
+ # : The name used to generate routing helpers.
477
541
  #
478
- # [:via]
479
- # Allowed HTTP verb(s) for route.
542
+ # :via
543
+ # : Allowed HTTP verb(s) for route.
480
544
  #
481
- # match 'path', to: 'c#a', via: :get
482
- # match 'path', to: 'c#a', via: [:get, :post]
483
- # match 'path', to: 'c#a', via: :all
545
+ # match 'path', to: 'c#a', via: :get
546
+ # match 'path', to: 'c#a', via: [:get, :post]
547
+ # match 'path', to: 'c#a', via: :all
484
548
  #
485
- # [:to]
486
- # Points to a +Rack+ endpoint. Can be an object that responds to
487
- # +call+ or a string representing a controller's action.
549
+ # :to
550
+ # : Points to a `Rack` endpoint. Can be an object that responds to `call` or a
551
+ # string representing a controller's action.
488
552
  #
489
- # match 'path', to: 'controller#action', via: :get
490
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
491
- # match 'path', to: RackApp, via: :get
553
+ # match 'path', to: 'controller#action', via: :get
554
+ # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
555
+ # match 'path', to: RackApp, via: :get
492
556
  #
493
- # [:on]
494
- # Shorthand for wrapping routes in a specific RESTful context. Valid
495
- # values are +:member+, +:collection+, and +:new+. Only use within
496
- # <tt>resource(s)</tt> block. For example:
557
+ # :on
558
+ # : Shorthand for wrapping routes in a specific RESTful context. Valid values
559
+ # are `:member`, `:collection`, and `:new`. Only use within `resource(s)`
560
+ # block. For example:
497
561
  #
498
- # resource :bar do
499
- # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
500
- # end
562
+ # resource :bar do
563
+ # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
564
+ # end
501
565
  #
502
- # Is equivalent to:
566
+ # Is equivalent to:
503
567
  #
504
- # resource :bar do
505
- # member do
506
- # match 'foo', to: 'c#a', via: [:get, :post]
507
- # end
508
- # end
568
+ # resource :bar do
569
+ # member do
570
+ # match 'foo', to: 'c#a', via: [:get, :post]
571
+ # end
572
+ # end
509
573
  #
510
- # [:constraints]
511
- # Constrains parameters with a hash of regular expressions
512
- # or an object that responds to <tt>matches?</tt>. In addition, constraints
513
- # other than path can also be specified with any object
514
- # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
574
+ # :constraints
575
+ # : Constrains parameters with a hash of regular expressions or an object that
576
+ # responds to `matches?`. In addition, constraints other than path can also
577
+ # be specified with any object that responds to `===` (e.g. String, Array,
578
+ # Range, etc.).
515
579
  #
516
- # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
580
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
517
581
  #
518
- # match 'json_only', constraints: { format: 'json' }, via: :get
582
+ # match 'json_only', constraints: { format: 'json' }, via: :get
519
583
  #
520
- # class Whitelist
521
- # def matches?(request) request.remote_ip == '1.2.3.4' end
522
- # end
523
- # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
584
+ # class PermitList
585
+ # def matches?(request) request.remote_ip == '1.2.3.4' end
586
+ # end
587
+ # match 'path', to: 'c#a', constraints: PermitList.new, via: :get
524
588
  #
525
- # See <tt>Scoping#constraints</tt> for more examples with its scope
526
- # equivalent.
589
+ # See `Scoping#constraints` for more examples with its scope equivalent.
527
590
  #
528
- # [:defaults]
529
- # Sets defaults for parameters
591
+ # :defaults
592
+ # : Sets defaults for parameters
530
593
  #
531
- # # Sets params[:format] to 'jpg' by default
532
- # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
594
+ # # Sets params[:format] to 'jpg' by default
595
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
533
596
  #
534
- # See <tt>Scoping#defaults</tt> for its scope equivalent.
597
+ # See `Scoping#defaults` for its scope equivalent.
535
598
  #
536
- # [:anchor]
537
- # Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
538
- # false, the pattern matches any request prefixed with the given path.
599
+ # :anchor
600
+ # : Boolean to anchor a `match` pattern. Default is true. When set to false,
601
+ # the pattern matches any request prefixed with the given path.
539
602
  #
540
- # # Matches any request starting with 'path'
541
- # match 'path', to: 'c#a', anchor: false, via: :get
603
+ # # Matches any request starting with 'path'
604
+ # match 'path', to: 'c#a', anchor: false, via: :get
542
605
  #
543
- # [:format]
544
- # Allows you to specify the default value for optional +format+
545
- # segment or disable it by supplying +false+.
546
- def match(path, options=nil)
606
+ # :format
607
+ # : Allows you to specify the default value for optional `format` segment or
608
+ # disable it by supplying `false`.
609
+ #
610
+ def match(path, options = nil)
547
611
  end
548
612
 
549
613
  # Mount a Rack-based application to be used within the application.
550
614
  #
551
- # mount SomeRackApp, at: "some_route"
615
+ # mount SomeRackApp, at: "some_route"
552
616
  #
553
617
  # Alternatively:
554
618
  #
555
- # mount(SomeRackApp => "some_route")
619
+ # mount(SomeRackApp => "some_route")
556
620
  #
557
- # For options, see +match+, as +mount+ uses it internally.
621
+ # For options, see `match`, as `mount` uses it internally.
558
622
  #
559
- # All mounted applications come with routing helpers to access them.
560
- # These are named after the class specified, so for the above example
561
- # the helper is either +some_rack_app_path+ or +some_rack_app_url+.
562
- # To customize this helper's name, use the +:as+ option:
623
+ # All mounted applications come with routing helpers to access them. These are
624
+ # named after the class specified, so for the above example the helper is either
625
+ # `some_rack_app_path` or `some_rack_app_url`. To customize this helper's name,
626
+ # use the `:as` option:
563
627
  #
564
- # mount(SomeRackApp => "some_route", as: "exciting")
628
+ # mount(SomeRackApp => "some_route", as: "exciting")
565
629
  #
566
- # This will generate the +exciting_path+ and +exciting_url+ helpers
567
- # which can be used to navigate to this mounted app.
630
+ # This will generate the `exciting_path` and `exciting_url` helpers which can be
631
+ # used to navigate to this mounted app.
568
632
  def mount(app, options = nil)
569
633
  if options
570
634
  path = options.delete(:at)
571
- else
572
- unless Hash === app
573
- raise ArgumentError, "must be called with mount point"
574
- end
575
-
635
+ elsif Hash === app
576
636
  options = app
577
637
  app, path = options.find { |k, _| k.respond_to?(:call) }
578
638
  options.delete(app) if app
579
639
  end
580
640
 
581
- raise "A rack application must be specified" unless path
641
+ raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
642
+ raise ArgumentError, <<~MSG unless path
643
+ Must be called with mount point
644
+
645
+ mount SomeRackApp, at: "some_route"
646
+ or
647
+ mount(SomeRackApp => "some_route")
648
+ MSG
582
649
 
583
650
  rails_app = rails_app? app
584
651
  options[:as] ||= app_name(app, rails_app)
@@ -586,7 +653,7 @@ module ActionDispatch
586
653
  target_as = name_for_action(options[:as], path)
587
654
  options[:via] ||= :all
588
655
 
589
- match(path, options.merge(:to => app, :anchor => false, :format => false))
656
+ match(path, { to: app, anchor: false, format: false }.merge(options))
590
657
 
591
658
  define_generate_prefix(app, target_as) if rails_app
592
659
  self
@@ -605,7 +672,7 @@ module ActionDispatch
605
672
 
606
673
  # Query if the following named route was already defined.
607
674
  def has_named_route?(name)
608
- @set.named_routes.routes[name.to_sym]
675
+ @set.named_routes.key?(name)
609
676
  end
610
677
 
611
678
  private
@@ -625,18 +692,32 @@ module ActionDispatch
625
692
  def define_generate_prefix(app, name)
626
693
  _route = @set.named_routes.get name
627
694
  _routes = @set
628
- app.routes.define_mounted_helper(name)
695
+ _url_helpers = @set.url_helpers
696
+
697
+ script_namer = ->(options) do
698
+ prefix_options = options.slice(*_route.segment_keys)
699
+ prefix_options[:script_name] = "" if options[:original_script_name]
700
+
701
+ if options[:_recall]
702
+ prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
703
+ end
704
+
705
+ # We must actually delete prefix segment keys to avoid passing them to next
706
+ # url_for.
707
+ _route.segment_keys.each { |k| options.delete(k) }
708
+ _url_helpers.public_send("#{name}_path", prefix_options)
709
+ end
710
+
711
+ app.routes.define_mounted_helper(name, script_namer)
712
+
629
713
  app.routes.extend Module.new {
630
714
  def optimize_routes_generation?; false; end
715
+
631
716
  define_method :find_script_name do |options|
632
- if options.key? :script_name
717
+ if options.key?(:script_name) && options[:script_name].present?
633
718
  super(options)
634
719
  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)
720
+ script_namer.call(options)
640
721
  end
641
722
  end
642
723
  }
@@ -644,46 +725,54 @@ module ActionDispatch
644
725
  end
645
726
 
646
727
  module HttpHelpers
647
- # Define a route that only recognizes HTTP GET.
648
- # For supported arguments, see match[rdoc-ref:Base#match]
728
+ # Define a route that only recognizes HTTP GET. For supported arguments, see
729
+ # [match](rdoc-ref:Base#match)
649
730
  #
650
- # get 'bacon', to: 'food#bacon'
731
+ # get 'bacon', to: 'food#bacon'
651
732
  def get(*args, &block)
652
733
  map_method(:get, args, &block)
653
734
  end
654
735
 
655
- # Define a route that only recognizes HTTP POST.
656
- # For supported arguments, see match[rdoc-ref:Base#match]
736
+ # Define a route that only recognizes HTTP POST. For supported arguments, see
737
+ # [match](rdoc-ref:Base#match)
657
738
  #
658
- # post 'bacon', to: 'food#bacon'
739
+ # post 'bacon', to: 'food#bacon'
659
740
  def post(*args, &block)
660
741
  map_method(:post, args, &block)
661
742
  end
662
743
 
663
- # Define a route that only recognizes HTTP PATCH.
664
- # For supported arguments, see match[rdoc-ref:Base#match]
744
+ # Define a route that only recognizes HTTP PATCH. For supported arguments, see
745
+ # [match](rdoc-ref:Base#match)
665
746
  #
666
- # patch 'bacon', to: 'food#bacon'
747
+ # patch 'bacon', to: 'food#bacon'
667
748
  def patch(*args, &block)
668
749
  map_method(:patch, args, &block)
669
750
  end
670
751
 
671
- # Define a route that only recognizes HTTP PUT.
672
- # For supported arguments, see match[rdoc-ref:Base#match]
752
+ # Define a route that only recognizes HTTP PUT. For supported arguments, see
753
+ # [match](rdoc-ref:Base#match)
673
754
  #
674
- # put 'bacon', to: 'food#bacon'
755
+ # put 'bacon', to: 'food#bacon'
675
756
  def put(*args, &block)
676
757
  map_method(:put, args, &block)
677
758
  end
678
759
 
679
- # Define a route that only recognizes HTTP DELETE.
680
- # For supported arguments, see match[rdoc-ref:Base#match]
760
+ # Define a route that only recognizes HTTP DELETE. For supported arguments, see
761
+ # [match](rdoc-ref:Base#match)
681
762
  #
682
- # delete 'broccoli', to: 'food#broccoli'
763
+ # delete 'broccoli', to: 'food#broccoli'
683
764
  def delete(*args, &block)
684
765
  map_method(:delete, args, &block)
685
766
  end
686
767
 
768
+ # Define a route that only recognizes HTTP OPTIONS. For supported arguments, see
769
+ # [match](rdoc-ref:Base#match)
770
+ #
771
+ # options 'carrots', to: 'food#carrots'
772
+ def options(*args, &block)
773
+ map_method(:options, args, &block)
774
+ end
775
+
687
776
  private
688
777
  def map_method(method, args, &block)
689
778
  options = args.extract_options!
@@ -693,96 +782,95 @@ module ActionDispatch
693
782
  end
694
783
  end
695
784
 
696
- # You may wish to organize groups of controllers under a namespace.
697
- # Most commonly, you might group a number of administrative controllers
698
- # under an +admin+ namespace. You would place these controllers under
699
- # the <tt>app/controllers/admin</tt> directory, and you can group them
700
- # together in your router:
785
+ # You may wish to organize groups of controllers under a namespace. Most
786
+ # commonly, you might group a number of administrative controllers under an
787
+ # `admin` namespace. You would place these controllers under the
788
+ # `app/controllers/admin` directory, and you can group them together in your
789
+ # router:
701
790
  #
702
- # namespace "admin" do
703
- # resources :posts, :comments
704
- # end
791
+ # namespace "admin" do
792
+ # resources :posts, :comments
793
+ # end
705
794
  #
706
795
  # This will create a number of routes for each of the posts and comments
707
- # controller. For <tt>Admin::PostsController</tt>, Rails will create:
796
+ # controller. For `Admin::PostsController`, Rails will create:
708
797
  #
709
- # GET /admin/posts
710
- # GET /admin/posts/new
711
- # POST /admin/posts
712
- # GET /admin/posts/1
713
- # GET /admin/posts/1/edit
714
- # PATCH/PUT /admin/posts/1
715
- # DELETE /admin/posts/1
798
+ # GET /admin/posts
799
+ # GET /admin/posts/new
800
+ # POST /admin/posts
801
+ # GET /admin/posts/1
802
+ # GET /admin/posts/1/edit
803
+ # PATCH/PUT /admin/posts/1
804
+ # DELETE /admin/posts/1
716
805
  #
717
806
  # If you want to route /posts (without the prefix /admin) to
718
- # <tt>Admin::PostsController</tt>, you could use
807
+ # `Admin::PostsController`, you could use
719
808
  #
720
- # scope module: "admin" do
721
- # resources :posts
722
- # end
809
+ # scope module: "admin" do
810
+ # resources :posts
811
+ # end
723
812
  #
724
813
  # or, for a single case
725
814
  #
726
- # resources :posts, module: "admin"
815
+ # resources :posts, module: "admin"
727
816
  #
728
- # If you want to route /admin/posts to +PostsController+
729
- # (without the <tt>Admin::</tt> module prefix), you could use
817
+ # If you want to route /admin/posts to `PostsController` (without the `Admin::`
818
+ # module prefix), you could use
730
819
  #
731
- # scope "/admin" do
732
- # resources :posts
733
- # end
820
+ # scope "/admin" do
821
+ # resources :posts
822
+ # end
734
823
  #
735
824
  # or, for a single case
736
825
  #
737
- # resources :posts, path: "/admin/posts"
826
+ # resources :posts, path: "/admin/posts"
738
827
  #
739
- # In each of these cases, the named routes remain the same as if you did
740
- # not use scope. In the last case, the following paths map to
741
- # +PostsController+:
828
+ # In each of these cases, the named routes remain the same as if you did not use
829
+ # scope. In the last case, the following paths map to `PostsController`:
742
830
  #
743
- # GET /admin/posts
744
- # GET /admin/posts/new
745
- # POST /admin/posts
746
- # GET /admin/posts/1
747
- # GET /admin/posts/1/edit
748
- # PATCH/PUT /admin/posts/1
749
- # DELETE /admin/posts/1
831
+ # GET /admin/posts
832
+ # GET /admin/posts/new
833
+ # POST /admin/posts
834
+ # GET /admin/posts/1
835
+ # GET /admin/posts/1/edit
836
+ # PATCH/PUT /admin/posts/1
837
+ # DELETE /admin/posts/1
750
838
  module Scoping
751
839
  # Scopes a set of routes to the given default options.
752
840
  #
753
841
  # Take the following route definition as an example:
754
842
  #
755
- # scope path: ":account_id", as: "account" do
756
- # resources :projects
757
- # end
843
+ # scope path: ":account_id", as: "account" do
844
+ # resources :projects
845
+ # end
758
846
  #
759
- # This generates helpers such as +account_projects_path+, just like +resources+ does.
760
- # The difference here being that the routes generated are like /:account_id/projects,
761
- # rather than /accounts/:account_id/projects.
847
+ # This generates helpers such as `account_projects_path`, just like `resources`
848
+ # does. The difference here being that the routes generated are like
849
+ # /:account_id/projects, rather than /accounts/:account_id/projects.
762
850
  #
763
- # === Options
851
+ # ### Options
764
852
  #
765
- # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
853
+ # Takes same options as `Base#match` and `Resources#resources`.
766
854
  #
767
- # # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
768
- # scope module: "admin" do
769
- # resources :posts
770
- # end
855
+ # # route /posts (without the prefix /admin) to +Admin::PostsController+
856
+ # scope module: "admin" do
857
+ # resources :posts
858
+ # end
771
859
  #
772
- # # prefix the posts resource's requests with '/admin'
773
- # scope path: "/admin" do
774
- # resources :posts
775
- # end
860
+ # # prefix the posts resource's requests with '/admin'
861
+ # scope path: "/admin" do
862
+ # resources :posts
863
+ # end
776
864
  #
777
- # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
778
- # scope as: "sekret" do
779
- # resources :posts
780
- # end
865
+ # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
866
+ # scope as: "sekret" do
867
+ # resources :posts
868
+ # end
781
869
  def scope(*args)
782
870
  options = args.extract_options!.dup
783
871
  scope = {}
784
872
 
785
- options[:path] = args.flatten.join('/') if args.any?
873
+ options[:path] = args.flatten.join("/") if args.any?
786
874
  options[:constraints] ||= {}
787
875
 
788
876
  unless nested_scope?
@@ -795,21 +883,30 @@ module ActionDispatch
795
883
  URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
796
884
  end
797
885
 
798
- (options[:defaults] ||= {}).reverse_merge!(defaults)
886
+ options[:defaults] = defaults.merge(options[:defaults] || {})
799
887
  else
800
888
  block, options[:constraints] = options[:constraints], {}
801
889
  end
802
890
 
891
+ if options.key?(:only) || options.key?(:except)
892
+ scope[:action_options] = { only: options.delete(:only),
893
+ except: options.delete(:except) }
894
+ end
895
+
896
+ if options.key? :anchor
897
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
898
+ end
899
+
803
900
  @scope.options.each do |option|
804
901
  if option == :blocks
805
902
  value = block
806
903
  elsif option == :options
807
904
  value = options
808
905
  else
809
- value = options.delete(option)
906
+ value = options.delete(option) { POISON }
810
907
  end
811
908
 
812
- if value
909
+ unless POISON == value
813
910
  scope[option] = send("merge_#{option}_scope", @scope[option], value)
814
911
  end
815
912
  end
@@ -821,264 +918,299 @@ module ActionDispatch
821
918
  @scope = @scope.parent
822
919
  end
823
920
 
921
+ POISON = Object.new # :nodoc:
922
+
824
923
  # Scopes routes to a specific controller
825
924
  #
826
- # controller "food" do
827
- # match "bacon", action: "bacon"
828
- # end
829
- def controller(controller, options={})
830
- options[:controller] = controller
831
- scope(options) { yield }
925
+ # controller "food" do
926
+ # match "bacon", action: :bacon, via: :get
927
+ # end
928
+ def controller(controller)
929
+ @scope = @scope.new(controller: controller)
930
+ yield
931
+ ensure
932
+ @scope = @scope.parent
832
933
  end
833
934
 
834
935
  # Scopes routes to a specific namespace. For example:
835
936
  #
836
- # namespace :admin do
837
- # resources :posts
838
- # end
937
+ # namespace :admin do
938
+ # resources :posts
939
+ # end
839
940
  #
840
941
  # This generates the following routes:
841
942
  #
842
- # admin_posts GET /admin/posts(.:format) admin/posts#index
843
- # admin_posts POST /admin/posts(.:format) admin/posts#create
844
- # new_admin_post GET /admin/posts/new(.:format) admin/posts#new
845
- # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
846
- # admin_post GET /admin/posts/:id(.:format) admin/posts#show
847
- # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
848
- # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
943
+ # admin_posts GET /admin/posts(.:format) admin/posts#index
944
+ # admin_posts POST /admin/posts(.:format) admin/posts#create
945
+ # new_admin_post GET /admin/posts/new(.:format) admin/posts#new
946
+ # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
947
+ # admin_post GET /admin/posts/:id(.:format) admin/posts#show
948
+ # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
949
+ # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
849
950
  #
850
- # === Options
951
+ # ### Options
851
952
  #
852
- # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
853
- # options all default to the name of the namespace.
953
+ # The `:path`, `:as`, `:module`, `:shallow_path`, and `:shallow_prefix` options
954
+ # all default to the name of the namespace.
854
955
  #
855
- # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
856
- # <tt>Resources#resources</tt>.
956
+ # For options, see `Base#match`. For `:shallow_path` option, see
957
+ # `Resources#resources`.
857
958
  #
858
- # # accessible through /sekret/posts rather than /admin/posts
859
- # namespace :admin, path: "sekret" do
860
- # resources :posts
861
- # end
959
+ # # accessible through /sekret/posts rather than /admin/posts
960
+ # namespace :admin, path: "sekret" do
961
+ # resources :posts
962
+ # end
862
963
  #
863
- # # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
864
- # namespace :admin, module: "sekret" do
865
- # resources :posts
866
- # end
964
+ # # maps to +Sekret::PostsController+ rather than +Admin::PostsController+
965
+ # namespace :admin, module: "sekret" do
966
+ # resources :posts
967
+ # end
867
968
  #
868
- # # generates +sekret_posts_path+ rather than +admin_posts_path+
869
- # namespace :admin, as: "sekret" do
870
- # resources :posts
871
- # end
872
- def namespace(path, options = {})
969
+ # # generates +sekret_posts_path+ rather than +admin_posts_path+
970
+ # namespace :admin, as: "sekret" do
971
+ # resources :posts
972
+ # end
973
+ def namespace(path, options = {}, &block)
873
974
  path = path.to_s
874
975
 
875
976
  defaults = {
876
977
  module: path,
877
- path: options.fetch(:path, path),
878
978
  as: options.fetch(:as, path),
879
979
  shallow_path: options.fetch(:path, path),
880
980
  shallow_prefix: options.fetch(:as, path)
881
981
  }
882
982
 
883
- scope(defaults.merge!(options)) { yield }
983
+ path_scope(options.delete(:path) { path }) do
984
+ scope(defaults.merge!(options), &block)
985
+ end
884
986
  end
885
987
 
886
- # === Parameter Restriction
887
- # Allows you to constrain the nested routes based on a set of rules.
888
- # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
988
+ # ### Parameter Restriction
989
+ # Allows you to constrain the nested routes based on a set of rules. For
990
+ # instance, in order to change the routes to allow for a dot character in the
991
+ # `id` parameter:
889
992
  #
890
- # constraints(id: /\d+\.\d+/) do
891
- # resources :posts
892
- # end
993
+ # constraints(id: /\d+\.\d+/) do
994
+ # resources :posts
995
+ # end
893
996
  #
894
- # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
895
- # The +id+ parameter must match the constraint passed in for this example.
997
+ # Now routes such as `/posts/1` will no longer be valid, but `/posts/1.1` will
998
+ # be. The `id` parameter must match the constraint passed in for this example.
896
999
  #
897
1000
  # You may use this to also restrict other parameters:
898
1001
  #
899
- # resources :posts do
900
- # constraints(post_id: /\d+\.\d+/) do
901
- # resources :comments
1002
+ # resources :posts do
1003
+ # constraints(post_id: /\d+\.\d+/) do
1004
+ # resources :comments
1005
+ # end
902
1006
  # end
903
- # end
904
1007
  #
905
- # === Restricting based on IP
1008
+ # ### Restricting based on IP
906
1009
  #
907
1010
  # Routes can also be constrained to an IP or a certain range of IP addresses:
908
1011
  #
909
- # constraints(ip: /192\.168\.\d+\.\d+/) do
910
- # resources :posts
911
- # end
1012
+ # constraints(ip: /192\.168\.\d+\.\d+/) do
1013
+ # resources :posts
1014
+ # end
912
1015
  #
913
- # Any user connecting from the 192.168.* range will be able to see this resource,
914
- # where as any user connecting outside of this range will be told there is no such route.
1016
+ # Any user connecting from the 192.168.* range will be able to see this
1017
+ # resource, where as any user connecting outside of this range will be told
1018
+ # there is no such route.
915
1019
  #
916
- # === Dynamic request matching
1020
+ # ### Dynamic request matching
917
1021
  #
918
1022
  # Requests to routes can be constrained based on specific criteria:
919
1023
  #
920
- # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
921
- # resources :iphones
922
- # end
1024
+ # constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do
1025
+ # resources :iphones
1026
+ # end
923
1027
  #
924
- # You are able to move this logic out into a class if it is too complex for routes.
925
- # This class must have a +matches?+ method defined on it which either returns +true+
926
- # if the user should be given access to that route, or +false+ if the user should not.
1028
+ # You are able to move this logic out into a class if it is too complex for
1029
+ # routes. This class must have a `matches?` method defined on it which either
1030
+ # returns `true` if the user should be given access to that route, or `false` if
1031
+ # the user should not.
927
1032
  #
928
- # class Iphone
929
- # def self.matches?(request)
930
- # request.env["HTTP_USER_AGENT"] =~ /iPhone/
931
- # end
932
- # end
1033
+ # class Iphone
1034
+ # def self.matches?(request)
1035
+ # /iPhone/.match?(request.env["HTTP_USER_AGENT"])
1036
+ # end
1037
+ # end
933
1038
  #
934
- # An expected place for this code would be +lib/constraints+.
1039
+ # An expected place for this code would be `lib/constraints`.
935
1040
  #
936
1041
  # This class is then used like this:
937
1042
  #
938
- # constraints(Iphone) do
939
- # resources :iphones
940
- # end
941
- def constraints(constraints = {})
942
- scope(:constraints => constraints) { yield }
1043
+ # constraints(Iphone) do
1044
+ # resources :iphones
1045
+ # end
1046
+ def constraints(constraints = {}, &block)
1047
+ scope(constraints: constraints, &block)
943
1048
  end
944
1049
 
945
1050
  # Allows you to set default parameters for a route, such as this:
946
- # defaults id: 'home' do
947
- # match 'scoped_pages/(:id)', to: 'pages#show'
948
- # end
949
- # Using this, the +:id+ parameter here will default to 'home'.
1051
+ # defaults id: 'home' do
1052
+ # match 'scoped_pages/(:id)', to: 'pages#show'
1053
+ # end
1054
+ #
1055
+ # Using this, the `:id` parameter here will default to 'home'.
950
1056
  def defaults(defaults = {})
951
- scope(:defaults => defaults) { yield }
1057
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
1058
+ yield
1059
+ ensure
1060
+ @scope = @scope.parent
952
1061
  end
953
1062
 
954
1063
  private
955
- def merge_path_scope(parent, child) #:nodoc:
1064
+ def merge_path_scope(parent, child)
956
1065
  Mapper.normalize_path("#{parent}/#{child}")
957
1066
  end
958
1067
 
959
- def merge_shallow_path_scope(parent, child) #:nodoc:
1068
+ def merge_shallow_path_scope(parent, child)
960
1069
  Mapper.normalize_path("#{parent}/#{child}")
961
1070
  end
962
1071
 
963
- def merge_as_scope(parent, child) #:nodoc:
1072
+ def merge_as_scope(parent, child)
964
1073
  parent ? "#{parent}_#{child}" : child
965
1074
  end
966
1075
 
967
- def merge_shallow_prefix_scope(parent, child) #:nodoc:
1076
+ def merge_shallow_prefix_scope(parent, child)
968
1077
  parent ? "#{parent}_#{child}" : child
969
1078
  end
970
1079
 
971
- def merge_module_scope(parent, child) #:nodoc:
1080
+ def merge_module_scope(parent, child)
972
1081
  parent ? "#{parent}/#{child}" : child
973
1082
  end
974
1083
 
975
- def merge_controller_scope(parent, child) #:nodoc:
1084
+ def merge_controller_scope(parent, child)
1085
+ child
1086
+ end
1087
+
1088
+ def merge_action_scope(parent, child)
1089
+ child
1090
+ end
1091
+
1092
+ def merge_via_scope(parent, child)
976
1093
  child
977
1094
  end
978
1095
 
979
- def merge_action_scope(parent, child) #:nodoc:
1096
+ def merge_format_scope(parent, child)
980
1097
  child
981
1098
  end
982
1099
 
983
- def merge_path_names_scope(parent, child) #:nodoc:
1100
+ def merge_path_names_scope(parent, child)
984
1101
  merge_options_scope(parent, child)
985
1102
  end
986
1103
 
987
- def merge_constraints_scope(parent, child) #:nodoc:
1104
+ def merge_constraints_scope(parent, child)
988
1105
  merge_options_scope(parent, child)
989
1106
  end
990
1107
 
991
- def merge_defaults_scope(parent, child) #:nodoc:
1108
+ def merge_defaults_scope(parent, child)
992
1109
  merge_options_scope(parent, child)
993
1110
  end
994
1111
 
995
- def merge_blocks_scope(parent, child) #:nodoc:
1112
+ def merge_blocks_scope(parent, child)
996
1113
  merged = parent ? parent.dup : []
997
1114
  merged << child if child
998
1115
  merged
999
1116
  end
1000
1117
 
1001
- def merge_options_scope(parent, child) #:nodoc:
1002
- (parent || {}).except(*override_keys(child)).merge!(child)
1118
+ def merge_options_scope(parent, child)
1119
+ (parent || {}).merge(child)
1003
1120
  end
1004
1121
 
1005
- def merge_shallow_scope(parent, child) #:nodoc:
1122
+ def merge_shallow_scope(parent, child)
1006
1123
  child ? true : false
1007
1124
  end
1008
1125
 
1009
- def override_keys(child) #:nodoc:
1010
- child.key?(:only) || child.key?(:except) ? [:only, :except] : []
1126
+ def merge_to_scope(parent, child)
1127
+ child
1011
1128
  end
1012
1129
  end
1013
1130
 
1014
- # Resource routing allows you to quickly declare all of the common routes
1015
- # for a given resourceful controller. Instead of declaring separate routes
1016
- # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
1017
- # actions, a resourceful route declares them in a single line of code:
1131
+ # Resource routing allows you to quickly declare all of the common routes for a
1132
+ # given resourceful controller. Instead of declaring separate routes for your
1133
+ # `index`, `show`, `new`, `edit`, `create`, `update`, and `destroy` actions, a
1134
+ # resourceful route declares them in a single line of code:
1018
1135
  #
1019
- # resources :photos
1136
+ # resources :photos
1020
1137
  #
1021
- # Sometimes, you have a resource that clients always look up without
1022
- # referencing an ID. A common example, /profile always shows the profile of
1023
- # the currently logged in user. In this case, you can use a singular resource
1024
- # to map /profile (rather than /profile/:id) to the show action.
1138
+ # Sometimes, you have a resource that clients always look up without referencing
1139
+ # an ID. A common example, /profile always shows the profile of the currently
1140
+ # logged in user. In this case, you can use a singular resource to map /profile
1141
+ # (rather than /profile/:id) to the show action.
1025
1142
  #
1026
- # resource :profile
1143
+ # resource :profile
1027
1144
  #
1028
- # It's common to have resources that are logically children of other
1029
- # resources:
1145
+ # It's common to have resources that are logically children of other resources:
1030
1146
  #
1031
- # resources :magazines do
1032
- # resources :ads
1033
- # end
1147
+ # resources :magazines do
1148
+ # resources :ads
1149
+ # end
1034
1150
  #
1035
1151
  # You may wish to organize groups of controllers under a namespace. Most
1036
- # commonly, you might group a number of administrative controllers under
1037
- # an +admin+ namespace. You would place these controllers under the
1038
- # <tt>app/controllers/admin</tt> directory, and you can group them together
1039
- # in your router:
1152
+ # commonly, you might group a number of administrative controllers under an
1153
+ # `admin` namespace. You would place these controllers under the
1154
+ # `app/controllers/admin` directory, and you can group them together in your
1155
+ # router:
1040
1156
  #
1041
- # namespace "admin" do
1042
- # resources :posts, :comments
1043
- # end
1157
+ # namespace "admin" do
1158
+ # resources :posts, :comments
1159
+ # end
1044
1160
  #
1045
- # By default the +:id+ parameter doesn't accept dots. If you need to
1046
- # use dots as part of the +:id+ parameter add a constraint which
1047
- # overrides this restriction, e.g:
1161
+ # By default the `:id` parameter doesn't accept dots. If you need to use dots as
1162
+ # part of the `:id` parameter add a constraint which overrides this restriction,
1163
+ # e.g:
1048
1164
  #
1049
- # resources :articles, id: /[^\/]+/
1050
- #
1051
- # This allows any character other than a slash as part of your +:id+.
1165
+ # resources :articles, id: /[^\/]+/
1052
1166
  #
1167
+ # This allows any character other than a slash as part of your `:id`.
1053
1168
  module Resources
1054
- # CANONICAL_ACTIONS holds all actions that does not need a prefix or
1055
- # a path appended since they fit properly in their scope level.
1169
+ # CANONICAL_ACTIONS holds all actions that does not need a prefix or a path
1170
+ # appended since they fit properly in their scope level.
1056
1171
  VALID_ON_OPTIONS = [:new, :collection, :member]
1057
1172
  RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
1058
1173
  CANONICAL_ACTIONS = %w(index create new show update destroy)
1059
1174
 
1060
- class Resource #:nodoc:
1061
- attr_reader :controller, :path, :options, :param
1175
+ class Resource # :nodoc:
1176
+ attr_reader :controller, :path, :param
1177
+
1178
+ def initialize(entities, api_only, shallow, options = {})
1179
+ if options[:param].to_s.include?(":")
1180
+ raise ArgumentError, ":param option can't contain colons"
1181
+ end
1062
1182
 
1063
- def initialize(entities, options = {})
1064
1183
  @name = entities.to_s
1065
1184
  @path = (options[:path] || @name).to_s
1066
1185
  @controller = (options[:controller] || @name).to_s
1067
1186
  @as = options[:as]
1068
1187
  @param = (options[:param] || :id).to_sym
1069
1188
  @options = options
1070
- @shallow = false
1189
+ @shallow = shallow
1190
+ @api_only = api_only
1191
+ @only = options.delete :only
1192
+ @except = options.delete :except
1071
1193
  end
1072
1194
 
1073
1195
  def default_actions
1074
- [:index, :create, :new, :show, :update, :destroy, :edit]
1196
+ if @api_only
1197
+ [:index, :create, :show, :update, :destroy]
1198
+ else
1199
+ [:index, :create, :new, :show, :update, :destroy, :edit]
1200
+ end
1075
1201
  end
1076
1202
 
1077
1203
  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)
1204
+ if @except
1205
+ available_actions - Array(@except).map(&:to_sym)
1206
+ else
1207
+ available_actions
1208
+ end
1209
+ end
1210
+
1211
+ def available_actions
1212
+ if @only
1213
+ Array(@only).map(&:to_sym)
1082
1214
  else
1083
1215
  default_actions
1084
1216
  end
@@ -1098,14 +1230,14 @@ module ActionDispatch
1098
1230
 
1099
1231
  alias :member_name :singular
1100
1232
 
1101
- # Checks for uncountable plurals, and appends "_index" if the plural
1102
- # and singular form are the same.
1233
+ # Checks for uncountable plurals, and appends "_index" if the plural and
1234
+ # singular form are the same.
1103
1235
  def collection_name
1104
1236
  singular == plural ? "#{plural}_index" : plural
1105
1237
  end
1106
1238
 
1107
1239
  def resource_scope
1108
- { :controller => controller }
1240
+ controller
1109
1241
  end
1110
1242
 
1111
1243
  alias :collection_scope :path
@@ -1128,17 +1260,15 @@ module ActionDispatch
1128
1260
  "#{path}/:#{nested_param}"
1129
1261
  end
1130
1262
 
1131
- def shallow=(value)
1132
- @shallow = value
1133
- end
1134
-
1135
1263
  def shallow?
1136
1264
  @shallow
1137
1265
  end
1266
+
1267
+ def singleton?; false; end
1138
1268
  end
1139
1269
 
1140
- class SingletonResource < Resource #:nodoc:
1141
- def initialize(entities, options)
1270
+ class SingletonResource < Resource # :nodoc:
1271
+ def initialize(entities, api_only, shallow, options)
1142
1272
  super
1143
1273
  @as = nil
1144
1274
  @controller = (options[:controller] || plural).to_s
@@ -1146,7 +1276,11 @@ module ActionDispatch
1146
1276
  end
1147
1277
 
1148
1278
  def default_actions
1149
- [:show, :create, :update, :destroy, :new, :edit]
1279
+ if @api_only
1280
+ [:show, :create, :update, :destroy]
1281
+ else
1282
+ [:show, :create, :update, :destroy, :new, :edit]
1283
+ end
1150
1284
  end
1151
1285
 
1152
1286
  def plural
@@ -1162,33 +1296,43 @@ module ActionDispatch
1162
1296
 
1163
1297
  alias :member_scope :path
1164
1298
  alias :nested_scope :path
1299
+
1300
+ def singleton?; true; end
1165
1301
  end
1166
1302
 
1167
1303
  def resources_path_names(options)
1168
1304
  @scope[:path_names].merge!(options)
1169
1305
  end
1170
1306
 
1171
- # Sometimes, you have a resource that clients always look up without
1172
- # referencing an ID. A common example, /profile always shows the
1173
- # profile of the currently logged in user. In this case, you can use
1174
- # a singular resource to map /profile (rather than /profile/:id) to
1175
- # the show action:
1307
+ # Sometimes, you have a resource that clients always look up without referencing
1308
+ # an ID. A common example, /profile always shows the profile of the currently
1309
+ # logged in user. In this case, you can use a singular resource to map /profile
1310
+ # (rather than /profile/:id) to the show action:
1311
+ #
1312
+ # resource :profile
1313
+ #
1314
+ # This creates six different routes in your application, all mapping to the
1315
+ # `Profiles` controller (note that the controller is named after the plural):
1316
+ #
1317
+ # GET /profile/new
1318
+ # GET /profile
1319
+ # GET /profile/edit
1320
+ # PATCH/PUT /profile
1321
+ # DELETE /profile
1322
+ # POST /profile
1176
1323
  #
1177
- # resource :profile
1324
+ # If you want instances of a model to work with this resource via record
1325
+ # identification (e.g. in `form_with` or `redirect_to`), you will need to call
1326
+ # [resolve](rdoc-ref:CustomUrls#resolve):
1178
1327
  #
1179
- # creates six different routes in your application, all mapping to
1180
- # the +Profiles+ controller (note that the controller is named after
1181
- # the plural):
1328
+ # resource :profile
1329
+ # resolve('Profile') { [:profile] }
1182
1330
  #
1183
- # GET /profile/new
1184
- # POST /profile
1185
- # GET /profile
1186
- # GET /profile/edit
1187
- # PATCH/PUT /profile
1188
- # DELETE /profile
1331
+ # # Enables this to work with singular routes:
1332
+ # form_with(model: @profile) {}
1189
1333
  #
1190
- # === Options
1191
- # Takes same options as +resources+.
1334
+ # ### Options
1335
+ # Takes same options as [resources](rdoc-ref:#resources)
1192
1336
  def resource(*resources, &block)
1193
1337
  options = resources.extract_options!.dup
1194
1338
 
@@ -1196,157 +1340,169 @@ module ActionDispatch
1196
1340
  return self
1197
1341
  end
1198
1342
 
1199
- resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
1200
- yield if block_given?
1343
+ with_scope_level(:resource) do
1344
+ options = apply_action_options options
1345
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
1346
+ yield if block_given?
1201
1347
 
1202
- concerns(options[:concerns]) if options[:concerns]
1348
+ concerns(options[:concerns]) if options[:concerns]
1203
1349
 
1204
- collection do
1205
- post :create
1206
- end if parent_resource.actions.include?(:create)
1350
+ new do
1351
+ get :new
1352
+ end if parent_resource.actions.include?(:new)
1207
1353
 
1208
- new do
1209
- get :new
1210
- end if parent_resource.actions.include?(:new)
1354
+ set_member_mappings_for_resource
1211
1355
 
1212
- set_member_mappings_for_resource
1356
+ collection do
1357
+ post :create
1358
+ end if parent_resource.actions.include?(:create)
1359
+ end
1213
1360
  end
1214
1361
 
1215
1362
  self
1216
1363
  end
1217
1364
 
1218
- # In Rails, a resourceful route provides a mapping between HTTP verbs
1219
- # and URLs and controller actions. By convention, each action also maps
1220
- # to particular CRUD operations in a database. A single entry in the
1221
- # routing file, such as
1365
+ # In Rails, a resourceful route provides a mapping between HTTP verbs and URLs
1366
+ # and controller actions. By convention, each action also maps to particular
1367
+ # CRUD operations in a database. A single entry in the routing file, such as
1222
1368
  #
1223
- # resources :photos
1369
+ # resources :photos
1224
1370
  #
1225
- # creates seven different routes in your application, all mapping to
1226
- # the +Photos+ controller:
1371
+ # creates seven different routes in your application, all mapping to the
1372
+ # `Photos` controller:
1227
1373
  #
1228
- # GET /photos
1229
- # GET /photos/new
1230
- # POST /photos
1231
- # GET /photos/:id
1232
- # GET /photos/:id/edit
1233
- # PATCH/PUT /photos/:id
1234
- # DELETE /photos/:id
1374
+ # GET /photos
1375
+ # GET /photos/new
1376
+ # POST /photos
1377
+ # GET /photos/:id
1378
+ # GET /photos/:id/edit
1379
+ # PATCH/PUT /photos/:id
1380
+ # DELETE /photos/:id
1235
1381
  #
1236
1382
  # Resources can also be nested infinitely by using this block syntax:
1237
1383
  #
1238
- # resources :photos do
1239
- # resources :comments
1240
- # end
1384
+ # resources :photos do
1385
+ # resources :comments
1386
+ # end
1241
1387
  #
1242
1388
  # This generates the following comments routes:
1243
1389
  #
1244
- # GET /photos/:photo_id/comments
1245
- # GET /photos/:photo_id/comments/new
1246
- # POST /photos/:photo_id/comments
1247
- # GET /photos/:photo_id/comments/:id
1248
- # GET /photos/:photo_id/comments/:id/edit
1249
- # PATCH/PUT /photos/:photo_id/comments/:id
1250
- # DELETE /photos/:photo_id/comments/:id
1390
+ # GET /photos/:photo_id/comments
1391
+ # GET /photos/:photo_id/comments/new
1392
+ # POST /photos/:photo_id/comments
1393
+ # GET /photos/:photo_id/comments/:id
1394
+ # GET /photos/:photo_id/comments/:id/edit
1395
+ # PATCH/PUT /photos/:photo_id/comments/:id
1396
+ # DELETE /photos/:photo_id/comments/:id
1251
1397
  #
1252
- # === Options
1253
- # Takes same options as <tt>Base#match</tt> as well as:
1398
+ # ### Options
1399
+ # Takes same options as [match](rdoc-ref:Base#match) as well as:
1254
1400
  #
1255
- # [:path_names]
1256
- # Allows you to change the segment component of the +edit+ and +new+ actions.
1257
- # Actions not specified are not changed.
1401
+ # :path_names
1402
+ # : Allows you to change the segment component of the `edit` and `new`
1403
+ # actions. Actions not specified are not changed.
1258
1404
  #
1259
- # resources :posts, path_names: { new: "brand_new" }
1405
+ # resources :posts, path_names: { new: "brand_new" }
1260
1406
  #
1261
- # The above example will now change /posts/new to /posts/brand_new
1407
+ # The above example will now change /posts/new to /posts/brand_new.
1262
1408
  #
1263
- # [:path]
1264
- # Allows you to change the path prefix for the resource.
1409
+ # :path
1410
+ # : Allows you to change the path prefix for the resource.
1265
1411
  #
1266
- # resources :posts, path: 'postings'
1412
+ # resources :posts, path: 'postings'
1267
1413
  #
1268
- # The resource and all segments will now route to /postings instead of /posts
1414
+ # The resource and all segments will now route to /postings instead of
1415
+ # /posts.
1269
1416
  #
1270
- # [:only]
1271
- # Only generate routes for the given actions.
1417
+ # :only
1418
+ # : Only generate routes for the given actions.
1272
1419
  #
1273
- # resources :cows, only: :show
1274
- # resources :cows, only: [:show, :index]
1420
+ # resources :cows, only: :show
1421
+ # resources :cows, only: [:show, :index]
1275
1422
  #
1276
- # [:except]
1277
- # Generate all routes except for the given actions.
1423
+ # :except
1424
+ # : Generate all routes except for the given actions.
1278
1425
  #
1279
- # resources :cows, except: :show
1280
- # resources :cows, except: [:show, :index]
1426
+ # resources :cows, except: :show
1427
+ # resources :cows, except: [:show, :index]
1281
1428
  #
1282
- # [:shallow]
1283
- # Generates shallow routes for nested resource(s). When placed on a parent resource,
1284
- # generates shallow routes for all nested resources.
1429
+ # :shallow
1430
+ # : Generates shallow routes for nested resource(s). When placed on a parent
1431
+ # resource, generates shallow routes for all nested resources.
1285
1432
  #
1286
- # resources :posts, shallow: true do
1287
- # resources :comments
1288
- # end
1433
+ # resources :posts, shallow: true do
1434
+ # resources :comments
1435
+ # end
1289
1436
  #
1290
- # Is the same as:
1437
+ # Is the same as:
1291
1438
  #
1292
- # resources :posts do
1293
- # resources :comments, except: [:show, :edit, :update, :destroy]
1294
- # end
1295
- # resources :comments, only: [:show, :edit, :update, :destroy]
1439
+ # resources :posts do
1440
+ # resources :comments, except: [:show, :edit, :update, :destroy]
1441
+ # end
1442
+ # resources :comments, only: [:show, :edit, :update, :destroy]
1296
1443
  #
1297
- # This allows URLs for resources that otherwise would be deeply nested such
1298
- # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
1299
- # to be shortened to just <tt>/comments/1234</tt>.
1444
+ # This allows URLs for resources that otherwise would be deeply nested such
1445
+ # as a comment on a blog post like `/posts/a-long-permalink/comments/1234`
1446
+ # to be shortened to just `/comments/1234`.
1300
1447
  #
1301
- # [:shallow_path]
1302
- # Prefixes nested shallow routes with the specified path.
1448
+ # Set `shallow: false` on a child resource to ignore a parent's shallow
1449
+ # parameter.
1303
1450
  #
1304
- # scope shallow_path: "sekret" do
1305
- # resources :posts do
1306
- # resources :comments, shallow: true
1307
- # end
1308
- # end
1451
+ # :shallow_path
1452
+ # : Prefixes nested shallow routes with the specified path.
1309
1453
  #
1310
- # The +comments+ resource here will have the following routes generated for it:
1454
+ # scope shallow_path: "sekret" do
1455
+ # resources :posts do
1456
+ # resources :comments, shallow: true
1457
+ # end
1458
+ # end
1311
1459
  #
1312
- # post_comments GET /posts/:post_id/comments(.:format)
1313
- # post_comments POST /posts/:post_id/comments(.:format)
1314
- # new_post_comment GET /posts/:post_id/comments/new(.:format)
1315
- # edit_comment GET /sekret/comments/:id/edit(.:format)
1316
- # comment GET /sekret/comments/:id(.:format)
1317
- # comment PATCH/PUT /sekret/comments/:id(.:format)
1318
- # comment DELETE /sekret/comments/:id(.:format)
1460
+ # The `comments` resource here will have the following routes generated for
1461
+ # it:
1319
1462
  #
1320
- # [:shallow_prefix]
1321
- # Prefixes nested shallow route names with specified prefix.
1463
+ # post_comments GET /posts/:post_id/comments(.:format)
1464
+ # post_comments POST /posts/:post_id/comments(.:format)
1465
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
1466
+ # edit_comment GET /sekret/comments/:id/edit(.:format)
1467
+ # comment GET /sekret/comments/:id(.:format)
1468
+ # comment PATCH/PUT /sekret/comments/:id(.:format)
1469
+ # comment DELETE /sekret/comments/:id(.:format)
1322
1470
  #
1323
- # scope shallow_prefix: "sekret" do
1324
- # resources :posts do
1325
- # resources :comments, shallow: true
1326
- # end
1327
- # end
1471
+ # :shallow_prefix
1472
+ # : Prefixes nested shallow route names with specified prefix.
1328
1473
  #
1329
- # The +comments+ resource here will have the following routes generated for it:
1474
+ # scope shallow_prefix: "sekret" do
1475
+ # resources :posts do
1476
+ # resources :comments, shallow: true
1477
+ # end
1478
+ # end
1330
1479
  #
1331
- # post_comments GET /posts/:post_id/comments(.:format)
1332
- # post_comments POST /posts/:post_id/comments(.:format)
1333
- # new_post_comment GET /posts/:post_id/comments/new(.:format)
1334
- # edit_sekret_comment GET /comments/:id/edit(.:format)
1335
- # sekret_comment GET /comments/:id(.:format)
1336
- # sekret_comment PATCH/PUT /comments/:id(.:format)
1337
- # sekret_comment DELETE /comments/:id(.:format)
1480
+ # The `comments` resource here will have the following routes generated for
1481
+ # it:
1338
1482
  #
1339
- # [:format]
1340
- # Allows you to specify the default value for optional +format+
1341
- # segment or disable it by supplying +false+.
1483
+ # post_comments GET /posts/:post_id/comments(.:format)
1484
+ # post_comments POST /posts/:post_id/comments(.:format)
1485
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
1486
+ # edit_sekret_comment GET /comments/:id/edit(.:format)
1487
+ # sekret_comment GET /comments/:id(.:format)
1488
+ # sekret_comment PATCH/PUT /comments/:id(.:format)
1489
+ # sekret_comment DELETE /comments/:id(.:format)
1342
1490
  #
1343
- # === Examples
1491
+ # :format
1492
+ # : Allows you to specify the default value for optional `format` segment or
1493
+ # disable it by supplying `false`.
1344
1494
  #
1345
- # # routes call <tt>Admin::PostsController</tt>
1346
- # resources :posts, module: "admin"
1495
+ # :param
1496
+ # : Allows you to override the default param name of `:id` in the URL.
1347
1497
  #
1348
- # # resource actions are at /admin/posts.
1349
- # resources :posts, path: "admin/posts"
1498
+ #
1499
+ # ### Examples
1500
+ #
1501
+ # # routes call +Admin::PostsController+
1502
+ # resources :posts, module: "admin"
1503
+ #
1504
+ # # resource actions are at /admin/posts.
1505
+ # resources :posts, path: "admin/posts"
1350
1506
  def resources(*resources, &block)
1351
1507
  options = resources.extract_options!.dup
1352
1508
 
@@ -1354,21 +1510,24 @@ module ActionDispatch
1354
1510
  return self
1355
1511
  end
1356
1512
 
1357
- resource_scope(:resources, Resource.new(resources.pop, options)) do
1358
- yield if block_given?
1513
+ with_scope_level(:resources) do
1514
+ options = apply_action_options options
1515
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
1516
+ yield if block_given?
1359
1517
 
1360
- concerns(options[:concerns]) if options[:concerns]
1518
+ concerns(options[:concerns]) if options[:concerns]
1361
1519
 
1362
- collection do
1363
- get :index if parent_resource.actions.include?(:index)
1364
- post :create if parent_resource.actions.include?(:create)
1365
- end
1520
+ collection do
1521
+ get :index if parent_resource.actions.include?(:index)
1522
+ post :create if parent_resource.actions.include?(:create)
1523
+ end
1366
1524
 
1367
- new do
1368
- get :new
1369
- end if parent_resource.actions.include?(:new)
1525
+ new do
1526
+ get :new
1527
+ end if parent_resource.actions.include?(:new)
1370
1528
 
1371
- set_member_mappings_for_resource
1529
+ set_member_mappings_for_resource
1530
+ end
1372
1531
  end
1373
1532
 
1374
1533
  self
@@ -1376,80 +1535,83 @@ module ActionDispatch
1376
1535
 
1377
1536
  # To add a route to the collection:
1378
1537
  #
1379
- # resources :photos do
1380
- # collection do
1381
- # get 'search'
1538
+ # resources :photos do
1539
+ # collection do
1540
+ # get 'search'
1541
+ # end
1382
1542
  # end
1383
- # end
1384
1543
  #
1385
- # This will enable Rails to recognize paths such as <tt>/photos/search</tt>
1386
- # with GET, and route to the search action of +PhotosController+. It will also
1387
- # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
1388
- # route helpers.
1389
- def collection
1544
+ # This will enable Rails to recognize paths such as `/photos/search` with GET,
1545
+ # and route to the search action of `PhotosController`. It will also create the
1546
+ # `search_photos_url` and `search_photos_path` route helpers.
1547
+ def collection(&block)
1390
1548
  unless resource_scope?
1391
1549
  raise ArgumentError, "can't use collection outside resource(s) scope"
1392
1550
  end
1393
1551
 
1394
1552
  with_scope_level(:collection) do
1395
- scope(parent_resource.collection_scope) do
1396
- yield
1397
- end
1553
+ path_scope(parent_resource.collection_scope, &block)
1398
1554
  end
1399
1555
  end
1400
1556
 
1401
1557
  # To add a member route, add a member block into the resource block:
1402
1558
  #
1403
- # resources :photos do
1404
- # member do
1405
- # get 'preview'
1559
+ # resources :photos do
1560
+ # member do
1561
+ # get 'preview'
1562
+ # end
1406
1563
  # end
1407
- # end
1408
1564
  #
1409
- # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
1410
- # preview action of +PhotosController+. It will also create the
1411
- # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
1412
- def member
1565
+ # This will recognize `/photos/1/preview` with GET, and route to the preview
1566
+ # action of `PhotosController`. It will also create the `preview_photo_url` and
1567
+ # `preview_photo_path` helpers.
1568
+ def member(&block)
1413
1569
  unless resource_scope?
1414
1570
  raise ArgumentError, "can't use member outside resource(s) scope"
1415
1571
  end
1416
1572
 
1417
1573
  with_scope_level(:member) do
1418
1574
  if shallow?
1419
- shallow_scope(parent_resource.member_scope) { yield }
1575
+ shallow_scope {
1576
+ path_scope(parent_resource.member_scope, &block)
1577
+ }
1420
1578
  else
1421
- scope(parent_resource.member_scope) { yield }
1579
+ path_scope(parent_resource.member_scope, &block)
1422
1580
  end
1423
1581
  end
1424
1582
  end
1425
1583
 
1426
- def new
1584
+ def new(&block)
1427
1585
  unless resource_scope?
1428
1586
  raise ArgumentError, "can't use new outside resource(s) scope"
1429
1587
  end
1430
1588
 
1431
1589
  with_scope_level(:new) do
1432
- scope(parent_resource.new_scope(action_path(:new))) do
1433
- yield
1434
- end
1590
+ path_scope(parent_resource.new_scope(action_path(:new)), &block)
1435
1591
  end
1436
1592
  end
1437
1593
 
1438
- def nested
1594
+ def nested(&block)
1439
1595
  unless resource_scope?
1440
1596
  raise ArgumentError, "can't use nested outside resource(s) scope"
1441
1597
  end
1442
1598
 
1443
1599
  with_scope_level(:nested) do
1444
1600
  if shallow? && shallow_nesting_depth >= 1
1445
- shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1601
+ shallow_scope do
1602
+ path_scope(parent_resource.nested_scope) do
1603
+ scope(nested_options, &block)
1604
+ end
1605
+ end
1446
1606
  else
1447
- scope(parent_resource.nested_scope, nested_options) { yield }
1607
+ path_scope(parent_resource.nested_scope) do
1608
+ scope(nested_options, &block)
1609
+ end
1448
1610
  end
1449
1611
  end
1450
1612
  end
1451
1613
 
1452
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
1614
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
1453
1615
  def namespace(path, options = {})
1454
1616
  if resource_scope?
1455
1617
  nested { super }
@@ -1459,28 +1621,72 @@ module ActionDispatch
1459
1621
  end
1460
1622
 
1461
1623
  def shallow
1462
- scope(:shallow => true) do
1463
- yield
1464
- end
1624
+ @scope = @scope.new(shallow: true)
1625
+ yield
1626
+ ensure
1627
+ @scope = @scope.parent
1465
1628
  end
1466
1629
 
1467
1630
  def shallow?
1468
- parent_resource.instance_of?(Resource) && @scope[:shallow]
1631
+ !parent_resource.singleton? && @scope[:shallow]
1632
+ end
1633
+
1634
+ # Loads another routes file with the given `name` located inside the
1635
+ # `config/routes` directory. In that file, you can use the normal routing DSL,
1636
+ # but *do not* surround it with a `Rails.application.routes.draw` block.
1637
+ #
1638
+ # # config/routes.rb
1639
+ # Rails.application.routes.draw do
1640
+ # draw :admin # Loads `config/routes/admin.rb`
1641
+ # draw "third_party/some_gem" # Loads `config/routes/third_party/some_gem.rb`
1642
+ # end
1643
+ #
1644
+ # # config/routes/admin.rb
1645
+ # namespace :admin do
1646
+ # resources :accounts
1647
+ # end
1648
+ #
1649
+ # # config/routes/third_party/some_gem.rb
1650
+ # mount SomeGem::Engine, at: "/some_gem"
1651
+ #
1652
+ # **CAUTION:** Use this feature with care. Having multiple routes files can
1653
+ # negatively impact discoverability and readability. For most applications —
1654
+ # even those with a few hundred routes — it's easier for developers to have a
1655
+ # single routes file.
1656
+ def draw(name)
1657
+ path = @draw_paths.find do |_path|
1658
+ File.exist? "#{_path}/#{name}.rb"
1659
+ end
1660
+
1661
+ unless path
1662
+ msg = "Your router tried to #draw the external file #{name}.rb,\n" \
1663
+ "but the file was not found in:\n\n"
1664
+ msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n")
1665
+ raise ArgumentError, msg
1666
+ end
1667
+
1668
+ route_path = "#{path}/#{name}.rb"
1669
+ instance_eval(File.read(route_path), route_path.to_s)
1469
1670
  end
1470
1671
 
1471
- # match 'path' => 'controller#action'
1472
- # match 'path', to: 'controller#action'
1473
- # match 'path', 'otherpath', on: :member, via: :get
1474
- def match(path, *rest)
1672
+ # Matches a URL pattern to one or more routes. For more information, see
1673
+ # [match](rdoc-ref:Base#match).
1674
+ #
1675
+ # match 'path' => 'controller#action', via: :patch
1676
+ # match 'path', to: 'controller#action', via: :post
1677
+ # match 'path', 'otherpath', on: :member, via: :get
1678
+ def match(path, *rest, &block)
1475
1679
  if rest.empty? && Hash === path
1476
1680
  options = path
1477
1681
  path, to = options.find { |name, _value| name.is_a?(String) }
1478
1682
 
1683
+ raise ArgumentError, "Route path not specified" if path.nil?
1684
+
1479
1685
  case to
1480
1686
  when Symbol
1481
1687
  options[:action] = to
1482
1688
  when String
1483
- if to =~ /#/
1689
+ if to.include?("#")
1484
1690
  options[:to] = to
1485
1691
  else
1486
1692
  options[:controller] = to
@@ -1496,77 +1702,30 @@ module ActionDispatch
1496
1702
  paths = [path] + rest
1497
1703
  end
1498
1704
 
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?("/")
1705
+ if options.key?(:defaults)
1706
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
1551
1707
  else
1552
- action = nil
1708
+ map_match(paths, options, &block)
1553
1709
  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
1710
  end
1565
1711
 
1566
- def root(path, options={})
1712
+ # You can specify what Rails should route "/" to with the root method:
1713
+ #
1714
+ # root to: 'pages#main'
1715
+ #
1716
+ # For options, see `match`, as `root` uses it internally.
1717
+ #
1718
+ # You can also pass a string which will expand
1719
+ #
1720
+ # root 'pages#main'
1721
+ #
1722
+ # You should put the root route at the top of `config/routes.rb`, because this
1723
+ # means it will be matched first. As this is the most popular route of most
1724
+ # Rails applications, this is beneficial.
1725
+ def root(path, options = {})
1567
1726
  if path.is_a?(String)
1568
1727
  options[:to] = path
1569
- elsif path.is_a?(Hash) and options.empty?
1728
+ elsif path.is_a?(Hash) && options.empty?
1570
1729
  options = path
1571
1730
  else
1572
1731
  raise ArgumentError, "must be called with a path and/or options"
@@ -1574,36 +1733,36 @@ module ActionDispatch
1574
1733
 
1575
1734
  if @scope.resources?
1576
1735
  with_scope_level(:root) do
1577
- scope(parent_resource.path) do
1578
- super(options)
1736
+ path_scope(parent_resource.path) do
1737
+ match_root_route(options)
1579
1738
  end
1580
1739
  end
1581
1740
  else
1582
- super(options)
1741
+ match_root_route(options)
1583
1742
  end
1584
1743
  end
1585
1744
 
1586
- protected
1587
-
1588
- def parent_resource #:nodoc:
1745
+ private
1746
+ def parent_resource
1589
1747
  @scope[:scope_level_resource]
1590
1748
  end
1591
1749
 
1592
- def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1750
+ def apply_common_behavior_for(method, resources, options, &block)
1593
1751
  if resources.length > 1
1594
- resources.each { |r| send(method, r, options, &block) }
1752
+ resources.each { |r| public_send(method, r, options, &block) }
1595
1753
  return true
1596
1754
  end
1597
1755
 
1598
- if options.delete(:shallow)
1756
+ if options[:shallow]
1757
+ options.delete(:shallow)
1599
1758
  shallow do
1600
- send(method, resources.pop, options, &block)
1759
+ public_send(method, resources.pop, options, &block)
1601
1760
  end
1602
1761
  return true
1603
1762
  end
1604
1763
 
1605
1764
  if resource_scope?
1606
- nested { send(method, resources.pop, options, &block) }
1765
+ nested { public_send(method, resources.pop, options, &block) }
1607
1766
  return true
1608
1767
  end
1609
1768
 
@@ -1614,76 +1773,56 @@ module ActionDispatch
1614
1773
  scope_options = options.slice!(*RESOURCE_OPTIONS)
1615
1774
  unless scope_options.empty?
1616
1775
  scope(scope_options) do
1617
- send(method, resources.pop, options, &block)
1776
+ public_send(method, resources.pop, options, &block)
1618
1777
  end
1619
1778
  return true
1620
1779
  end
1621
1780
 
1622
- unless action_options?(options)
1623
- options.merge!(scope_action_options) if scope_action_options?
1624
- end
1625
-
1626
1781
  false
1627
1782
  end
1628
1783
 
1629
- def action_options?(options) #:nodoc:
1630
- options[:only] || options[:except]
1784
+ def apply_action_options(options)
1785
+ return options if action_options? options
1786
+ options.merge scope_action_options
1631
1787
  end
1632
1788
 
1633
- def scope_action_options? #:nodoc:
1634
- @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1789
+ def action_options?(options)
1790
+ options[:only] || options[:except]
1635
1791
  end
1636
1792
 
1637
- def scope_action_options #:nodoc:
1638
- @scope[:options].slice(:only, :except)
1793
+ def scope_action_options
1794
+ @scope[:action_options] || {}
1639
1795
  end
1640
1796
 
1641
- def resource_scope? #:nodoc:
1797
+ def resource_scope?
1642
1798
  @scope.resource_scope?
1643
1799
  end
1644
1800
 
1645
- def resource_method_scope? #:nodoc:
1801
+ def resource_method_scope?
1646
1802
  @scope.resource_method_scope?
1647
1803
  end
1648
1804
 
1649
- def nested_scope? #:nodoc:
1805
+ def nested_scope?
1650
1806
  @scope.nested?
1651
1807
  end
1652
1808
 
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)
1809
+ def with_scope_level(kind) # :doc:
1666
1810
  @scope = @scope.new_level(kind)
1667
1811
  yield
1668
1812
  ensure
1669
1813
  @scope = @scope.parent
1670
1814
  end
1671
1815
 
1672
- def resource_scope(kind, resource) #:nodoc:
1673
- resource.shallow = @scope[:shallow]
1674
- @scope = @scope.new(:scope_level_resource => resource)
1675
- @nesting.push(resource)
1816
+ def resource_scope(resource, &block)
1817
+ @scope = @scope.new(scope_level_resource: resource)
1676
1818
 
1677
- with_scope_level(kind) do
1678
- scope(parent_resource.resource_scope) { yield }
1679
- end
1819
+ controller(resource.resource_scope, &block)
1680
1820
  ensure
1681
- @nesting.pop
1682
1821
  @scope = @scope.parent
1683
1822
  end
1684
1823
 
1685
- def nested_options #:nodoc:
1686
- options = { :as => parent_resource.member_name }
1824
+ def nested_options
1825
+ options = { as: parent_resource.member_name }
1687
1826
  options[:constraints] = {
1688
1827
  parent_resource.nested_param => param_constraint
1689
1828
  } if param_constraint?
@@ -1691,62 +1830,61 @@ module ActionDispatch
1691
1830
  options
1692
1831
  end
1693
1832
 
1694
- def nesting_depth #:nodoc:
1695
- @nesting.size
1696
- end
1697
-
1698
- def shallow_nesting_depth #:nodoc:
1699
- @nesting.select(&:shallow?).size
1833
+ def shallow_nesting_depth
1834
+ @scope.find_all { |node|
1835
+ node.frame[:scope_level_resource]
1836
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
1700
1837
  end
1701
1838
 
1702
- def param_constraint? #:nodoc:
1839
+ def param_constraint?
1703
1840
  @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1704
1841
  end
1705
1842
 
1706
- def param_constraint #:nodoc:
1843
+ def param_constraint
1707
1844
  @scope[:constraints][parent_resource.param]
1708
1845
  end
1709
1846
 
1710
- def canonical_action?(action) #:nodoc:
1847
+ def canonical_action?(action)
1711
1848
  resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1712
1849
  end
1713
1850
 
1714
- def shallow_scope(path, options = {}) #:nodoc:
1715
- scope = { :as => @scope[:shallow_prefix],
1716
- :path => @scope[:shallow_path] }
1851
+ def shallow_scope
1852
+ scope = { as: @scope[:shallow_prefix],
1853
+ path: @scope[:shallow_path] }
1717
1854
  @scope = @scope.new scope
1718
1855
 
1719
- scope(path, options) { yield }
1856
+ yield
1720
1857
  ensure
1721
1858
  @scope = @scope.parent
1722
1859
  end
1723
1860
 
1724
- def path_for_action(action, path) #:nodoc:
1725
- if path.blank? && canonical_action?(action)
1861
+ def path_for_action(action, path)
1862
+ return "#{@scope[:path]}/#{path}" if path
1863
+
1864
+ if canonical_action?(action)
1726
1865
  @scope[:path].to_s
1727
1866
  else
1728
- "#{@scope[:path]}/#{action_path(action, path)}"
1867
+ "#{@scope[:path]}/#{action_path(action)}"
1729
1868
  end
1730
1869
  end
1731
1870
 
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
1871
+ def action_path(name)
1872
+ @scope[:path_names][name.to_sym] || name
1735
1873
  end
1736
1874
 
1737
- def prefix_name_for_action(as, action) #:nodoc:
1875
+ def prefix_name_for_action(as, action)
1738
1876
  if as
1739
1877
  prefix = as
1740
1878
  elsif !canonical_action?(action)
1741
1879
  prefix = action
1742
1880
  end
1743
1881
 
1744
- if prefix && prefix != '/' && !prefix.empty?
1745
- Mapper.normalize_name prefix.to_s.tr('-', '_')
1882
+ if prefix && prefix != "/" && !prefix.empty?
1883
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
1746
1884
  end
1747
1885
  end
1748
1886
 
1749
- def name_for_action(as, action) #:nodoc:
1887
+ def name_for_action(as, action)
1750
1888
  prefix = prefix_name_for_action(as, action)
1751
1889
  name_prefix = @scope[:as]
1752
1890
 
@@ -1757,21 +1895,22 @@ module ActionDispatch
1757
1895
  member_name = parent_resource.member_name
1758
1896
  end
1759
1897
 
1760
- name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1898
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1899
+ candidate = action_name.select(&:present?).join("_")
1761
1900
 
1762
- if candidate = name.compact.join("_").presence
1763
- # If a name was not explicitly given, we check if it is valid
1764
- # and return nil in case it isn't. Otherwise, we pass the invalid name
1765
- # forward so the underlying router engine treats it and raises an exception.
1901
+ unless candidate.empty?
1902
+ # If a name was not explicitly given, we check if it is valid and return nil in
1903
+ # case it isn't. Otherwise, we pass the invalid name forward so the underlying
1904
+ # router engine treats it and raises an exception.
1766
1905
  if as.nil?
1767
- candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
1906
+ candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
1768
1907
  else
1769
1908
  candidate
1770
1909
  end
1771
1910
  end
1772
1911
  end
1773
1912
 
1774
- def set_member_mappings_for_resource
1913
+ def set_member_mappings_for_resource # :doc:
1775
1914
  member do
1776
1915
  get :edit if parent_resource.actions.include?(:edit)
1777
1916
  get :show if parent_resource.actions.include?(:show)
@@ -1782,85 +1921,197 @@ module ActionDispatch
1782
1921
  delete :destroy if parent_resource.actions.include?(:destroy)
1783
1922
  end
1784
1923
  end
1924
+
1925
+ def api_only? # :doc:
1926
+ @set.api_only?
1927
+ end
1928
+
1929
+ def path_scope(path)
1930
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
1931
+ yield
1932
+ ensure
1933
+ @scope = @scope.parent
1934
+ end
1935
+
1936
+ def map_match(paths, options)
1937
+ if (on = options[:on]) && !VALID_ON_OPTIONS.include?(on)
1938
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1939
+ end
1940
+
1941
+ if @scope[:to]
1942
+ options[:to] ||= @scope[:to]
1943
+ end
1944
+
1945
+ if @scope[:controller] && @scope[:action]
1946
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
1947
+ end
1948
+
1949
+ controller = options.delete(:controller) || @scope[:controller]
1950
+ option_path = options.delete :path
1951
+ to = options.delete :to
1952
+ via = Mapping.check_via Array(options.delete(:via) {
1953
+ @scope[:via]
1954
+ })
1955
+ formatted = options.delete(:format) { @scope[:format] }
1956
+ anchor = options.delete(:anchor) { true }
1957
+ options_constraints = options.delete(:constraints) || {}
1958
+
1959
+ path_types = paths.group_by(&:class)
1960
+ (path_types[String] || []).each do |_path|
1961
+ route_options = options.dup
1962
+ if _path && option_path
1963
+ raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
1964
+ end
1965
+ to = get_to_from_path(_path, to, route_options[:action])
1966
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
1967
+ end
1968
+
1969
+ (path_types[Symbol] || []).each do |action|
1970
+ route_options = options.dup
1971
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
1972
+ end
1973
+
1974
+ self
1975
+ end
1976
+
1977
+ def get_to_from_path(path, to, action)
1978
+ return to if to || action
1979
+
1980
+ path_without_format = path.sub(/\(\.:format\)$/, "")
1981
+ if using_match_shorthand?(path_without_format)
1982
+ path_without_format.delete_prefix("/").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
1983
+ else
1984
+ nil
1985
+ end
1986
+ end
1987
+
1988
+ def using_match_shorthand?(path)
1989
+ %r{^/?[-\w]+/[-\w/]+$}.match?(path)
1990
+ end
1991
+
1992
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
1993
+ if on = options.delete(:on)
1994
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1995
+ else
1996
+ case @scope.scope_level
1997
+ when :resources
1998
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
1999
+ when :resource
2000
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
2001
+ else
2002
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
2003
+ end
2004
+ end
2005
+ end
2006
+
2007
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
2008
+ path = path_for_action(action, _path)
2009
+ raise ArgumentError, "path is required" if path.blank?
2010
+
2011
+ action = action.to_s
2012
+
2013
+ default_action = options.delete(:action) || @scope[:action]
2014
+
2015
+ if /^[\w\-\/]+$/.match?(action)
2016
+ default_action ||= action.tr("-", "_") unless action.include?("/")
2017
+ else
2018
+ action = nil
2019
+ end
2020
+
2021
+ as = if !options.fetch(:as, true) # if it's set to nil or false
2022
+ options.delete(:as)
2023
+ else
2024
+ name_for_action(options.delete(:as), action)
2025
+ end
2026
+
2027
+ path = Mapping.normalize_path URI::DEFAULT_PARSER.escape(path), formatted
2028
+ ast = Journey::Parser.parse path
2029
+
2030
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
2031
+ @set.add_route(mapping, as)
2032
+ end
2033
+
2034
+ def match_root_route(options)
2035
+ args = ["/", { as: :root, via: :get }.merge(options)]
2036
+ match(*args)
2037
+ end
1785
2038
  end
1786
2039
 
1787
- # Routing Concerns allow you to declare common routes that can be reused
1788
- # inside others resources and routes.
2040
+ # Routing Concerns allow you to declare common routes that can be reused inside
2041
+ # others resources and routes.
1789
2042
  #
1790
- # concern :commentable do
1791
- # resources :comments
1792
- # end
2043
+ # concern :commentable do
2044
+ # resources :comments
2045
+ # end
1793
2046
  #
1794
- # concern :image_attachable do
1795
- # resources :images, only: :index
1796
- # end
2047
+ # concern :image_attachable do
2048
+ # resources :images, only: :index
2049
+ # end
1797
2050
  #
1798
2051
  # These concerns are used in Resources routing:
1799
2052
  #
1800
- # resources :messages, concerns: [:commentable, :image_attachable]
2053
+ # resources :messages, concerns: [:commentable, :image_attachable]
1801
2054
  #
1802
2055
  # or in a scope or namespace:
1803
2056
  #
1804
- # namespace :posts do
1805
- # concerns :commentable
1806
- # end
2057
+ # namespace :posts do
2058
+ # concerns :commentable
2059
+ # end
1807
2060
  module Concerns
1808
2061
  # Define a routing concern using a name.
1809
2062
  #
1810
- # Concerns may be defined inline, using a block, or handled by
1811
- # another object, by passing that object as the second parameter.
2063
+ # Concerns may be defined inline, using a block, or handled by another object,
2064
+ # by passing that object as the second parameter.
1812
2065
  #
1813
- # The concern object, if supplied, should respond to <tt>call</tt>,
1814
- # which will receive two parameters:
2066
+ # The concern object, if supplied, should respond to `call`, which will receive
2067
+ # two parameters:
1815
2068
  #
1816
- # * The current mapper
1817
- # * A hash of options which the concern object may use
2069
+ # * The current mapper
2070
+ # * A hash of options which the concern object may use
1818
2071
  #
1819
- # Options may also be used by concerns defined in a block by accepting
1820
- # a block parameter. So, using a block, you might do something as
1821
- # simple as limit the actions available on certain resources, passing
1822
- # standard resource options through the concern:
2072
+ # Options may also be used by concerns defined in a block by accepting a block
2073
+ # parameter. So, using a block, you might do something as simple as limit the
2074
+ # actions available on certain resources, passing standard resource options
2075
+ # through the concern:
1823
2076
  #
1824
- # concern :commentable do |options|
1825
- # resources :comments, options
1826
- # end
2077
+ # concern :commentable do |options|
2078
+ # resources :comments, options
2079
+ # end
1827
2080
  #
1828
- # resources :posts, concerns: :commentable
1829
- # resources :archived_posts do
1830
- # # Don't allow comments on archived posts
1831
- # concerns :commentable, only: [:index, :show]
1832
- # end
2081
+ # resources :posts, concerns: :commentable
2082
+ # resources :archived_posts do
2083
+ # # Don't allow comments on archived posts
2084
+ # concerns :commentable, only: [:index, :show]
2085
+ # end
1833
2086
  #
1834
- # Or, using a callable object, you might implement something more
1835
- # specific to your application, which would be out of place in your
1836
- # routes file.
2087
+ # Or, using a callable object, you might implement something more specific to
2088
+ # your application, which would be out of place in your routes file.
1837
2089
  #
1838
- # # purchasable.rb
1839
- # class Purchasable
1840
- # def initialize(defaults = {})
1841
- # @defaults = defaults
1842
- # end
2090
+ # # purchasable.rb
2091
+ # class Purchasable
2092
+ # def initialize(defaults = {})
2093
+ # @defaults = defaults
2094
+ # end
1843
2095
  #
1844
- # def call(mapper, options = {})
1845
- # options = @defaults.merge(options)
1846
- # mapper.resources :purchases
1847
- # mapper.resources :receipts
1848
- # mapper.resources :returns if options[:returnable]
2096
+ # def call(mapper, options = {})
2097
+ # options = @defaults.merge(options)
2098
+ # mapper.resources :purchases
2099
+ # mapper.resources :receipts
2100
+ # mapper.resources :returns if options[:returnable]
2101
+ # end
1849
2102
  # end
1850
- # end
1851
2103
  #
1852
- # # routes.rb
1853
- # concern :purchasable, Purchasable.new(returnable: true)
2104
+ # # routes.rb
2105
+ # concern :purchasable, Purchasable.new(returnable: true)
1854
2106
  #
1855
- # resources :toys, concerns: :purchasable
1856
- # resources :electronics, concerns: :purchasable
1857
- # resources :pets do
1858
- # concerns :purchasable, returnable: false
1859
- # end
2107
+ # resources :toys, concerns: :purchasable
2108
+ # resources :electronics, concerns: :purchasable
2109
+ # resources :pets do
2110
+ # concerns :purchasable, returnable: false
2111
+ # end
1860
2112
  #
1861
- # Any routing helpers can be used inside a concern. If using a
1862
- # callable, they're accessible from the Mapper that's passed to
1863
- # <tt>call</tt>.
2113
+ # Any routing helpers can be used inside a concern. If using a callable, they're
2114
+ # accessible from the Mapper that's passed to `call`.
1864
2115
  def concern(name, callable = nil, &block)
1865
2116
  callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
1866
2117
  @concerns[name] = callable
@@ -1868,15 +2119,15 @@ module ActionDispatch
1868
2119
 
1869
2120
  # Use the named concerns
1870
2121
  #
1871
- # resources :posts do
1872
- # concerns :commentable
1873
- # end
2122
+ # resources :posts do
2123
+ # concerns :commentable
2124
+ # end
1874
2125
  #
1875
- # concerns also work in any routes helper that you want to use:
2126
+ # Concerns also work in any routes helper that you want to use:
1876
2127
  #
1877
- # namespace :posts do
1878
- # concerns :commentable
1879
- # end
2128
+ # namespace :posts do
2129
+ # concerns :commentable
2130
+ # end
1880
2131
  def concerns(*args)
1881
2132
  options = args.extract_options!
1882
2133
  args.flatten.each do |name|
@@ -1889,17 +2140,133 @@ module ActionDispatch
1889
2140
  end
1890
2141
  end
1891
2142
 
2143
+ module CustomUrls
2144
+ # Define custom URL helpers that will be added to the application's routes. This
2145
+ # allows you to override and/or replace the default behavior of routing helpers,
2146
+ # e.g:
2147
+ #
2148
+ # direct :homepage do
2149
+ # "https://rubyonrails.org"
2150
+ # end
2151
+ #
2152
+ # direct :commentable do |model|
2153
+ # [ model, anchor: model.dom_id ]
2154
+ # end
2155
+ #
2156
+ # direct :main do
2157
+ # { controller: "pages", action: "index", subdomain: "www" }
2158
+ # end
2159
+ #
2160
+ # The return value from the block passed to `direct` must be a valid set of
2161
+ # arguments for `url_for` which will actually build the URL string. This can be
2162
+ # one of the following:
2163
+ #
2164
+ # * A string, which is treated as a generated URL
2165
+ # * A hash, e.g. `{ controller: "pages", action: "index" }`
2166
+ # * An array, which is passed to `polymorphic_url`
2167
+ # * An Active Model instance
2168
+ # * An Active Model class
2169
+ #
2170
+ #
2171
+ # NOTE: Other URL helpers can be called in the block but be careful not to
2172
+ # invoke your custom URL helper again otherwise it will result in a stack
2173
+ # overflow error.
2174
+ #
2175
+ # You can also specify default options that will be passed through to your URL
2176
+ # helper definition, e.g:
2177
+ #
2178
+ # direct :browse, page: 1, size: 10 do |options|
2179
+ # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
2180
+ # end
2181
+ #
2182
+ # In this instance the `params` object comes from the context in which the block
2183
+ # is executed, e.g. generating a URL inside a controller action or a view. If
2184
+ # the block is executed where there isn't a `params` object such as this:
2185
+ #
2186
+ # Rails.application.routes.url_helpers.browse_path
2187
+ #
2188
+ # then it will raise a `NameError`. Because of this you need to be aware of the
2189
+ # context in which you will use your custom URL helper when defining it.
2190
+ #
2191
+ # NOTE: The `direct` method can't be used inside of a scope block such as
2192
+ # `namespace` or `scope` and will raise an error if it detects that it is.
2193
+ def direct(name, options = {}, &block)
2194
+ unless @scope.root?
2195
+ raise RuntimeError, "The direct method can't be used inside a routes scope block"
2196
+ end
2197
+
2198
+ @set.add_url_helper(name, options, &block)
2199
+ end
2200
+
2201
+ # Define custom polymorphic mappings of models to URLs. This alters the behavior
2202
+ # of `polymorphic_url` and consequently the behavior of `link_to` and `form_for`
2203
+ # when passed a model instance, e.g:
2204
+ #
2205
+ # resource :basket
2206
+ #
2207
+ # resolve "Basket" do
2208
+ # [:basket]
2209
+ # end
2210
+ #
2211
+ # This will now generate "/basket" when a `Basket` instance is passed to
2212
+ # `link_to` or `form_for` instead of the standard "/baskets/:id".
2213
+ #
2214
+ # NOTE: This custom behavior only applies to simple polymorphic URLs where a
2215
+ # single model instance is passed and not more complicated forms, e.g:
2216
+ #
2217
+ # # config/routes.rb
2218
+ # resource :profile
2219
+ # namespace :admin do
2220
+ # resources :users
2221
+ # end
2222
+ #
2223
+ # resolve("User") { [:profile] }
2224
+ #
2225
+ # # app/views/application/_menu.html.erb
2226
+ # link_to "Profile", @current_user
2227
+ # link_to "Profile", [:admin, @current_user]
2228
+ #
2229
+ # The first `link_to` will generate "/profile" but the second will generate the
2230
+ # standard polymorphic URL of "/admin/users/1".
2231
+ #
2232
+ # You can pass options to a polymorphic mapping - the arity for the block needs
2233
+ # to be two as the instance is passed as the first argument, e.g:
2234
+ #
2235
+ # resolve "Basket", anchor: "items" do |basket, options|
2236
+ # [:basket, options]
2237
+ # end
2238
+ #
2239
+ # This generates the URL "/basket#items" because when the last item in an array
2240
+ # passed to `polymorphic_url` is a hash then it's treated as options to the URL
2241
+ # helper that gets called.
2242
+ #
2243
+ # NOTE: The `resolve` method can't be used inside of a scope block such as
2244
+ # `namespace` or `scope` and will raise an error if it detects that it is.
2245
+ def resolve(*args, &block)
2246
+ unless @scope.root?
2247
+ raise RuntimeError, "The resolve method can't be used inside a routes scope block"
2248
+ end
2249
+
2250
+ options = args.extract_options!
2251
+ args = args.flatten(1)
2252
+
2253
+ args.each do |klass|
2254
+ @set.add_polymorphic_mapping(klass, options, &block)
2255
+ end
2256
+ end
2257
+ end
2258
+
1892
2259
  class Scope # :nodoc:
1893
2260
  OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
1894
2261
  :controller, :action, :path_names, :constraints,
1895
- :shallow, :blocks, :defaults, :options]
2262
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
1896
2263
 
1897
2264
  RESOURCE_SCOPES = [:resource, :resources]
1898
2265
  RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1899
2266
 
1900
2267
  attr_reader :parent, :scope_level
1901
2268
 
1902
- def initialize(hash, parent = {}, scope_level = nil)
2269
+ def initialize(hash, parent = NULL, scope_level = nil)
1903
2270
  @hash = hash
1904
2271
  @parent = parent
1905
2272
  @scope_level = scope_level
@@ -1909,6 +2276,14 @@ module ActionDispatch
1909
2276
  scope_level == :nested
1910
2277
  end
1911
2278
 
2279
+ def null?
2280
+ @hash.nil? && @parent.nil?
2281
+ end
2282
+
2283
+ def root?
2284
+ @parent.null?
2285
+ end
2286
+
1912
2287
  def resources?
1913
2288
  scope_level == :resources
1914
2289
  end
@@ -1947,27 +2322,34 @@ module ActionDispatch
1947
2322
  end
1948
2323
 
1949
2324
  def new_level(level)
1950
- self.class.new(self, self, level)
1951
- end
1952
-
1953
- def fetch(key, &block)
1954
- @hash.fetch(key, &block)
2325
+ self.class.new(frame, self, level)
1955
2326
  end
1956
2327
 
1957
2328
  def [](key)
1958
- @hash.fetch(key) { @parent[key] }
2329
+ scope = find { |node| node.frame.key? key }
2330
+ scope && scope.frame[key]
1959
2331
  end
1960
2332
 
1961
- def []=(k,v)
1962
- @hash[k] = v
2333
+ include Enumerable
2334
+
2335
+ def each
2336
+ node = self
2337
+ until node.equal? NULL
2338
+ yield node
2339
+ node = node.parent
2340
+ end
1963
2341
  end
2342
+
2343
+ def frame; @hash; end
2344
+
2345
+ NULL = Scope.new(nil, nil)
1964
2346
  end
1965
2347
 
1966
- def initialize(set) #:nodoc:
2348
+ def initialize(set) # :nodoc:
1967
2349
  @set = set
1968
- @scope = Scope.new({ :path_names => @set.resources_path_names })
2350
+ @draw_paths = set.draw_paths
2351
+ @scope = Scope.new(path_names: @set.resources_path_names)
1969
2352
  @concerns = {}
1970
- @nesting = []
1971
2353
  end
1972
2354
 
1973
2355
  include Base
@@ -1976,6 +2358,7 @@ module ActionDispatch
1976
2358
  include Scoping
1977
2359
  include Concerns
1978
2360
  include Resources
2361
+ include CustomUrls
1979
2362
  end
1980
2363
  end
1981
2364
  end