actionpack 3.2.19 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +850 -401
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -288
  5. data/lib/abstract_controller/asset_paths.rb +2 -2
  6. data/lib/abstract_controller/base.rb +39 -37
  7. data/lib/abstract_controller/callbacks.rb +101 -82
  8. data/lib/abstract_controller/collector.rb +7 -3
  9. data/lib/abstract_controller/helpers.rb +25 -13
  10. data/lib/abstract_controller/layouts.rb +74 -74
  11. data/lib/abstract_controller/logger.rb +1 -2
  12. data/lib/abstract_controller/rendering.rb +30 -13
  13. data/lib/abstract_controller/translation.rb +16 -1
  14. data/lib/abstract_controller/url_for.rb +6 -6
  15. data/lib/abstract_controller/view_paths.rb +1 -1
  16. data/lib/abstract_controller.rb +1 -8
  17. data/lib/action_controller/base.rb +46 -22
  18. data/lib/action_controller/caching/fragments.rb +23 -53
  19. data/lib/action_controller/caching.rb +46 -33
  20. data/lib/action_controller/deprecated/integration_test.rb +3 -0
  21. data/lib/action_controller/deprecated.rb +5 -1
  22. data/lib/action_controller/log_subscriber.rb +16 -8
  23. data/lib/action_controller/metal/conditional_get.rb +76 -32
  24. data/lib/action_controller/metal/data_streaming.rb +20 -26
  25. data/lib/action_controller/metal/exceptions.rb +19 -6
  26. data/lib/action_controller/metal/flash.rb +24 -9
  27. data/lib/action_controller/metal/force_ssl.rb +70 -12
  28. data/lib/action_controller/metal/head.rb +25 -4
  29. data/lib/action_controller/metal/helpers.rb +5 -9
  30. data/lib/action_controller/metal/hide_actions.rb +0 -1
  31. data/lib/action_controller/metal/http_authentication.rb +107 -83
  32. data/lib/action_controller/metal/implicit_render.rb +1 -1
  33. data/lib/action_controller/metal/instrumentation.rb +2 -1
  34. data/lib/action_controller/metal/live.rb +175 -0
  35. data/lib/action_controller/metal/mime_responds.rb +161 -47
  36. data/lib/action_controller/metal/params_wrapper.rb +112 -74
  37. data/lib/action_controller/metal/rack_delegation.rb +9 -3
  38. data/lib/action_controller/metal/redirecting.rb +15 -20
  39. data/lib/action_controller/metal/renderers.rb +11 -9
  40. data/lib/action_controller/metal/rendering.rb +9 -1
  41. data/lib/action_controller/metal/request_forgery_protection.rb +112 -19
  42. data/lib/action_controller/metal/responder.rb +20 -19
  43. data/lib/action_controller/metal/streaming.rb +12 -18
  44. data/lib/action_controller/metal/strong_parameters.rb +520 -0
  45. data/lib/action_controller/metal/testing.rb +13 -18
  46. data/lib/action_controller/metal/url_for.rb +28 -25
  47. data/lib/action_controller/metal.rb +17 -32
  48. data/lib/action_controller/model_naming.rb +12 -0
  49. data/lib/action_controller/railtie.rb +33 -17
  50. data/lib/action_controller/railties/helpers.rb +22 -0
  51. data/lib/action_controller/record_identifier.rb +18 -72
  52. data/lib/action_controller/test_case.rb +251 -131
  53. data/lib/action_controller/vendor/html-scanner.rb +4 -19
  54. data/lib/action_controller.rb +15 -6
  55. data/lib/action_dispatch/http/cache.rb +63 -11
  56. data/lib/action_dispatch/http/filter_parameters.rb +18 -8
  57. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  58. data/lib/action_dispatch/http/headers.rb +49 -17
  59. data/lib/action_dispatch/http/mime_negotiation.rb +24 -1
  60. data/lib/action_dispatch/http/mime_type.rb +154 -100
  61. data/lib/action_dispatch/http/mime_types.rb +1 -1
  62. data/lib/action_dispatch/http/parameter_filter.rb +44 -46
  63. data/lib/action_dispatch/http/parameters.rb +28 -28
  64. data/lib/action_dispatch/http/rack_cache.rb +2 -3
  65. data/lib/action_dispatch/http/request.rb +64 -18
  66. data/lib/action_dispatch/http/response.rb +130 -35
  67. data/lib/action_dispatch/http/upload.rb +63 -20
  68. data/lib/action_dispatch/http/url.rb +98 -35
  69. data/lib/action_dispatch/journey/backwards.rb +5 -0
  70. data/lib/action_dispatch/journey/formatter.rb +146 -0
  71. data/lib/action_dispatch/journey/gtg/builder.rb +162 -0
  72. data/lib/action_dispatch/journey/gtg/simulator.rb +44 -0
  73. data/lib/action_dispatch/journey/gtg/transition_table.rb +156 -0
  74. data/lib/action_dispatch/journey/nfa/builder.rb +76 -0
  75. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  76. data/lib/action_dispatch/journey/nfa/simulator.rb +47 -0
  77. data/lib/action_dispatch/journey/nfa/transition_table.rb +163 -0
  78. data/lib/action_dispatch/journey/nodes/node.rb +124 -0
  79. data/lib/action_dispatch/journey/parser.rb +206 -0
  80. data/lib/action_dispatch/journey/parser.y +47 -0
  81. data/lib/action_dispatch/journey/parser_extras.rb +23 -0
  82. data/lib/action_dispatch/journey/path/pattern.rb +196 -0
  83. data/lib/action_dispatch/journey/route.rb +124 -0
  84. data/lib/action_dispatch/journey/router/strexp.rb +24 -0
  85. data/lib/action_dispatch/journey/router/utils.rb +54 -0
  86. data/lib/action_dispatch/journey/router.rb +166 -0
  87. data/lib/action_dispatch/journey/routes.rb +75 -0
  88. data/lib/action_dispatch/journey/scanner.rb +61 -0
  89. data/lib/action_dispatch/journey/visitors.rb +197 -0
  90. data/lib/action_dispatch/journey/visualizer/fsm.css +34 -0
  91. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  92. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  93. data/lib/action_dispatch/journey.rb +5 -0
  94. data/lib/action_dispatch/middleware/callbacks.rb +9 -4
  95. data/lib/action_dispatch/middleware/cookies.rb +259 -114
  96. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -17
  97. data/lib/action_dispatch/middleware/exception_wrapper.rb +29 -3
  98. data/lib/action_dispatch/middleware/flash.rb +58 -58
  99. data/lib/action_dispatch/middleware/params_parser.rb +14 -29
  100. data/lib/action_dispatch/middleware/public_exceptions.rb +30 -14
  101. data/lib/action_dispatch/middleware/reloader.rb +6 -6
  102. data/lib/action_dispatch/middleware/remote_ip.rb +145 -39
  103. data/lib/action_dispatch/middleware/request_id.rb +2 -6
  104. data/lib/action_dispatch/middleware/session/abstract_store.rb +22 -20
  105. data/lib/action_dispatch/middleware/session/cookie_store.rb +82 -28
  106. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -3
  107. data/lib/action_dispatch/middleware/show_exceptions.rb +12 -45
  108. data/lib/action_dispatch/middleware/ssl.rb +70 -0
  109. data/lib/action_dispatch/middleware/stack.rb +6 -1
  110. data/lib/action_dispatch/middleware/static.rb +2 -1
  111. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +14 -11
  112. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +25 -0
  113. data/lib/action_dispatch/middleware/templates/rescues/_trace.erb +7 -9
  114. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +15 -9
  115. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +127 -5
  116. data/lib/action_dispatch/middleware/templates/rescues/missing_template.erb +7 -2
  117. data/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +30 -15
  118. data/lib/action_dispatch/middleware/templates/rescues/template_error.erb +39 -13
  119. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb +6 -2
  120. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  121. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +144 -0
  122. data/lib/action_dispatch/railtie.rb +16 -6
  123. data/lib/action_dispatch/request/session.rb +181 -0
  124. data/lib/action_dispatch/routing/inspector.rb +240 -0
  125. data/lib/action_dispatch/routing/mapper.rb +540 -291
  126. data/lib/action_dispatch/routing/polymorphic_routes.rb +16 -20
  127. data/lib/action_dispatch/routing/redirection.rb +46 -29
  128. data/lib/action_dispatch/routing/route_set.rb +207 -164
  129. data/lib/action_dispatch/routing/routes_proxy.rb +2 -0
  130. data/lib/action_dispatch/routing/url_for.rb +48 -33
  131. data/lib/action_dispatch/routing.rb +48 -83
  132. data/lib/action_dispatch/testing/assertions/dom.rb +3 -13
  133. data/lib/action_dispatch/testing/assertions/response.rb +32 -40
  134. data/lib/action_dispatch/testing/assertions/routing.rb +42 -41
  135. data/lib/action_dispatch/testing/assertions/selector.rb +17 -22
  136. data/lib/action_dispatch/testing/assertions/tag.rb +20 -23
  137. data/lib/action_dispatch/testing/integration.rb +65 -51
  138. data/lib/action_dispatch/testing/test_process.rb +9 -6
  139. data/lib/action_dispatch/testing/test_request.rb +7 -3
  140. data/lib/action_dispatch.rb +21 -15
  141. data/lib/action_pack/version.rb +7 -6
  142. data/lib/action_pack.rb +1 -1
  143. data/lib/action_view/base.rb +15 -34
  144. data/lib/action_view/buffers.rb +7 -1
  145. data/lib/action_view/context.rb +4 -4
  146. data/lib/action_view/dependency_tracker.rb +93 -0
  147. data/lib/action_view/digestor.rb +85 -0
  148. data/lib/action_view/flows.rb +1 -4
  149. data/lib/action_view/helpers/active_model_helper.rb +3 -4
  150. data/lib/action_view/helpers/asset_tag_helper.rb +215 -352
  151. data/lib/action_view/helpers/asset_url_helper.rb +355 -0
  152. data/lib/action_view/helpers/atom_feed_helper.rb +13 -10
  153. data/lib/action_view/helpers/cache_helper.rb +150 -18
  154. data/lib/action_view/helpers/capture_helper.rb +44 -31
  155. data/lib/action_view/helpers/csrf_helper.rb +0 -2
  156. data/lib/action_view/helpers/date_helper.rb +269 -248
  157. data/lib/action_view/helpers/debug_helper.rb +10 -11
  158. data/lib/action_view/helpers/form_helper.rb +931 -537
  159. data/lib/action_view/helpers/form_options_helper.rb +341 -166
  160. data/lib/action_view/helpers/form_tag_helper.rb +190 -90
  161. data/lib/action_view/helpers/javascript_helper.rb +23 -16
  162. data/lib/action_view/helpers/number_helper.rb +148 -329
  163. data/lib/action_view/helpers/output_safety_helper.rb +3 -3
  164. data/lib/action_view/helpers/record_tag_helper.rb +17 -22
  165. data/lib/action_view/helpers/rendering_helper.rb +2 -2
  166. data/lib/action_view/helpers/sanitize_helper.rb +3 -6
  167. data/lib/action_view/helpers/tag_helper.rb +46 -33
  168. data/lib/action_view/helpers/tags/base.rb +147 -0
  169. data/lib/action_view/helpers/tags/check_box.rb +64 -0
  170. data/lib/action_view/helpers/tags/checkable.rb +16 -0
  171. data/lib/action_view/helpers/tags/collection_check_boxes.rb +43 -0
  172. data/lib/action_view/helpers/tags/collection_helpers.rb +83 -0
  173. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +36 -0
  174. data/lib/action_view/helpers/tags/collection_select.rb +28 -0
  175. data/lib/action_view/helpers/tags/color_field.rb +25 -0
  176. data/lib/action_view/helpers/tags/date_field.rb +13 -0
  177. data/lib/action_view/helpers/tags/date_select.rb +72 -0
  178. data/lib/action_view/helpers/tags/datetime_field.rb +22 -0
  179. data/lib/action_view/helpers/tags/datetime_local_field.rb +19 -0
  180. data/lib/action_view/helpers/tags/datetime_select.rb +8 -0
  181. data/lib/action_view/helpers/tags/email_field.rb +8 -0
  182. data/lib/action_view/helpers/tags/file_field.rb +8 -0
  183. data/lib/action_view/helpers/tags/grouped_collection_select.rb +29 -0
  184. data/lib/action_view/helpers/tags/hidden_field.rb +8 -0
  185. data/lib/action_view/helpers/tags/label.rb +65 -0
  186. data/lib/action_view/helpers/tags/month_field.rb +13 -0
  187. data/lib/action_view/helpers/tags/number_field.rb +18 -0
  188. data/lib/action_view/helpers/tags/password_field.rb +12 -0
  189. data/lib/action_view/helpers/tags/radio_button.rb +31 -0
  190. data/lib/action_view/helpers/tags/range_field.rb +8 -0
  191. data/lib/action_view/helpers/tags/search_field.rb +24 -0
  192. data/lib/action_view/helpers/tags/select.rb +40 -0
  193. data/lib/action_view/helpers/tags/tel_field.rb +8 -0
  194. data/lib/action_view/helpers/tags/text_area.rb +18 -0
  195. data/lib/action_view/helpers/tags/text_field.rb +29 -0
  196. data/lib/action_view/helpers/tags/time_field.rb +13 -0
  197. data/lib/action_view/helpers/tags/time_select.rb +8 -0
  198. data/lib/action_view/helpers/tags/time_zone_select.rb +20 -0
  199. data/lib/action_view/helpers/tags/url_field.rb +8 -0
  200. data/lib/action_view/helpers/tags/week_field.rb +13 -0
  201. data/lib/action_view/helpers/tags.rb +39 -0
  202. data/lib/action_view/helpers/text_helper.rb +130 -114
  203. data/lib/action_view/helpers/translation_helper.rb +32 -16
  204. data/lib/action_view/helpers/url_helper.rb +211 -270
  205. data/lib/action_view/helpers.rb +2 -4
  206. data/lib/action_view/locale/en.yml +1 -105
  207. data/lib/action_view/log_subscriber.rb +6 -4
  208. data/lib/action_view/lookup_context.rb +15 -28
  209. data/lib/action_view/model_naming.rb +12 -0
  210. data/lib/action_view/path_set.rb +8 -20
  211. data/lib/action_view/railtie.rb +6 -22
  212. data/lib/action_view/record_identifier.rb +84 -0
  213. data/lib/action_view/renderer/abstract_renderer.rb +25 -19
  214. data/lib/action_view/renderer/partial_renderer.rb +158 -81
  215. data/lib/action_view/renderer/renderer.rb +8 -12
  216. data/lib/action_view/renderer/streaming_template_renderer.rb +2 -5
  217. data/lib/action_view/renderer/template_renderer.rb +12 -10
  218. data/lib/action_view/routing_url_for.rb +107 -0
  219. data/lib/action_view/template/error.rb +22 -12
  220. data/lib/action_view/template/handlers/builder.rb +1 -1
  221. data/lib/action_view/template/handlers/erb.rb +40 -19
  222. data/lib/action_view/template/handlers/raw.rb +11 -0
  223. data/lib/action_view/template/handlers.rb +12 -9
  224. data/lib/action_view/template/resolver.rb +107 -53
  225. data/lib/action_view/template/text.rb +12 -8
  226. data/lib/action_view/template/types.rb +57 -0
  227. data/lib/action_view/template.rb +25 -23
  228. data/lib/action_view/test_case.rb +67 -42
  229. data/lib/{action_controller → action_view}/vendor/html-scanner/html/document.rb +0 -0
  230. data/lib/{action_controller → action_view}/vendor/html-scanner/html/node.rb +12 -12
  231. data/lib/{action_controller → action_view}/vendor/html-scanner/html/sanitizer.rb +13 -2
  232. data/lib/{action_controller → action_view}/vendor/html-scanner/html/selector.rb +9 -9
  233. data/lib/{action_controller → action_view}/vendor/html-scanner/html/tokenizer.rb +1 -1
  234. data/lib/{action_controller → action_view}/vendor/html-scanner/html/version.rb +0 -0
  235. data/lib/action_view/vendor/html-scanner.rb +20 -0
  236. data/lib/action_view.rb +17 -8
  237. metadata +184 -214
  238. data/lib/action_controller/caching/actions.rb +0 -185
  239. data/lib/action_controller/caching/pages.rb +0 -187
  240. data/lib/action_controller/caching/sweeping.rb +0 -97
  241. data/lib/action_controller/deprecated/performance_test.rb +0 -1
  242. data/lib/action_controller/metal/compatibility.rb +0 -65
  243. data/lib/action_controller/metal/session_management.rb +0 -14
  244. data/lib/action_controller/railties/paths.rb +0 -25
  245. data/lib/action_dispatch/middleware/best_standards_support.rb +0 -30
  246. data/lib/action_dispatch/middleware/body_proxy.rb +0 -30
  247. data/lib/action_dispatch/middleware/head.rb +0 -18
  248. data/lib/action_dispatch/middleware/rescue.rb +0 -26
  249. data/lib/action_dispatch/testing/performance_test.rb +0 -10
  250. data/lib/action_view/asset_paths.rb +0 -142
  251. data/lib/action_view/helpers/asset_paths.rb +0 -7
  252. data/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb +0 -146
  253. data/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +0 -93
  254. data/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +0 -193
  255. data/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +0 -148
  256. data/lib/sprockets/assets.rake +0 -99
  257. data/lib/sprockets/bootstrap.rb +0 -37
  258. data/lib/sprockets/compressors.rb +0 -83
  259. data/lib/sprockets/helpers/isolated_helper.rb +0 -13
  260. data/lib/sprockets/helpers/rails_helper.rb +0 -182
  261. data/lib/sprockets/helpers.rb +0 -6
  262. data/lib/sprockets/railtie.rb +0 -62
  263. data/lib/sprockets/static_compiler.rb +0 -56
