actionpack 4.2.8 → 5.2.4.2

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

Potentially problematic release.


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

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