@@ -1,13 +1,19 @@
1
1
  require 'active_support/core_ext/hash/except'
2
- require 'active_support/core_ext/object/blank'
3
- require 'active_support/core_ext/object/inclusion'
2
+ require 'active_support/core_ext/hash/reverse_merge'
3
+ require 'active_support/core_ext/hash/slice'
4
4
  require 'active_support/core_ext/enumerable'
5
+ require 'active_support/core_ext/array/extract_options'
5
6
  require 'active_support/inflector'
6
7
  require 'action_dispatch/routing/redirection'
7
8
 
8
9
  module ActionDispatch
9
10
  module Routing
10
11
  class Mapper
12
+ URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
13
+ SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
14
+ :controller, :action, :path_names, :constraints,
15
+ :shallow, :blocks, :defaults, :options]
16
+
11
17
  class Constraints #:nodoc:
12
18
  def self.new(app, constraints, request = Rack::Request)
13
19
  if constraints.any?
@@ -26,15 +32,10 @@ module ActionDispatch
26
32
  def matches?(env)
27
33
  req = @request.new(env)
28
34
 
29
- @constraints.each { |constraint|
30
- if constraint.respond_to?(:matches?) && !constraint.matches?(req)
31
- return false
32
- elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req))
33
- return false
34
- end
35
- }
36
-
37
- return true
35
+ @constraints.all? do |constraint|
36
+ (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
37
+ (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
38
+ end
38
39
  ensure
39
40
  req.reset_parameters
40
41
  end
@@ -50,100 +51,154 @@ module ActionDispatch
50
51
  end
51
52
 
52
53
  class Mapping #:nodoc:
53
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
54
+ IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
54
55
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
55
56
  WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
56
57
 
58
+ attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
59
+
57
60
  def initialize(set, scope, path, options)
58
- @set, @scope = set, scope
59
- @options = (@scope[:options] || {}).merge(options)
60
- @path = normalize_path(path)
61
+ @set, @scope, @path, @options = set, scope, path, options
62
+ @requirements, @conditions, @defaults = {}, {}, {}
63
+
61
64
  normalize_options!
65
+ normalize_path!
66
+ normalize_requirements!
67
+ normalize_conditions!
68
+ normalize_defaults!
62
69
  end
63
70
 
64
71
  def to_route
65
- [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
72
+ [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
66
73
  end
67
74
 
68
75
  private
69
76
 
70
- def normalize_options!
71
- @options.merge!(default_controller_and_action)
72
-
73
- requirements.each do |name, requirement|
74
- # segment_keys.include?(k.to_s) || k == :controller
75
- next unless Regexp === requirement && !constraints[name]
77
+ def normalize_path!
78
+ raise ArgumentError, "path is required" if @path.blank?
79
+ @path = Mapper.normalize_path(@path)
76
80
 
77
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
78
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
79
- end
80
- if requirement.multiline?
81
- raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
82
- end
81
+ if required_format?
82
+ @path = "#{@path}.:format"
83
+ elsif optional_format?
84
+ @path = "#{@path}(.:format)"
83
85
  end
84
86
  end
85
87
 
86
- # match "account/overview"
87
- def using_match_shorthand?(path, options)
88
- path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
88
+ def required_format?
89
+ options[:format] == true
89
90
  end
90
91
 
91
- def normalize_path(path)
92
- raise ArgumentError, "path is required" if path.blank?
93
- path = Mapper.normalize_path(path)
92
+ def optional_format?
93
+ options[:format] != false && !path.include?(':format') && !path.end_with?('/')
94
+ end
94
95
 
95
- if path.match(':controller')
96
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
96
+ def normalize_options!
97
+ @options.reverse_merge!(scope[:options]) if scope[:options]
98
+ path_without_format = path.sub(/\(\.:format\)$/, '')
99
+
100
+ # Add a constraint for wildcard route to make it non-greedy and match the
101
+ # optional format part of the route by default
102
+ if path_without_format.match(WILDCARD_PATH) && @options[:format] != false
103
+ @options[$1.to_sym] ||= /.+?/
104
+ end
105
+
106
+ if path_without_format.match(':controller')
107
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]
97
108
 
98
109
  # Add a default constraint for :controller path segments that matches namespaced
99
110
  # controllers with default routes like :controller/:action/:id(.:format), e.g:
100
111
  # GET /admin/products/show/1
101
- # => { :controller => 'admin/products', :action => 'show', :id => '1' }
112
+ # => { controller: 'admin/products', action: 'show', id: '1' }
102
113
  @options[:controller] ||= /.+?/
103
114
  end
104
115
 
105
- # Add a constraint for wildcard route to make it non-greedy and match the
106
- # optional format part of the route by default
107
- if path.match(WILDCARD_PATH) && @options[:format] != false
108
- @options[$1.to_sym] ||= /.+?/
116
+ @options.merge!(default_controller_and_action)
117
+ end
118
+
119
+ def normalize_requirements!
120
+ constraints.each do |key, requirement|
121
+ next unless segment_keys.include?(key) || key == :controller
122
+ verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
123
+ @requirements[key] = requirement
109
124
  end
110
125
 
111
- if @options[:format] == false
112
- @options.delete(:format)
113
- path
114
- elsif path.include?(":format") || path.end_with?('/')
115
- path
116
- elsif @options[:format] == true
117
- "#{path}.:format"
118
- else
119
- "#{path}(.:format)"
126
+ if options[:format] == true
127
+ @requirements[:format] ||= /.+/
128
+ elsif Regexp === options[:format]
129
+ @requirements[:format] = options[:format]
130
+ elsif String === options[:format]
131
+ @requirements[:format] = Regexp.compile(options[:format])
120
132
  end
121
133
  end
122
134
 
123
- def app
124
- Constraints.new(
125
- to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
126
- blocks,
127
- @set.request_class
128
- )
129
- end
135
+ def verify_regexp_requirement(requirement)
136
+ if requirement.source =~ ANCHOR_CHARACTERS_REGEX
137
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
138
+ end
130
139
 
131
- def conditions
132
- { :path_info => @path }.merge(constraints).merge(request_method_condition)
140
+ if requirement.multiline?
141
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
142
+ end
133
143
  end
134
144
 
135
- def requirements
136
- @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
137
- requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
138
- @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
145
+ def normalize_defaults!
146
+ @defaults.merge!(scope[:defaults]) if scope[:defaults]
147
+ @defaults.merge!(options[:defaults]) if options[:defaults]
148
+
149
+ options.each do |key, default|
150
+ next if Regexp === default || IGNORE_OPTIONS.include?(key)
151
+ @defaults[key] = default
152
+ end
153
+
154
+ if options[:constraints].is_a?(Hash)
155
+ options[:constraints].each do |key, default|
156
+ next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
157
+ @defaults[key] ||= default
158
+ end
159
+ end
160
+
161
+ if Regexp === options[:format]
162
+ @defaults[:format] = nil
163
+ elsif String === options[:format]
164
+ @defaults[:format] = options[:format]
139
165
  end
140
166
  end
141
167
 
142
- def defaults
143
- @defaults ||= (@options[:defaults] || {}).tap do |defaults|
144
- defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults]
145
- @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) }
168
+ def normalize_conditions!
169
+ @conditions.merge!(:path_info => path)
170
+
171
+ constraints.each do |key, condition|
172
+ next if segment_keys.include?(key) || key == :controller
173
+ @conditions[key] = condition
174
+ end
175
+
176
+ @conditions[:required_defaults] = []
177
+ options.each do |key, required_default|
178
+ next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key)
179
+ next if Regexp === required_default
180
+ @conditions[:required_defaults] << key
146
181
  end
182
+
183
+ via_all = options.delete(:via) if options[:via] == :all
184
+
185
+ if !via_all && options[:via].blank?
186
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
187
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
188
+ "If you want to expose your action to GET, use `get` in the router:\n" \
189
+ " Instead of: match \"controller#action\"\n" \
190
+ " Do: get \"controller#action\""
191
+ raise msg
192
+ end
193
+
194
+ if via = options[:via]
195
+ list = Array(via).map { |m| m.to_s.dasherize.upcase }
196
+ @conditions.merge!(:request_method => list)
197
+ end
198
+ end
199
+
200
+ def app
201
+ Constraints.new(endpoint, blocks, @set.request_class)
147
202
  end
148
203
 
149
204
  def default_controller_and_action
@@ -170,14 +225,20 @@ module ActionDispatch
170
225
  controller = controller.to_s unless controller.is_a?(Regexp)
171
226
  action = action.to_s unless action.is_a?(Regexp)
172
227
 
173
- if controller.blank? && segment_keys.exclude?("controller")
228
+ if controller.blank? && segment_keys.exclude?(:controller)
174
229
  raise ArgumentError, "missing :controller"
175
230
  end
176
231
 
177
- if action.blank? && segment_keys.exclude?("action")
232
+ if action.blank? && segment_keys.exclude?(:action)
178
233
  raise ArgumentError, "missing :action"
179
234
  end
180
235
 
236
+ if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
237
+ message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems."
238
+ message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
239
+ raise ArgumentError, message
240
+ end
241
+
181
242
  hash = {}
182
243
  hash[:controller] = controller unless controller.blank?
183
244
  hash[:action] = action unless action.blank?
@@ -186,47 +247,59 @@ module ActionDispatch
186
247
  end
187
248
 
188
249
  def blocks
189
- constraints = @options[:constraints]
190
- if constraints.present? && !constraints.is_a?(Hash)
191
- [constraints]
250
+ if options[:constraints].present? && !options[:constraints].is_a?(Hash)
251
+ [options[:constraints]]
192
252
  else
193
- @scope[:blocks] || []
253
+ scope[:blocks] || []
194
254
  end
195
255
  end
196
256
 
197
257
  def constraints
198
- @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
199
- end
258
+ @constraints ||= {}.tap do |constraints|
259
+ constraints.merge!(scope[:constraints]) if scope[:constraints]
200
260
 
201
- def request_method_condition
202
- if via = @options[:via]
203
- list = Array(via).map { |m| m.to_s.dasherize.upcase }
204
- { :request_method => list }
205
- else
206
- { }
261
+ options.except(*IGNORE_OPTIONS).each do |key, option|
262
+ constraints[key] = option if Regexp === option
263
+ end
264
+
265
+ constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
207
266
  end
208
267
  end
209
268
 
210
269
  def segment_keys
211
- @segment_keys ||= Journey::Path::Pattern.new(
212
- Journey::Router::Strexp.compile(@path, requirements, SEPARATORS)
213
- ).names
270
+ @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
271
+ end
272
+
273
+ def path_pattern
274
+ Journey::Path::Pattern.new(strexp)
275
+ end
276
+
277
+ def strexp
278
+ Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
279
+ end
280
+
281
+ def endpoint
282
+ to.respond_to?(:call) ? to : dispatcher
283
+ end
284
+
285
+ def dispatcher
286
+ Routing::RouteSet::Dispatcher.new(:defaults => defaults)
214
287
  end
215
288
 
216
289
  def to
217
- @options[:to]
290
+ options[:to]
218
291
  end
219
292
 
220
293
  def default_controller
221
- @options[:controller] || @scope[:controller]
294
+ options[:controller] || scope[:controller]
222
295
  end
223
296
 
224
297
  def default_action
225
- @options[:action] || @scope[:action]
298
+ options[:action] || scope[:action]
226
299
  end
227
300
  end
228
301
 
229
- # Invokes Rack::Mount::Utils.normalize path and ensure that
302
+ # Invokes Journey::Router::Utils.normalize_path and ensure that
230
303
  # (:locale) becomes (/:locale) instead of /(:locale). Except
231
304
  # for root cases, where the latter is the correct one.
232
305
  def self.normalize_path(path)
@@ -236,21 +309,25 @@ module ActionDispatch
236
309
  end
237
310
 
238
311
  def self.normalize_name(name)
239
- normalize_path(name)[1..-1].gsub("/", "_")
312
+ normalize_path(name)[1..-1].tr("/", "_")
240
313
  end
241
314
 
242
315
  module Base
243
316
  # You can specify what Rails should route "/" to with the root method:
244
317
  #
245
- # root :to => 'pages#main'
318
+ # root to: 'pages#main'
246
319
  #
247
320
  # For options, see +match+, as +root+ uses it internally.
248
321
  #
322
+ # You can also pass a string which will expand
323
+ #
324
+ # root 'pages#main'
325
+ #
249
326
  # You should put the root route at the top of <tt>config/routes.rb</tt>,
250
327
  # because this means it will be matched first. As this is the most popular route
251
328
  # of most Rails applications, this is beneficial.
252
329
  def root(options = {})
253
- match '/', { :as => :root }.merge(options)
330
+ match '/', { :as => :root, :via => :get }.merge!(options)
254
331
  end
255
332
 
256
333
  # Matches a url pattern to one or more routes. Any symbols in a pattern
@@ -264,7 +341,7 @@ module ActionDispatch
264
341
  # and +:action+ to the controller's action. A pattern can also map
265
342
  # wildcard segments (globs) to params:
266
343
  #
267
- # match 'songs/*category/:title' => 'songs#show'
344
+ # match 'songs/*category/:title', to: 'songs#show'
268
345
  #
269
346
  # # 'songs/rock/classic/stairway-to-heaven' sets
270
347
  # # params[:category] = 'rock/classic'
@@ -274,16 +351,20 @@ module ActionDispatch
274
351
  # +:controller+ should be set in options or hash shorthand. Examples:
275
352
  #
276
353
  # match 'photos/:id' => 'photos#show'
277
- # match 'photos/:id', :to => 'photos#show'
278
- # match 'photos/:id', :controller => 'photos', :action => 'show'
354
+ # match 'photos/:id', to: 'photos#show'
355
+ # match 'photos/:id', controller: 'photos', action: 'show'
279
356
  #
280
357
  # A pattern can also point to a +Rack+ endpoint i.e. anything that
281
358
  # responds to +call+:
282
359
  #
283
- # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] }
284
- # match 'photos/:id' => PhotoRackApp
360
+ # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }
361
+ # match 'photos/:id', to: PhotoRackApp
285
362
  # # Yes, controller actions are just rack endpoints
286
- # match 'photos/:id' => PhotosController.action(:show)
363
+ # match 'photos/:id', to: PhotosController.action(:show)
364
+ #
365
+ # Because request various HTTP verbs with a single action has security
366
+ # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers]
367
+ # instead +match+
287
368
  #
288
369
  # === Options
289
370
  #
@@ -301,7 +382,7 @@ module ActionDispatch
301
382
  # [:module]
302
383
  # The namespace for :controller.
303
384
  #
304
- # match 'path' => 'c#a', :module => 'sekret', :controller => 'posts'
385
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
305
386
  # #=> Sekret::PostsController
306
387
  #
307
388
  # See <tt>Scoping#namespace</tt> for its scope equivalent.
@@ -312,16 +393,17 @@ module ActionDispatch
312
393
  # [:via]
313
394
  # Allowed HTTP verb(s) for route.
314
395
  #
315
- # match 'path' => 'c#a', :via => :get
316
- # match 'path' => 'c#a', :via => [:get, :post]
396
+ # match 'path', to: 'c#a', via: :get
397
+ # match 'path', to: 'c#a', via: [:get, :post]
398
+ # match 'path', to: 'c#a', via: :all
317
399
  #
318
400
  # [:to]
319
401
  # Points to a +Rack+ endpoint. Can be an object that responds to
320
402
  # +call+ or a string representing a controller's action.
321
403
  #
322
- # match 'path', :to => 'controller#action'
323
- # match 'path', :to => lambda { |env| [200, {}, "Success!"] }
324
- # match 'path', :to => RackApp
404
+ # match 'path', to: 'controller#action'
405
+ # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }
406
+ # match 'path', to: RackApp
325
407
  #
326
408
  # [:on]
327
409
  # Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -329,27 +411,31 @@ module ActionDispatch
329
411
  # <tt>resource(s)</tt> block. For example:
330
412
  #
331
413
  # resource :bar do
332
- # match 'foo' => 'c#a', :on => :member, :via => [:get, :post]
414
+ # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
333
415
  # end
334
416
  #
335
417
  # Is equivalent to:
336
418
  #
337
419
  # resource :bar do
338
420
  # member do
339
- # match 'foo' => 'c#a', :via => [:get, :post]
421
+ # match 'foo', to: 'c#a', via: [:get, :post]
340
422
  # end
341
423
  # end
342
424
  #
343
425
  # [:constraints]
344
- # Constrains parameters with a hash of regular expressions or an
345
- # object that responds to <tt>matches?</tt>
426
+ # Constrains parameters with a hash of regular expressions
427
+ # or an object that responds to <tt>matches?</tt>. In addition, constraints
428
+ # other than path can also be specified with any object
429
+ # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
346
430
  #
347
- # match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ }
431
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }
432
+ #
433
+ # match 'json_only', constraints: { format: 'json' }
348
434
  #
349
435
  # class Blacklist
350
436
  # def matches?(request) request.remote_ip == '1.2.3.4' end
351
437
  # end
352
- # match 'path' => 'c#a', :constraints => Blacklist.new
438
+ # match 'path', to: 'c#a', constraints: Blacklist.new
353
439
  #
354
440
  # See <tt>Scoping#constraints</tt> for more examples with its scope
355
441
  # equivalent.
@@ -358,7 +444,7 @@ module ActionDispatch
358
444
  # Sets defaults for parameters
359
445
  #
360
446
  # # Sets params[:format] to 'jpg' by default
361
- # match 'path' => 'c#a', :defaults => { :format => 'jpg' }
447
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }
362
448
  #
363
449
  # See <tt>Scoping#defaults</tt> for its scope equivalent.
364
450
  #
@@ -367,13 +453,17 @@ module ActionDispatch
367
453
  # false, the pattern matches any request prefixed with the given path.
368
454
  #
369
455
  # # Matches any request starting with 'path'
370
- # match 'path' => 'c#a', :anchor => false
456
+ # match 'path', to: 'c#a', anchor: false
457
+ #
458
+ # [:format]
459
+ # Allows you to specify the default value for optional +format+
460
+ # segment or disable it by supplying +false+.
371
461
  def match(path, options=nil)
372
462
  end
373
463
 
374
464
  # Mount a Rack-based application to be used within the application.
375
465
  #
376
- # mount SomeRackApp, :at => "some_route"
466
+ # mount SomeRackApp, at: "some_route"
377
467
  #
378
468
  # Alternatively:
379
469
  #
@@ -386,7 +476,7 @@ module ActionDispatch
386
476
  # the helper is either +some_rack_app_path+ or +some_rack_app_url+.
387
477
  # To customize this helper's name, use the +:as+ option:
388
478
  #
389
- # mount(SomeRackApp => "some_route", :as => "exciting")
479
+ # mount(SomeRackApp => "some_route", as: "exciting")
390
480
  #
391
481
  # This will generate the +exciting_path+ and +exciting_url+ helpers
392
482
  # which can be used to navigate to this mounted app.
@@ -394,14 +484,19 @@ module ActionDispatch
394
484
  if options
395
485
  path = options.delete(:at)
396
486
  else
487
+ unless Hash === app
488
+ raise ArgumentError, "must be called with mount point"
489
+ end
490
+
397
491
  options = app
398
- app, path = options.find { |k, v| k.respond_to?(:call) }
492
+ app, path = options.find { |k, _| k.respond_to?(:call) }
399
493
  options.delete(app) if app
400
494
  end
401
495
 
402
496
  raise "A rack application must be specified" unless path
403
497
 
404
- options[:as] ||= app_name(app)
498
+ options[:as] ||= app_name(app)
499
+ options[:via] ||= :all
405
500
 
406
501
  match(path, options.merge(:to => app, :anchor => false, :format => false))
407
502
 
@@ -428,7 +523,7 @@ module ActionDispatch
428
523
  app.railtie_name
429
524
  else
430
525
  class_name = app.class.is_a?(Class) ? app.name : app.class.name
431
- ActiveSupport::Inflector.underscore(class_name).gsub("/", "_")
526
+ ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
432
527
  end
433
528
  end
434
529
 
@@ -438,14 +533,16 @@ module ActionDispatch
438
533
  _route = @set.named_routes.routes[name.to_sym]
439
534
  _routes = @set
440
535
  app.routes.define_mounted_helper(name)
441
- app.routes.class_eval do
536
+ app.routes.singleton_class.class_eval do
537
+ define_method :mounted? do
538
+ true
539
+ end
540
+
442
541
  define_method :_generate_prefix do |options|
443
542
  prefix_options = options.slice(*_route.segment_keys)
444
543
  # we must actually delete prefix segment keys to avoid passing them to next url_for
445
544
  _route.segment_keys.each { |k| options.delete(k) }
446
- prefix = _routes.url_helpers.send("#{name}_path", prefix_options)
447
- prefix = prefix.gsub(%r{/\z}, '')
448
- prefix
545
+ _routes.url_helpers.send("#{name}_path", prefix_options)
449
546
  end
450
547
  end
451
548
  end
@@ -453,51 +550,50 @@ module ActionDispatch
453
550
 
454
551
  module HttpHelpers
455
552
  # Define a route that only recognizes HTTP GET.
456
- # For supported arguments, see <tt>Base#match</tt>.
457
- #
458
- # Example:
553
+ # For supported arguments, see match[rdoc-ref:Base#match]
459
554
  #
460
- # get 'bacon', :to => 'food#bacon'
555
+ # get 'bacon', to: 'food#bacon'
461
556
  def get(*args, &block)
462
- map_method(:get, *args, &block)
557
+ map_method(:get, args, &block)
463
558
  end
464
559
 
465
560
  # Define a route that only recognizes HTTP POST.
466
- # For supported arguments, see <tt>Base#match</tt>.
467
- #
468
- # Example:
561
+ # For supported arguments, see match[rdoc-ref:Base#match]
469
562
  #
470
- # post 'bacon', :to => 'food#bacon'
563
+ # post 'bacon', to: 'food#bacon'
471
564
  def post(*args, &block)
472
- map_method(:post, *args, &block)
565
+ map_method(:post, args, &block)
473
566
  end
474
567
 
475
- # Define a route that only recognizes HTTP PUT.
476
- # For supported arguments, see <tt>Base#match</tt>.
477
- #
478
- # Example:
568
+ # Define a route that only recognizes HTTP PATCH.
569
+ # For supported arguments, see match[rdoc-ref:Base#match]
479
570
  #
480
- # put 'bacon', :to => 'food#bacon'
481
- def put(*args, &block)
482
- map_method(:put, *args, &block)
571
+ # patch 'bacon', to: 'food#bacon'
572
+ def patch(*args, &block)
573
+ map_method(:patch, args, &block)
483
574
  end
484
575
 
485
576
  # Define a route that only recognizes HTTP PUT.
486
- # For supported arguments, see <tt>Base#match</tt>.
577
+ # For supported arguments, see match[rdoc-ref:Base#match]
487
578
  #
488
- # Example:
579
+ # put 'bacon', to: 'food#bacon'
580
+ def put(*args, &block)
581
+ map_method(:put, args, &block)
582
+ end
583
+
584
+ # Define a route that only recognizes HTTP DELETE.
585
+ # For supported arguments, see match[rdoc-ref:Base#match]
489
586
  #
490
- # delete 'broccoli', :to => 'food#broccoli'
587
+ # delete 'broccoli', to: 'food#broccoli'
491
588
  def delete(*args, &block)
492
- map_method(:delete, *args, &block)
589
+ map_method(:delete, args, &block)
493
590
  end
494
591
 
495
592
  private
496
- def map_method(method, *args, &block)
593
+ def map_method(method, args, &block)
497
594
  options = args.extract_options!
498
595
  options[:via] = method
499
- args.push(options)
500
- match(*args, &block)
596
+ match(*args, options, &block)
501
597
  self
502
598
  end
503
599
  end
@@ -515,24 +611,24 @@ module ActionDispatch
515
611
  # This will create a number of routes for each of the posts and comments
516
612
  # controller. For <tt>Admin::PostsController</tt>, Rails will create:
517
613
  #
518
- # GET /admin/posts
519
- # GET /admin/posts/new
520
- # POST /admin/posts
521
- # GET /admin/posts/1
522
- # GET /admin/posts/1/edit
523
- # PUT /admin/posts/1
524
- # DELETE /admin/posts/1
614
+ # GET /admin/posts
615
+ # GET /admin/posts/new
616
+ # POST /admin/posts
617
+ # GET /admin/posts/1
618
+ # GET /admin/posts/1/edit
619
+ # PATCH/PUT /admin/posts/1
620
+ # DELETE /admin/posts/1
525
621
  #
526
622
  # If you want to route /posts (without the prefix /admin) to
527
623
  # <tt>Admin::PostsController</tt>, you could use
528
624
  #
529
- # scope :module => "admin" do
625
+ # scope module: "admin" do
530
626
  # resources :posts
531
627
  # end
532
628
  #
533
629
  # or, for a single case
534
630
  #
535
- # resources :posts, :module => "admin"
631
+ # resources :posts, module: "admin"
536
632
  #
537
633
  # If you want to route /admin/posts to +PostsController+
538
634
  # (without the Admin:: module prefix), you could use
@@ -543,25 +639,25 @@ module ActionDispatch
543
639
  #
544
640
  # or, for a single case
545
641
  #
546
- # resources :posts, :path => "/admin/posts"
642
+ # resources :posts, path: "/admin/posts"
547
643
  #
548
644
  # In each of these cases, the named routes remain the same as if you did
549
645
  # not use scope. In the last case, the following paths map to
550
646
  # +PostsController+:
551
647
  #
552
- # GET /admin/posts
553
- # GET /admin/posts/new
554
- # POST /admin/posts
555
- # GET /admin/posts/1
556
- # GET /admin/posts/1/edit
557
- # PUT /admin/posts/1
558
- # DELETE /admin/posts/1
648
+ # GET /admin/posts
649
+ # GET /admin/posts/new
650
+ # POST /admin/posts
651
+ # GET /admin/posts/1
652
+ # GET /admin/posts/1/edit
653
+ # PATCH/PUT /admin/posts/1
654
+ # DELETE /admin/posts/1
559
655
  module Scoping
560
656
  # Scopes a set of routes to the given default options.
561
657
  #
562
658
  # Take the following route definition as an example:
563
659
  #
564
- # scope :path => ":account_id", :as => "account" do
660
+ # scope path: ":account_id", as: "account" do
565
661
  # resources :projects
566
662
  # end
567
663
  #
@@ -573,63 +669,62 @@ module ActionDispatch
573
669
  #
574
670
  # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
575
671
  #
576
- # === Examples
577
- #
578
672
  # # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
579
- # scope :module => "admin" do
673
+ # scope module: "admin" do
580
674
  # resources :posts
581
675
  # end
582
676
  #
583
677
  # # prefix the posts resource's requests with '/admin'
584
- # scope :path => "/admin" do
678
+ # scope path: "/admin" do
585
679
  # resources :posts
586
680
  # end
587
681
  #
588
682
  # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
589
- # scope :as => "sekret" do
683
+ # scope as: "sekret" do
590
684
  # resources :posts
591
685
  # end
592
686
  def scope(*args)
593
- options = args.extract_options!
594
- options = options.dup
595
-
596
- options[:path] = args.first if args.first.is_a?(String)
687
+ options = args.extract_options!.dup
597
688
  recover = {}
598
689
 
690
+ options[:path] = args.flatten.join('/') if args.any?
599
691
  options[:constraints] ||= {}
600
- unless options[:constraints].is_a?(Hash)
692
+
693
+ if options[:constraints].is_a?(Hash)
694
+ defaults = options[:constraints].select do
695
+ |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
696
+ end
697
+
698
+ (options[:defaults] ||= {}).reverse_merge!(defaults)
699
+ else
601
700
  block, options[:constraints] = options[:constraints], {}
602
701
  end
603
702
 
604
- scope_options.each do |option|
605
- if value = options.delete(option)
703
+ SCOPE_OPTIONS.each do |option|
704
+ if option == :blocks
705
+ value = block
706
+ elsif option == :options
707
+ value = options
708
+ else
709
+ value = options.delete(option)
710
+ end
711
+
712
+ if value
606
713
  recover[option] = @scope[option]
607
714
  @scope[option] = send("merge_#{option}_scope", @scope[option], value)
608
715
  end
609
716
  end
610
717
 
611
- recover[:block] = @scope[:blocks]
612
- @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
613
-
614
- recover[:options] = @scope[:options]
615
- @scope[:options] = merge_options_scope(@scope[:options], options)
616
-
617
718
  yield
618
719
  self
619
720
  ensure
620
- scope_options.each do |option|
621
- @scope[option] = recover[option] if recover.has_key?(option)
622
- end
623
-
624
- @scope[:options] = recover[:options]
625
- @scope[:blocks] = recover[:block]
721
+ @scope.merge!(recover)
626
722
  end
627
723
 
628
724
  # Scopes routes to a specific controller
629
725
  #
630
- # Example:
631
726
  # controller "food" do
632
- # match "bacon", :action => "bacon"
727
+ # match "bacon", action: "bacon"
633
728
  # end
634
729
  def controller(controller, options={})
635
730
  options[:controller] = controller
@@ -644,13 +739,13 @@ module ActionDispatch
644
739
  #
645
740
  # This generates the following routes:
646
741
  #
647
- # admin_posts GET /admin/posts(.:format) admin/posts#index
648
- # admin_posts POST /admin/posts(.:format) admin/posts#create
649
- # new_admin_post GET /admin/posts/new(.:format) admin/posts#new
650
- # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
651
- # admin_post GET /admin/posts/:id(.:format) admin/posts#show
652
- # admin_post PUT /admin/posts/:id(.:format) admin/posts#update
653
- # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
742
+ # admin_posts GET /admin/posts(.:format) admin/posts#index
743
+ # admin_posts POST /admin/posts(.:format) admin/posts#create
744
+ # new_admin_post GET /admin/posts/new(.:format) admin/posts#new
745
+ # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
746
+ # admin_post GET /admin/posts/:id(.:format) admin/posts#show
747
+ # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
748
+ # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
654
749
  #
655
750
  # === Options
656
751
  #
@@ -660,20 +755,18 @@ module ActionDispatch
660
755
  # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
661
756
  # <tt>Resources#resources</tt>.
662
757
  #
663
- # === Examples
664
- #
665
758
  # # accessible through /sekret/posts rather than /admin/posts
666
- # namespace :admin, :path => "sekret" do
759
+ # namespace :admin, path: "sekret" do
667
760
  # resources :posts
668
761
  # end
669
762
  #
670
763
  # # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
671
- # namespace :admin, :module => "sekret" do
764
+ # namespace :admin, module: "sekret" do
672
765
  # resources :posts
673
766
  # end
674
767
  #
675
768
  # # generates +sekret_posts_path+ rather than +admin_posts_path+
676
- # namespace :admin, :as => "sekret" do
769
+ # namespace :admin, as: "sekret" do
677
770
  # resources :posts
678
771
  # end
679
772
  def namespace(path, options = {})
@@ -687,7 +780,7 @@ module ActionDispatch
687
780
  # Allows you to constrain the nested routes based on a set of rules.
688
781
  # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
689
782
  #
690
- # constraints(:id => /\d+\.\d+/) do
783
+ # constraints(id: /\d+\.\d+/) do
691
784
  # resources :posts
692
785
  # end
693
786
  #
@@ -697,7 +790,7 @@ module ActionDispatch
697
790
  # You may use this to also restrict other parameters:
698
791
  #
699
792
  # resources :posts do
700
- # constraints(:post_id => /\d+\.\d+/) do
793
+ # constraints(post_id: /\d+\.\d+/) do
701
794
  # resources :comments
702
795
  # end
703
796
  # end
@@ -706,7 +799,7 @@ module ActionDispatch
706
799
  #
707
800
  # Routes can also be constrained to an IP or a certain range of IP addresses:
708
801
  #
709
- # constraints(:ip => /192.168.\d+.\d+/) do
802
+ # constraints(ip: /192\.168\.\d+\.\d+/) do
710
803
  # resources :posts
711
804
  # end
712
805
  #
@@ -743,8 +836,8 @@ module ActionDispatch
743
836
  end
744
837
 
745
838
  # Allows you to set default parameters for a route, such as this:
746
- # defaults :id => 'home' do
747
- # match 'scoped_pages/(:id)', :to => 'pages#show'
839
+ # defaults id: 'home' do
840
+ # match 'scoped_pages/(:id)', to: 'pages#show'
748
841
  # end
749
842
  # Using this, the +:id+ parameter here will default to 'home'.
750
843
  def defaults(defaults = {})
@@ -752,10 +845,6 @@ module ActionDispatch
752
845
  end
753
846
 
754
847
  private
755
- def scope_options #:nodoc:
756
- @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
757
- end
758
-
759
848
  def merge_path_scope(parent, child) #:nodoc:
760
849
  Mapper.normalize_path("#{parent}/#{child}")
761
850
  end
@@ -803,7 +892,7 @@ module ActionDispatch
803
892
  end
804
893
 
805
894
  def merge_options_scope(parent, child) #:nodoc:
806
- (parent || {}).except(*override_keys(child)).merge(child)
895
+ (parent || {}).except(*override_keys(child)).merge!(child)
807
896
  end
808
897
 
809
898
  def merge_shallow_scope(parent, child) #:nodoc:
@@ -850,7 +939,7 @@ module ActionDispatch
850
939
  # use dots as part of the +:id+ parameter add a constraint which
851
940
  # overrides this restriction, e.g:
852
941
  #
853
- # resources :articles, :id => /[^\/]+/
942
+ # resources :articles, id: /[^\/]+/
854
943
  #
855
944
  # This allows any character other than a slash as part of your +:id+.
856
945
  #
@@ -858,17 +947,20 @@ module ActionDispatch
858
947
  # CANONICAL_ACTIONS holds all actions that does not need a prefix or
859
948
  # a path appended since they fit properly in their scope level.
860
949
  VALID_ON_OPTIONS = [:new, :collection, :member]
861
- RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except]
950
+ RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
862
951
  CANONICAL_ACTIONS = %w(index create new show update destroy)
952
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
953
+ RESOURCE_SCOPES = [:resource, :resources]
863
954
 
864
955
  class Resource #:nodoc:
865
- attr_reader :controller, :path, :options
956
+ attr_reader :controller, :path, :options, :param
866
957
 
867
958
  def initialize(entities, options = {})
868
959
  @name = entities.to_s
869
960
  @path = (options[:path] || @name).to_s
870
961
  @controller = (options[:controller] || @name).to_s
871
962
  @as = options[:as]
963
+ @param = (options[:param] || :id).to_sym
872
964
  @options = options
873
965
  end
874
966
 
@@ -913,15 +1005,21 @@ module ActionDispatch
913
1005
  alias :collection_scope :path
914
1006
 
915
1007
  def member_scope
916
- "#{path}/:id"
1008
+ "#{path}/:#{param}"
917
1009
  end
918
1010
 
1011
+ alias :shallow_scope :member_scope
1012
+
919
1013
  def new_scope(new_path)
920
1014
  "#{path}/#{new_path}"
921
1015
  end
922
1016
 
1017
+ def nested_param
1018
+ :"#{singular}_#{param}"
1019
+ end
1020
+
923
1021
  def nested_scope
924
- "#{path}/:#{singular}_id"
1022
+ "#{path}/:#{nested_param}"
925
1023
  end
926
1024
 
927
1025
  end
@@ -969,12 +1067,12 @@ module ActionDispatch
969
1067
  # the +GeoCoders+ controller (note that the controller is named after
970
1068
  # the plural):
971
1069
  #
972
- # GET /geocoder/new
973
- # POST /geocoder
974
- # GET /geocoder
975
- # GET /geocoder/edit
976
- # PUT /geocoder
977
- # DELETE /geocoder
1070
+ # GET /geocoder/new
1071
+ # POST /geocoder
1072
+ # GET /geocoder
1073
+ # GET /geocoder/edit
1074
+ # PATCH/PUT /geocoder
1075
+ # DELETE /geocoder
978
1076
  #
979
1077
  # === Options
980
1078
  # Takes same options as +resources+.
@@ -988,6 +1086,8 @@ module ActionDispatch
988
1086
  resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
989
1087
  yield if block_given?
990
1088
 
1089
+ concerns(options[:concerns]) if options[:concerns]
1090
+
991
1091
  collection do
992
1092
  post :create
993
1093
  end if parent_resource.actions.include?(:create)
@@ -996,12 +1096,7 @@ module ActionDispatch
996
1096
  get :new
997
1097
  end if parent_resource.actions.include?(:new)
998
1098
 
999
- member do
1000
- get :edit if parent_resource.actions.include?(:edit)
1001
- get :show if parent_resource.actions.include?(:show)
1002
- put :update if parent_resource.actions.include?(:update)
1003
- delete :destroy if parent_resource.actions.include?(:destroy)
1004
- end
1099
+ set_member_mappings_for_resource
1005
1100
  end
1006
1101
 
1007
1102
  self
@@ -1017,13 +1112,13 @@ module ActionDispatch
1017
1112
  # creates seven different routes in your application, all mapping to
1018
1113
  # the +Photos+ controller:
1019
1114
  #
1020
- # GET /photos
1021
- # GET /photos/new
1022
- # POST /photos
1023
- # GET /photos/:id
1024
- # GET /photos/:id/edit
1025
- # PUT /photos/:id
1026
- # DELETE /photos/:id
1115
+ # GET /photos
1116
+ # GET /photos/new
1117
+ # POST /photos
1118
+ # GET /photos/:id
1119
+ # GET /photos/:id/edit
1120
+ # PATCH/PUT /photos/:id
1121
+ # DELETE /photos/:id
1027
1122
  #
1028
1123
  # Resources can also be nested infinitely by using this block syntax:
1029
1124
  #
@@ -1033,13 +1128,13 @@ module ActionDispatch
1033
1128
  #
1034
1129
  # This generates the following comments routes:
1035
1130
  #
1036
- # GET /photos/:photo_id/comments
1037
- # GET /photos/:photo_id/comments/new
1038
- # POST /photos/:photo_id/comments
1039
- # GET /photos/:photo_id/comments/:id
1040
- # GET /photos/:photo_id/comments/:id/edit
1041
- # PUT /photos/:photo_id/comments/:id
1042
- # DELETE /photos/:photo_id/comments/:id
1131
+ # GET /photos/:photo_id/comments
1132
+ # GET /photos/:photo_id/comments/new
1133
+ # POST /photos/:photo_id/comments
1134
+ # GET /photos/:photo_id/comments/:id
1135
+ # GET /photos/:photo_id/comments/:id/edit
1136
+ # PATCH/PUT /photos/:photo_id/comments/:id
1137
+ # DELETE /photos/:photo_id/comments/:id
1043
1138
  #
1044
1139
  # === Options
1045
1140
  # Takes same options as <tt>Base#match</tt> as well as:
@@ -1048,43 +1143,43 @@ module ActionDispatch
1048
1143
  # Allows you to change the segment component of the +edit+ and +new+ actions.
1049
1144
  # Actions not specified are not changed.
1050
1145
  #
1051
- # resources :posts, :path_names => { :new => "brand_new" }
1146
+ # resources :posts, path_names: { new: "brand_new" }
1052
1147
  #
1053
1148
  # The above example will now change /posts/new to /posts/brand_new
1054
1149
  #
1055
1150
  # [:path]
1056
1151
  # Allows you to change the path prefix for the resource.
1057
1152
  #
1058
- # resources :posts, :path => 'postings'
1153
+ # resources :posts, path: 'postings'
1059
1154
  #
1060
1155
  # The resource and all segments will now route to /postings instead of /posts
1061
1156
  #
1062
1157
  # [:only]
1063
1158
  # Only generate routes for the given actions.
1064
1159
  #
1065
- # resources :cows, :only => :show
1066
- # resources :cows, :only => [:show, :index]
1160
+ # resources :cows, only: :show
1161
+ # resources :cows, only: [:show, :index]
1067
1162
  #
1068
1163
  # [:except]
1069
1164
  # Generate all routes except for the given actions.
1070
1165
  #
1071
- # resources :cows, :except => :show
1072
- # resources :cows, :except => [:show, :index]
1166
+ # resources :cows, except: :show
1167
+ # resources :cows, except: [:show, :index]
1073
1168
  #
1074
1169
  # [:shallow]
1075
1170
  # Generates shallow routes for nested resource(s). When placed on a parent resource,
1076
1171
  # generates shallow routes for all nested resources.
1077
1172
  #
1078
- # resources :posts, :shallow => true do
1173
+ # resources :posts, shallow: true do
1079
1174
  # resources :comments
1080
1175
  # end
1081
1176
  #
1082
1177
  # Is the same as:
1083
1178
  #
1084
1179
  # resources :posts do
1085
- # resources :comments, :except => [:show, :edit, :update, :destroy]
1180
+ # resources :comments, except: [:show, :edit, :update, :destroy]
1086
1181
  # end
1087
- # resources :comments, :only => [:show, :edit, :update, :destroy]
1182
+ # resources :comments, only: [:show, :edit, :update, :destroy]
1088
1183
  #
1089
1184
  # This allows URLs for resources that otherwise would be deeply nested such
1090
1185
  # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
@@ -1093,29 +1188,52 @@ module ActionDispatch
1093
1188
  # [:shallow_path]
1094
1189
  # Prefixes nested shallow routes with the specified path.
1095
1190
  #
1096
- # scope :shallow_path => "sekret" do
1191
+ # scope shallow_path: "sekret" do
1192
+ # resources :posts do
1193
+ # resources :comments, shallow: true
1194
+ # end
1195
+ # end
1196
+ #
1197
+ # The +comments+ resource here will have the following routes generated for it:
1198
+ #
1199
+ # post_comments GET /posts/:post_id/comments(.:format)
1200
+ # post_comments POST /posts/:post_id/comments(.:format)
1201
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
1202
+ # edit_comment GET /sekret/comments/:id/edit(.:format)
1203
+ # comment GET /sekret/comments/:id(.:format)
1204
+ # comment PATCH/PUT /sekret/comments/:id(.:format)
1205
+ # comment DELETE /sekret/comments/:id(.:format)
1206
+ #
1207
+ # [:shallow_prefix]
1208
+ # Prefixes nested shallow route names with specified prefix.
1209
+ #
1210
+ # scope shallow_prefix: "sekret" do
1097
1211
  # resources :posts do
1098
- # resources :comments, :shallow => true
1212
+ # resources :comments, shallow: true
1099
1213
  # end
1100
1214
  # end
1101
1215
  #
1102
1216
  # The +comments+ resource here will have the following routes generated for it:
1103
1217
  #
1104
- # post_comments GET /posts/:post_id/comments(.:format)
1105
- # post_comments POST /posts/:post_id/comments(.:format)
1106
- # new_post_comment GET /posts/:post_id/comments/new(.:format)
1107
- # edit_comment GET /sekret/comments/:id/edit(.:format)
1108
- # comment GET /sekret/comments/:id(.:format)
1109
- # comment PUT /sekret/comments/:id(.:format)
1110
- # comment DELETE /sekret/comments/:id(.:format)
1218
+ # post_comments GET /posts/:post_id/comments(.:format)
1219
+ # post_comments POST /posts/:post_id/comments(.:format)
1220
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
1221
+ # edit_sekret_comment GET /comments/:id/edit(.:format)
1222
+ # sekret_comment GET /comments/:id(.:format)
1223
+ # sekret_comment PATCH/PUT /comments/:id(.:format)
1224
+ # sekret_comment DELETE /comments/:id(.:format)
1225
+ #
1226
+ # [:format]
1227
+ # Allows you to specify the default value for optional +format+
1228
+ # segment or disable it by supplying +false+.
1111
1229
  #
1112
1230
  # === Examples
1113
1231
  #
1114
1232
  # # routes call <tt>Admin::PostsController</tt>
1115
- # resources :posts, :module => "admin"
1233
+ # resources :posts, module: "admin"
1116
1234
  #
1117
1235
  # # resource actions are at /admin/posts.
1118
- # resources :posts, :path => "admin/posts"
1236
+ # resources :posts, path: "admin/posts"
1119
1237
  def resources(*resources, &block)
1120
1238
  options = resources.extract_options!.dup
1121
1239
 
@@ -1126,6 +1244,8 @@ module ActionDispatch
1126
1244
  resource_scope(:resources, Resource.new(resources.pop, options)) do
1127
1245
  yield if block_given?
1128
1246
 
1247
+ concerns(options[:concerns]) if options[:concerns]
1248
+
1129
1249
  collection do
1130
1250
  get :index if parent_resource.actions.include?(:index)
1131
1251
  post :create if parent_resource.actions.include?(:create)
@@ -1135,12 +1255,7 @@ module ActionDispatch
1135
1255
  get :new
1136
1256
  end if parent_resource.actions.include?(:new)
1137
1257
 
1138
- member do
1139
- get :edit if parent_resource.actions.include?(:edit)
1140
- get :show if parent_resource.actions.include?(:show)
1141
- put :update if parent_resource.actions.include?(:update)
1142
- delete :destroy if parent_resource.actions.include?(:destroy)
1143
- end
1258
+ set_member_mappings_for_resource
1144
1259
  end
1145
1260
 
1146
1261
  self
@@ -1246,10 +1361,13 @@ module ActionDispatch
1246
1361
  parent_resource.instance_of?(Resource) && @scope[:shallow]
1247
1362
  end
1248
1363
 
1364
+ # match 'path' => 'controller#action'
1365
+ # match 'path', to: 'controller#action'
1366
+ # match 'path', 'otherpath', on: :member, via: :get
1249
1367
  def match(path, *rest)
1250
1368
  if rest.empty? && Hash === path
1251
1369
  options = path
1252
- path, to = options.find { |name, value| name.is_a?(String) }
1370
+ path, to = options.find { |name, _value| name.is_a?(String) }
1253
1371
  options[:to] = to
1254
1372
  options.delete(path)
1255
1373
  paths = [path]
@@ -1258,22 +1376,27 @@ module ActionDispatch
1258
1376
  paths = [path] + rest
1259
1377
  end
1260
1378
 
1379
+ options[:anchor] = true unless options.key?(:anchor)
1380
+
1381
+ if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
1382
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1383
+ end
1384
+
1261
1385
  if @scope[:controller] && @scope[:action]
1262
1386
  options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
1263
1387
  end
1264
1388
 
1265
- path_without_format = path.to_s.sub(/\(\.:format\)$/, '')
1266
- if using_match_shorthand?(path_without_format, options)
1267
- options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
1268
- end
1389
+ paths.each do |_path|
1390
+ route_options = options.dup
1391
+ route_options[:path] ||= _path if _path.is_a?(String)
1269
1392
 
1270
- options[:anchor] = true unless options.key?(:anchor)
1393
+ path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
1394
+ if using_match_shorthand?(path_without_format, route_options)
1395
+ route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
1396
+ end
1271
1397
 
1272
- if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
1273
- raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1398
+ decomposed_match(_path, route_options)
1274
1399
  end
1275
-
1276
- paths.each { |_path| decomposed_match(_path, options.dup) }
1277
1400
  self
1278
1401
  end
1279
1402
 
@@ -1312,12 +1435,20 @@ module ActionDispatch
1312
1435
  options[:as] = name_for_action(options[:as], action)
1313
1436
  end
1314
1437
 
1315
- mapping = Mapping.new(@set, @scope, path, options)
1438
+ mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1316
1439
  app, conditions, requirements, defaults, as, anchor = mapping.to_route
1317
1440
  @set.add_route(app, conditions, requirements, defaults, as, anchor)
1318
1441
  end
1319
1442
 
1320
- def root(options={})
1443
+ def root(path, options={})
1444
+ if path.is_a?(String)
1445
+ options[:to] = path
1446
+ elsif path.is_a?(Hash) and options.empty?
1447
+ options = path
1448
+ else
1449
+ raise ArgumentError, "must be called with a path and/or options"
1450
+ end
1451
+
1321
1452
  if @scope[:scope_level] == :resources
1322
1453
  with_scope_level(:root) do
1323
1454
  scope(parent_resource.path) do
@@ -1378,11 +1509,11 @@ module ActionDispatch
1378
1509
  end
1379
1510
 
1380
1511
  def resource_scope? #:nodoc:
1381
- [:resource, :resources].include? @scope[:scope_level]
1512
+ RESOURCE_SCOPES.include? @scope[:scope_level]
1382
1513
  end
1383
1514
 
1384
1515
  def resource_method_scope? #:nodoc:
1385
- [:collection, :member, :new].include? @scope[:scope_level]
1516
+ RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1386
1517
  end
1387
1518
 
1388
1519
  def with_exclusive_scope
@@ -1418,18 +1549,18 @@ module ActionDispatch
1418
1549
  def nested_options #:nodoc:
1419
1550
  options = { :as => parent_resource.member_name }
1420
1551
  options[:constraints] = {
1421
- :"#{parent_resource.singular}_id" => id_constraint
1422
- } if id_constraint?
1552
+ parent_resource.nested_param => param_constraint
1553
+ } if param_constraint?
1423
1554
 
1424
1555
  options
1425
1556
  end
1426
1557
 
1427
- def id_constraint? #:nodoc:
1428
- @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
1558
+ def param_constraint? #:nodoc:
1559
+ @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1429
1560
  end
1430
1561
 
1431
- def id_constraint #:nodoc:
1432
- @scope[:constraints][:id]
1562
+ def param_constraint #:nodoc:
1563
+ @scope[:constraints][parent_resource.param]
1433
1564
  end
1434
1565
 
1435
1566
  def canonical_action?(action, flag) #:nodoc:
@@ -1442,9 +1573,9 @@ module ActionDispatch
1442
1573
 
1443
1574
  def path_for_action(action, path) #:nodoc:
1444
1575
  prefix = shallow_scoping? ?
1445
- "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]
1576
+ "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
1446
1577
 
1447
- path = if canonical_action?(action, path.blank?)
1578
+ if canonical_action?(action, path.blank?)
1448
1579
  prefix.to_s
1449
1580
  else
1450
1581
  "#{prefix}/#{action_path(action, path)}"
@@ -1452,8 +1583,7 @@ module ActionDispatch
1452
1583
  end
1453
1584
 
1454
1585
  def action_path(name, path = nil) #:nodoc:
1455
- # Ruby 1.8 can't transform empty strings to symbols
1456
- name = name.to_sym if name.is_a?(String) && !name.empty?
1586
+ name = name.to_sym if name.is_a?(String)
1457
1587
  path || @scope[:path_names][name] || name.to_s
1458
1588
  end
1459
1589
 
@@ -1503,17 +1633,136 @@ module ActionDispatch
1503
1633
  end
1504
1634
  end
1505
1635
  end
1636
+
1637
+ def set_member_mappings_for_resource
1638
+ member do
1639
+ get :edit if parent_resource.actions.include?(:edit)
1640
+ get :show if parent_resource.actions.include?(:show)
1641
+ if parent_resource.actions.include?(:update)
1642
+ patch :update
1643
+ put :update
1644
+ end
1645
+ delete :destroy if parent_resource.actions.include?(:destroy)
1646
+ end
1647
+ end
1648
+ end
1649
+
1650
+ # Routing Concerns allow you to declare common routes that can be reused
1651
+ # inside others resources and routes.
1652
+ #
1653
+ # concern :commentable do
1654
+ # resources :comments
1655
+ # end
1656
+ #
1657
+ # concern :image_attachable do
1658
+ # resources :images, only: :index
1659
+ # end
1660
+ #
1661
+ # These concerns are used in Resources routing:
1662
+ #
1663
+ # resources :messages, concerns: [:commentable, :image_attachable]
1664
+ #
1665
+ # or in a scope or namespace:
1666
+ #
1667
+ # namespace :posts do
1668
+ # concerns :commentable
1669
+ # end
1670
+ module Concerns
1671
+ # Define a routing concern using a name.
1672
+ #
1673
+ # Concerns may be defined inline, using a block, or handled by
1674
+ # another object, by passing that object as the second parameter.
1675
+ #
1676
+ # The concern object, if supplied, should respond to <tt>call</tt>,
1677
+ # which will receive two parameters:
1678
+ #
1679
+ # * The current mapper
1680
+ # * A hash of options which the concern object may use
1681
+ #
1682
+ # Options may also be used by concerns defined in a block by accepting
1683
+ # a block parameter. So, using a block, you might do something as
1684
+ # simple as limit the actions available on certain resources, passing
1685
+ # standard resource options through the concern:
1686
+ #
1687
+ # concern :commentable do |options|
1688
+ # resources :comments, options
1689
+ # end
1690
+ #
1691
+ # resources :posts, concerns: :commentable
1692
+ # resources :archived_posts do
1693
+ # # Don't allow comments on archived posts
1694
+ # concerns :commentable, only: [:index, :show]
1695
+ # end
1696
+ #
1697
+ # Or, using a callable object, you might implement something more
1698
+ # specific to your application, which would be out of place in your
1699
+ # routes file.
1700
+ #
1701
+ # # purchasable.rb
1702
+ # class Purchasable
1703
+ # def initialize(defaults = {})
1704
+ # @defaults = defaults
1705
+ # end
1706
+ #
1707
+ # def call(mapper, options = {})
1708
+ # options = @defaults.merge(options)
1709
+ # mapper.resources :purchases
1710
+ # mapper.resources :receipts
1711
+ # mapper.resources :returns if options[:returnable]
1712
+ # end
1713
+ # end
1714
+ #
1715
+ # # routes.rb
1716
+ # concern :purchasable, Purchasable.new(returnable: true)
1717
+ #
1718
+ # resources :toys, concerns: :purchasable
1719
+ # resources :electronics, concerns: :purchasable
1720
+ # resources :pets do
1721
+ # concerns :purchasable, returnable: false
1722
+ # end
1723
+ #
1724
+ # Any routing helpers can be used inside a concern. If using a
1725
+ # callable, they're accessible from the Mapper that's passed to
1726
+ # <tt>call</tt>.
1727
+ def concern(name, callable = nil, &block)
1728
+ callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
1729
+ @concerns[name] = callable
1730
+ end
1731
+
1732
+ # Use the named concerns
1733
+ #
1734
+ # resources :posts do
1735
+ # concerns :commentable
1736
+ # end
1737
+ #
1738
+ # concerns also work in any routes helper that you want to use:
1739
+ #
1740
+ # namespace :posts do
1741
+ # concerns :commentable
1742
+ # end
1743
+ def concerns(*args)
1744
+ options = args.extract_options!
1745
+ args.flatten.each do |name|
1746
+ if concern = @concerns[name]
1747
+ concern.call(self, options)
1748
+ else
1749
+ raise ArgumentError, "No concern named #{name} was found!"
1750
+ end
1751
+ end
1752
+ end
1506
1753
  end
1507
1754
 
1508
1755
  def initialize(set) #:nodoc:
1509
1756
  @set = set
1510
1757
  @scope = { :path_names => @set.resources_path_names }
1758
+ @concerns = {}
1511
1759
  end
1512
1760
 
1513
1761
  include Base
1514
1762
  include HttpHelpers
1515
1763
  include Redirection
1516
1764
  include Scoping
1765
+ include Concerns
1517
1766
  include Resources
1518
1767
  end
1519
1768
  end