actionpack 3.2.22.5 → 5.2.4

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 (271) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +279 -603
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -297
  5. data/lib/abstract_controller/asset_paths.rb +4 -2
  6. data/lib/abstract_controller/base.rb +82 -52
  7. data/lib/abstract_controller/caching/fragments.rb +166 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +117 -103
  10. data/lib/abstract_controller/collector.rb +18 -7
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +65 -38
  13. data/lib/abstract_controller/logger.rb +3 -2
  14. data/lib/abstract_controller/railties/routes_helpers.rb +5 -3
  15. data/lib/abstract_controller/rendering.rb +77 -129
  16. data/lib/abstract_controller/translation.rb +21 -3
  17. data/lib/abstract_controller/url_for.rb +9 -7
  18. data/lib/abstract_controller.rb +12 -13
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/base.rb +81 -40
  22. data/lib/action_controller/caching.rb +22 -62
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +30 -18
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +190 -47
  27. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  28. data/lib/action_controller/metal/cookies.rb +3 -3
  29. data/lib/action_controller/metal/data_streaming.rb +40 -65
  30. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  31. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  32. data/lib/action_controller/metal/exceptions.rb +19 -12
  33. data/lib/action_controller/metal/flash.rb +42 -9
  34. data/lib/action_controller/metal/force_ssl.rb +79 -19
  35. data/lib/action_controller/metal/head.rb +35 -10
  36. data/lib/action_controller/metal/helpers.rb +31 -21
  37. data/lib/action_controller/metal/http_authentication.rb +182 -134
  38. data/lib/action_controller/metal/implicit_render.rb +62 -8
  39. data/lib/action_controller/metal/instrumentation.rb +28 -26
  40. data/lib/action_controller/metal/live.rb +312 -0
  41. data/lib/action_controller/metal/mime_responds.rb +159 -163
  42. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  43. data/lib/action_controller/metal/params_wrapper.rb +146 -93
  44. data/lib/action_controller/metal/redirecting.rb +80 -56
  45. data/lib/action_controller/metal/renderers.rb +119 -47
  46. data/lib/action_controller/metal/rendering.rb +89 -32
  47. data/lib/action_controller/metal/request_forgery_protection.rb +373 -41
  48. data/lib/action_controller/metal/rescue.rb +9 -16
  49. data/lib/action_controller/metal/streaming.rb +39 -45
  50. data/lib/action_controller/metal/strong_parameters.rb +1086 -0
  51. data/lib/action_controller/metal/testing.rb +8 -29
  52. data/lib/action_controller/metal/url_for.rb +43 -32
  53. data/lib/action_controller/metal.rb +112 -106
  54. data/lib/action_controller/railtie.rb +56 -18
  55. data/lib/action_controller/railties/helpers.rb +24 -0
  56. data/lib/action_controller/renderer.rb +117 -0
  57. data/lib/action_controller/template_assertions.rb +11 -0
  58. data/lib/action_controller/test_case.rb +402 -347
  59. data/lib/action_controller.rb +31 -30
  60. data/lib/action_dispatch/http/cache.rb +133 -34
  61. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  62. data/lib/action_dispatch/http/filter_parameters.rb +40 -24
  63. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  64. data/lib/action_dispatch/http/headers.rb +117 -16
  65. data/lib/action_dispatch/http/mime_negotiation.rb +98 -33
  66. data/lib/action_dispatch/http/mime_type.rb +198 -146
  67. data/lib/action_dispatch/http/mime_types.rb +22 -7
  68. data/lib/action_dispatch/http/parameter_filter.rb +61 -49
  69. data/lib/action_dispatch/http/parameters.rb +94 -51
  70. data/lib/action_dispatch/http/rack_cache.rb +4 -3
  71. data/lib/action_dispatch/http/request.rb +262 -117
  72. data/lib/action_dispatch/http/response.rb +400 -86
  73. data/lib/action_dispatch/http/upload.rb +66 -29
  74. data/lib/action_dispatch/http/url.rb +232 -60
  75. data/lib/action_dispatch/journey/formatter.rb +189 -0
  76. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  77. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  78. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  79. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  80. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  81. data/lib/action_dispatch/journey/nfa/simulator.rb +49 -0
  82. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  83. data/lib/action_dispatch/journey/nodes/node.rb +140 -0
  84. data/lib/action_dispatch/journey/parser.rb +199 -0
  85. data/lib/action_dispatch/journey/parser.y +50 -0
  86. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  87. data/lib/action_dispatch/journey/path/pattern.rb +199 -0
  88. data/lib/action_dispatch/journey/route.rb +203 -0
  89. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  90. data/lib/action_dispatch/journey/router.rb +156 -0
  91. data/lib/action_dispatch/journey/routes.rb +82 -0
  92. data/lib/action_dispatch/journey/scanner.rb +64 -0
  93. data/lib/action_dispatch/journey/visitors.rb +268 -0
  94. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  95. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  96. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  97. data/lib/action_dispatch/journey.rb +7 -0
  98. data/lib/action_dispatch/middleware/callbacks.rb +17 -13
  99. data/lib/action_dispatch/middleware/cookies.rb +494 -162
  100. data/lib/action_dispatch/middleware/debug_exceptions.rb +176 -53
  101. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  102. data/lib/action_dispatch/middleware/exception_wrapper.rb +103 -38
  103. data/lib/action_dispatch/middleware/executor.rb +21 -0
  104. data/lib/action_dispatch/middleware/flash.rb +128 -91
  105. data/lib/action_dispatch/middleware/public_exceptions.rb +43 -16
  106. data/lib/action_dispatch/middleware/reloader.rb +6 -83
  107. data/lib/action_dispatch/middleware/remote_ip.rb +151 -49
  108. data/lib/action_dispatch/middleware/request_id.rb +19 -15
  109. data/lib/action_dispatch/middleware/session/abstract_store.rb +38 -34
  110. data/lib/action_dispatch/middleware/session/cache_store.rb +14 -9
  111. data/lib/action_dispatch/middleware/session/cookie_store.rb +94 -44
  112. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -4
  113. data/lib/action_dispatch/middleware/show_exceptions.rb +36 -61
  114. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  115. data/lib/action_dispatch/middleware/stack.rb +33 -41
  116. data/lib/action_dispatch/middleware/static.rb +92 -48
  117. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +22 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +27 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  122. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +16 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +134 -5
  128. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  136. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  137. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +200 -0
  138. data/lib/action_dispatch/railtie.rb +29 -8
  139. data/lib/action_dispatch/request/session.rb +234 -0
  140. data/lib/action_dispatch/request/utils.rb +78 -0
  141. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  142. data/lib/action_dispatch/routing/inspector.rb +225 -0
  143. data/lib/action_dispatch/routing/mapper.rb +1329 -582
  144. data/lib/action_dispatch/routing/polymorphic_routes.rb +237 -94
  145. data/lib/action_dispatch/routing/redirection.rb +120 -50
  146. data/lib/action_dispatch/routing/route_set.rb +545 -322
  147. data/lib/action_dispatch/routing/routes_proxy.rb +37 -7
  148. data/lib/action_dispatch/routing/url_for.rb +103 -34
  149. data/lib/action_dispatch/routing.rb +66 -99
  150. data/lib/action_dispatch/system_test_case.rb +147 -0
  151. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  152. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  153. data/lib/action_dispatch/system_testing/server.rb +31 -0
  154. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  156. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  157. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  158. data/lib/action_dispatch/testing/assertions/response.rb +53 -42
  159. data/lib/action_dispatch/testing/assertions/routing.rb +79 -74
  160. data/lib/action_dispatch/testing/assertions.rb +15 -9
  161. data/lib/action_dispatch/testing/integration.rb +361 -207
  162. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  163. data/lib/action_dispatch/testing/test_process.rb +28 -19
  164. data/lib/action_dispatch/testing/test_request.rb +30 -33
  165. data/lib/action_dispatch/testing/test_response.rb +35 -11
  166. data/lib/action_dispatch.rb +42 -32
  167. data/lib/action_pack/gem_version.rb +17 -0
  168. data/lib/action_pack/version.rb +7 -7
  169. data/lib/action_pack.rb +4 -2
  170. metadata +116 -175
  171. data/lib/abstract_controller/layouts.rb +0 -423
  172. data/lib/abstract_controller/view_paths.rb +0 -96
  173. data/lib/action_controller/caching/actions.rb +0 -185
  174. data/lib/action_controller/caching/fragments.rb +0 -127
  175. data/lib/action_controller/caching/pages.rb +0 -187
  176. data/lib/action_controller/caching/sweeping.rb +0 -97
  177. data/lib/action_controller/deprecated/integration_test.rb +0 -2
  178. data/lib/action_controller/deprecated/performance_test.rb +0 -1
  179. data/lib/action_controller/deprecated.rb +0 -3
  180. data/lib/action_controller/metal/compatibility.rb +0 -65
  181. data/lib/action_controller/metal/hide_actions.rb +0 -41
  182. data/lib/action_controller/metal/rack_delegation.rb +0 -26
  183. data/lib/action_controller/metal/responder.rb +0 -286
  184. data/lib/action_controller/metal/session_management.rb +0 -14
  185. data/lib/action_controller/middleware.rb +0 -39
  186. data/lib/action_controller/railties/paths.rb +0 -25
  187. data/lib/action_controller/record_identifier.rb +0 -85
  188. data/lib/action_controller/vendor/html-scanner/html/document.rb +0 -68
  189. data/lib/action_controller/vendor/html-scanner/html/node.rb +0 -532
  190. data/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +0 -177
  191. data/lib/action_controller/vendor/html-scanner/html/selector.rb +0 -830
  192. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +0 -107
  193. data/lib/action_controller/vendor/html-scanner/html/version.rb +0 -11
  194. data/lib/action_controller/vendor/html-scanner.rb +0 -20
  195. data/lib/action_dispatch/middleware/best_standards_support.rb +0 -30
  196. data/lib/action_dispatch/middleware/body_proxy.rb +0 -30
  197. data/lib/action_dispatch/middleware/head.rb +0 -18
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -75
  199. data/lib/action_dispatch/middleware/rescue.rb +0 -26
  200. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +0 -31
  201. data/lib/action_dispatch/middleware/templates/rescues/_trace.erb +0 -26
  202. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +0 -10
  203. data/lib/action_dispatch/middleware/templates/rescues/missing_template.erb +0 -2
  204. data/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +0 -15
  205. data/lib/action_dispatch/middleware/templates/rescues/template_error.erb +0 -17
  206. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb +0 -2
  207. data/lib/action_dispatch/testing/assertions/dom.rb +0 -37
  208. data/lib/action_dispatch/testing/assertions/selector.rb +0 -435
  209. data/lib/action_dispatch/testing/assertions/tag.rb +0 -138
  210. data/lib/action_dispatch/testing/performance_test.rb +0 -10
  211. data/lib/action_view/asset_paths.rb +0 -142
  212. data/lib/action_view/base.rb +0 -220
  213. data/lib/action_view/buffers.rb +0 -43
  214. data/lib/action_view/context.rb +0 -36
  215. data/lib/action_view/flows.rb +0 -79
  216. data/lib/action_view/helpers/active_model_helper.rb +0 -50
  217. data/lib/action_view/helpers/asset_paths.rb +0 -7
  218. data/lib/action_view/helpers/asset_tag_helper.rb +0 -457
  219. data/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb +0 -146
  220. data/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +0 -93
  221. data/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +0 -193
  222. data/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +0 -148
  223. data/lib/action_view/helpers/atom_feed_helper.rb +0 -200
  224. data/lib/action_view/helpers/cache_helper.rb +0 -64
  225. data/lib/action_view/helpers/capture_helper.rb +0 -203
  226. data/lib/action_view/helpers/controller_helper.rb +0 -25
  227. data/lib/action_view/helpers/csrf_helper.rb +0 -32
  228. data/lib/action_view/helpers/date_helper.rb +0 -1062
  229. data/lib/action_view/helpers/debug_helper.rb +0 -40
  230. data/lib/action_view/helpers/form_helper.rb +0 -1486
  231. data/lib/action_view/helpers/form_options_helper.rb +0 -658
  232. data/lib/action_view/helpers/form_tag_helper.rb +0 -685
  233. data/lib/action_view/helpers/javascript_helper.rb +0 -110
  234. data/lib/action_view/helpers/number_helper.rb +0 -622
  235. data/lib/action_view/helpers/output_safety_helper.rb +0 -38
  236. data/lib/action_view/helpers/record_tag_helper.rb +0 -111
  237. data/lib/action_view/helpers/rendering_helper.rb +0 -92
  238. data/lib/action_view/helpers/sanitize_helper.rb +0 -259
  239. data/lib/action_view/helpers/tag_helper.rb +0 -167
  240. data/lib/action_view/helpers/text_helper.rb +0 -426
  241. data/lib/action_view/helpers/translation_helper.rb +0 -91
  242. data/lib/action_view/helpers/url_helper.rb +0 -693
  243. data/lib/action_view/helpers.rb +0 -60
  244. data/lib/action_view/locale/en.yml +0 -160
  245. data/lib/action_view/log_subscriber.rb +0 -28
  246. data/lib/action_view/lookup_context.rb +0 -258
  247. data/lib/action_view/path_set.rb +0 -101
  248. data/lib/action_view/railtie.rb +0 -55
  249. data/lib/action_view/renderer/abstract_renderer.rb +0 -41
  250. data/lib/action_view/renderer/partial_renderer.rb +0 -415
  251. data/lib/action_view/renderer/renderer.rb +0 -61
  252. data/lib/action_view/renderer/streaming_template_renderer.rb +0 -106
  253. data/lib/action_view/renderer/template_renderer.rb +0 -95
  254. data/lib/action_view/template/error.rb +0 -128
  255. data/lib/action_view/template/handlers/builder.rb +0 -26
  256. data/lib/action_view/template/handlers/erb.rb +0 -125
  257. data/lib/action_view/template/handlers.rb +0 -50
  258. data/lib/action_view/template/resolver.rb +0 -298
  259. data/lib/action_view/template/text.rb +0 -30
  260. data/lib/action_view/template.rb +0 -337
  261. data/lib/action_view/test_case.rb +0 -246
  262. data/lib/action_view/testing/resolvers.rb +0 -49
  263. data/lib/action_view.rb +0 -84
  264. data/lib/sprockets/assets.rake +0 -99
  265. data/lib/sprockets/bootstrap.rb +0 -37
  266. data/lib/sprockets/compressors.rb +0 -83
  267. data/lib/sprockets/helpers/isolated_helper.rb +0 -13
  268. data/lib/sprockets/helpers/rails_helper.rb +0 -182
  269. data/lib/sprockets/helpers.rb +0 -6
  270. data/lib/sprockets/railtie.rb +0 -62
  271. data/lib/sprockets/static_compiler.rb +0 -56
@@ -1,293 +1,464 @@
1
- require 'active_support/core_ext/hash/except'
2
- require 'active_support/core_ext/object/blank'
3
- require 'active_support/core_ext/object/inclusion'
4
- require 'active_support/core_ext/enumerable'
5
- require 'active_support/inflector'
6
- require 'action_dispatch/routing/redirection'
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"
7
9
 
8
10
  module ActionDispatch
9
11
  module Routing
10
12
  class Mapper
11
- class Constraints #:nodoc:
12
- def self.new(app, constraints, request = Rack::Request)
13
- if constraints.any?
14
- super(app, constraints, request)
15
- else
16
- app
17
- end
18
- end
13
+ URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
19
14
 
15
+ class Constraints < Routing::Endpoint #:nodoc:
20
16
  attr_reader :app, :constraints
21
17
 
22
- def initialize(app, constraints, request)
23
- @app, @constraints, @request = app, constraints, request
24
- end
18
+ SERVE = ->(app, req) { app.serve req }
19
+ CALL = ->(app, req) { app.call req.env }
25
20
 
26
- def matches?(env)
27
- req = @request.new(env)
21
+ def initialize(app, constraints, strategy)
22
+ # Unwrap Constraints objects. I don't actually think it's possible
23
+ # to pass a Constraints object to this constructor, but there were
24
+ # multiple places that kept testing children of this object. I
25
+ # *think* they were just being defensive, but I have no idea.
26
+ if app.is_a?(self.class)
27
+ constraints += app.constraints
28
+ app = app.app
29
+ end
28
30
 
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
- }
31
+ @strategy = strategy
36
32
 
37
- return true
38
- ensure
39
- req.reset_parameters
33
+ @app, @constraints, = app, constraints
40
34
  end
41
35
 
42
- def call(env)
43
- matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
36
+ def dispatcher?; @strategy == SERVE; end
37
+
38
+ def matches?(req)
39
+ @constraints.all? do |constraint|
40
+ (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
41
+ (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
42
+ end
43
+ end
44
+
45
+ def serve(req)
46
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
47
+
48
+ @strategy.call @app, req
44
49
  end
45
50
 
46
51
  private
47
52
  def constraint_args(constraint, request)
48
- constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
53
+ constraint.arity == 1 ? [request] : [request.path_parameters, request]
49
54
  end
50
55
  end
51
56
 
52
57
  class Mapping #:nodoc:
53
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
54
58
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
55
- WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
59
+ OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
60
+
61
+ attr_reader :requirements, :defaults
62
+ attr_reader :to, :default_controller, :default_action
63
+ attr_reader :required_defaults, :ast
56
64
 
57
- def initialize(set, scope, path, options)
58
- @set, @scope = set, scope
59
- @options = (@scope[:options] || {}).merge(options)
60
- @path = normalize_path(path)
61
- normalize_options!
65
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
66
+ options = scope[:options].merge(options) if scope[:options]
67
+
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
62
72
  end
63
73
 
64
- def to_route
65
- [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
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
66
84
  end
67
85
 
68
- private
86
+ def self.normalize_path(path, format)
87
+ path = Mapper.normalize_path(path)
69
88
 
70
- def normalize_options!
71
- @options.merge!(default_controller_and_action)
89
+ if format == true
90
+ "#{path}.:format"
91
+ elsif optional_format?(path, format)
92
+ "#{path}(.:format)"
93
+ else
94
+ path
95
+ end
96
+ end
72
97
 
73
- requirements.each do |name, requirement|
74
- # segment_keys.include?(k.to_s) || k == :controller
75
- next unless Regexp === requirement && !constraints[name]
98
+ def self.optional_format?(path, format)
99
+ format != false && path !~ OPTIONAL_FORMAT_REGEX
100
+ end
76
101
 
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
83
- end
102
+ def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
103
+ @defaults = defaults
104
+ @set = set
105
+
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)
113
+
114
+ path_params = ast.find_all(&:symbol?).map(&:to_sym)
115
+
116
+ options = add_wildcard_options(options, formatted, ast)
117
+
118
+ options = normalize_options!(options, path_params, modyoule)
119
+
120
+ split_options = constraints(options, path_params)
121
+
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)
84
132
  end
85
133
 
86
- # match "account/overview"
87
- def using_match_shorthand?(path, options)
88
- path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
134
+ requirements, conditions = split_constraints path_params, constraints
135
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
136
+
137
+ formats = normalize_format(formatted)
138
+
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"
89
145
  end
90
146
 
91
- def normalize_path(path)
92
- raise ArgumentError, "path is required" if path.blank?
93
- path = Mapper.normalize_path(path)
147
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
148
+ end
94
149
 
95
- if path.match(':controller')
96
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
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
97
163
 
98
- # Add a default constraint for :controller path segments that matches namespaced
99
- # controllers with default routes like :controller/:action/:id(.:format), e.g:
100
- # GET /admin/products/show/1
101
- # => { :controller => 'admin/products', :action => 'show', :id => '1' }
102
- @options[:controller] ||= /.+?/
164
+ def application
165
+ app(@blocks)
166
+ end
167
+
168
+ def path
169
+ build_path @ast, requirements, @anchor
170
+ end
171
+
172
+ def conditions
173
+ build_conditions @conditions, @set.request_class
174
+ end
175
+
176
+ def build_conditions(current_conditions, request_class)
177
+ conditions = current_conditions.dup
178
+
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
208
+ else
209
+ next
103
210
  end
104
211
 
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] ||= /.+?/
212
+ if symbol
213
+ symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
109
214
  end
215
+ }
216
+
217
+ pattern
218
+ end
219
+ private :build_path
110
220
 
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"
221
+ private
222
+ def add_wildcard_options(options, formatted, path_ast)
223
+ # Add a constraint for wildcard route to make it non-greedy and match the
224
+ # optional format part of the route by default.
225
+ if formatted != false
226
+ path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
227
+ hash[node.name.to_sym] ||= /.+?/
228
+ }.merge options
118
229
  else
119
- "#{path}(.:format)"
230
+ options
120
231
  end
121
232
  end
122
233
 
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
234
+ def normalize_options!(options, path_params, modyoule)
235
+ if path_params.include?(:controller)
236
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
130
237
 
131
- def conditions
132
- { :path_info => @path }.merge(constraints).merge(request_method_condition)
133
- end
238
+ # Add a default constraint for :controller path segments that matches namespaced
239
+ # controllers with default routes like :controller/:action/:id(.:format), e.g:
240
+ # GET /admin/products/show/1
241
+ # => { controller: 'admin/products', action: 'show', id: '1' }
242
+ options[:controller] ||= /.+?/
243
+ end
134
244
 
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) }
245
+ if to.respond_to?(:action) || to.respond_to?(:call)
246
+ options
247
+ else
248
+ to_endpoint = split_to to
249
+ controller = to_endpoint[0] || default_controller
250
+ action = to_endpoint[1] || default_action
251
+
252
+ controller = add_controller_module(controller, modyoule)
253
+
254
+ options.merge! check_controller_and_action(path_params, controller, action)
139
255
  end
140
256
  end
141
257
 
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) }
258
+ def split_constraints(path_params, constraints)
259
+ constraints.partition do |key, requirement|
260
+ path_params.include?(key) || key == :controller
146
261
  end
147
262
  end
148
263
 
149
- def default_controller_and_action
150
- if to.respond_to?(:call)
151
- { }
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 } }
152
275
  else
153
- if to.is_a?(String)
154
- controller, action = to.split('#')
155
- elsif to.is_a?(Symbol)
156
- action = to.to_s
157
- end
158
-
159
- controller ||= default_controller
160
- action ||= default_action
276
+ { requirements: {}, defaults: {} }
277
+ end
278
+ end
161
279
 
162
- unless controller.is_a?(Regexp)
163
- controller = [@scope[:module], controller].compact.join("/").presence
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}"
164
284
  end
165
285
 
166
- if controller.is_a?(String) && controller =~ %r{\A/}
167
- raise ArgumentError, "controller name should not start with a slash"
286
+ if requirement.multiline?
287
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
168
288
  end
289
+ end
290
+ end
169
291
 
170
- controller = controller.to_s unless controller.is_a?(Regexp)
171
- action = action.to_s unless action.is_a?(Regexp)
292
+ def normalize_defaults(options)
293
+ Hash[options.reject { |_, default| Regexp === default }]
294
+ end
172
295
 
173
- if controller.blank? && segment_keys.exclude?("controller")
174
- raise ArgumentError, "missing :controller"
175
- end
296
+ def app(blocks)
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)
301
+ elsif blocks.any?
302
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
303
+ else
304
+ dispatcher(defaults.key?(:controller))
305
+ end
306
+ end
176
307
 
177
- if action.blank? && segment_keys.exclude?("action")
178
- raise ArgumentError, "missing :action"
179
- end
308
+ def check_controller_and_action(path_params, controller, action)
309
+ hash = check_part(:controller, controller, path_params, {}) do |part|
310
+ translate_controller(part) {
311
+ message = "'#{part}' is not a supported controller name. This can lead to potential routing problems.".dup
312
+ message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
180
313
 
181
- hash = {}
182
- hash[:controller] = controller unless controller.blank?
183
- hash[:action] = action unless action.blank?
184
- hash
314
+ raise ArgumentError, message
315
+ }
185
316
  end
317
+
318
+ check_part(:action, action, path_params, hash) { |part|
319
+ part.is_a?(Regexp) ? part : part.to_s
320
+ }
186
321
  end
187
322
 
188
- def blocks
189
- constraints = @options[:constraints]
190
- if constraints.present? && !constraints.is_a?(Hash)
191
- [constraints]
323
+ def check_part(name, part, path_params, hash)
324
+ if part
325
+ hash[name] = yield(part)
192
326
  else
193
- @scope[:blocks] || []
327
+ unless path_params.include?(name)
328
+ message = "Missing :#{name} key on routes definition, please check your routes."
329
+ raise ArgumentError, message
330
+ end
194
331
  end
332
+ hash
195
333
  end
196
334
 
197
- def constraints
198
- @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
335
+ def split_to(to)
336
+ if to =~ /#/
337
+ to.split("#")
338
+ else
339
+ []
340
+ end
199
341
  end
200
342
 
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 }
343
+ def add_controller_module(controller, modyoule)
344
+ if modyoule && !controller.is_a?(Regexp)
345
+ if controller =~ %r{\A/}
346
+ controller[1..-1]
347
+ else
348
+ [modyoule, controller].compact.join("/")
349
+ end
205
350
  else
206
- { }
351
+ controller
207
352
  end
208
353
  end
209
354
 
210
- def segment_keys
211
- @segment_keys ||= Journey::Path::Pattern.new(
212
- Journey::Router::Strexp.compile(@path, requirements, SEPARATORS)
213
- ).names
355
+ def translate_controller(controller)
356
+ return controller if Regexp === controller
357
+ return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
358
+
359
+ yield
214
360
  end
215
361
 
216
- def to
217
- @options[:to]
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?"
365
+ end
366
+ [callable_constraint]
218
367
  end
219
368
 
220
- def default_controller
221
- @options[:controller] || @scope[:controller]
369
+ def constraints(options, path_params)
370
+ options.group_by do |key, option|
371
+ if Regexp === option
372
+ :constraints
373
+ else
374
+ if path_params.include?(key)
375
+ :path_params
376
+ else
377
+ :required_defaults
378
+ end
379
+ end
380
+ end
222
381
  end
223
382
 
224
- def default_action
225
- @options[:action] || @scope[:action]
383
+ def dispatcher(raise_on_name_error)
384
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
226
385
  end
227
386
  end
228
387
 
229
- # Invokes Rack::Mount::Utils.normalize path and ensure that
388
+ # Invokes Journey::Router::Utils.normalize_path and ensure that
230
389
  # (:locale) becomes (/:locale) instead of /(:locale). Except
231
390
  # for root cases, where the latter is the correct one.
232
391
  def self.normalize_path(path)
233
392
  path = Journey::Router::Utils.normalize_path(path)
234
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
393
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$}
235
394
  path
236
395
  end
237
396
 
238
397
  def self.normalize_name(name)
239
- normalize_path(name)[1..-1].gsub("/", "_")
398
+ normalize_path(name)[1..-1].tr("/", "_")
240
399
  end
241
400
 
242
401
  module Base
243
- # You can specify what Rails should route "/" to with the root method:
402
+ # Matches a URL pattern to one or more routes.
244
403
  #
245
- # root :to => 'pages#main'
404
+ # You should not use the +match+ method in your router
405
+ # without specifying an HTTP method.
246
406
  #
247
- # For options, see +match+, as +root+ uses it internally.
248
- #
249
- # You should put the root route at the top of <tt>config/routes.rb</tt>,
250
- # because this means it will be matched first. As this is the most popular route
251
- # of most Rails applications, this is beneficial.
252
- def root(options = {})
253
- match '/', { :as => :root }.merge(options)
254
- end
255
-
256
- # Matches a url pattern to one or more routes. Any symbols in a pattern
257
- # are interpreted as url query parameters and thus available as +params+
258
- # in an action:
407
+ # If you want to expose your action to both GET and POST, use:
259
408
  #
260
409
  # # sets :controller, :action and :id in params
261
- # match ':controller/:action/:id'
410
+ # match ':controller/:action/:id', via: [:get, :post]
411
+ #
412
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as URL
413
+ # query parameters and thus available through +params+ in an action.
414
+ #
415
+ # If you want to expose your action to GET, use +get+ in the router:
416
+ #
417
+ # Instead of:
418
+ #
419
+ # match ":controller/:action/:id"
420
+ #
421
+ # Do:
422
+ #
423
+ # get ":controller/:action/:id"
262
424
  #
263
425
  # Two of these symbols are special, +:controller+ maps to the controller
264
426
  # and +:action+ to the controller's action. A pattern can also map
265
427
  # wildcard segments (globs) to params:
266
428
  #
267
- # match 'songs/*category/:title' => 'songs#show'
429
+ # get 'songs/*category/:title', to: 'songs#show'
268
430
  #
269
431
  # # 'songs/rock/classic/stairway-to-heaven' sets
270
432
  # # params[:category] = 'rock/classic'
271
433
  # # params[:title] = 'stairway-to-heaven'
272
434
  #
435
+ # To match a wildcard parameter, it must have a name assigned to it.
436
+ # Without a variable name to attach the glob parameter to, the route
437
+ # can't be parsed.
438
+ #
273
439
  # When a pattern points to an internal route, the route's +:action+ and
274
440
  # +:controller+ should be set in options or hash shorthand. Examples:
275
441
  #
276
- # match 'photos/:id' => 'photos#show'
277
- # match 'photos/:id', :to => 'photos#show'
278
- # match 'photos/:id', :controller => 'photos', :action => 'show'
442
+ # match 'photos/:id' => 'photos#show', via: :get
443
+ # match 'photos/:id', to: 'photos#show', via: :get
444
+ # match 'photos/:id', controller: 'photos', action: 'show', via: :get
279
445
  #
280
446
  # A pattern can also point to a +Rack+ endpoint i.e. anything that
281
447
  # responds to +call+:
282
448
  #
283
- # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] }
284
- # match 'photos/:id' => PhotoRackApp
449
+ # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
450
+ # match 'photos/:id', to: PhotoRackApp, via: :get
285
451
  # # Yes, controller actions are just rack endpoints
286
- # match 'photos/:id' => PhotosController.action(:show)
452
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
453
+ #
454
+ # Because requesting various HTTP verbs with a single action has security
455
+ # implications, you must either specify the actions in
456
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
457
+ # instead +match+
287
458
  #
288
459
  # === Options
289
460
  #
290
- # 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.
291
462
  #
292
463
  # [:controller]
293
464
  # The route's controller.
@@ -295,14 +466,45 @@ module ActionDispatch
295
466
  # [:action]
296
467
  # The route's action.
297
468
  #
469
+ # [:param]
470
+ # Overrides the default resource identifier +:id+ (name of the
471
+ # dynamic segment used to generate the routes).
472
+ # You can access that segment from your controller using
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"
499
+ #
298
500
  # [:path]
299
501
  # The path prefix for the routes.
300
502
  #
301
503
  # [:module]
302
504
  # The namespace for :controller.
303
505
  #
304
- # match 'path' => 'c#a', :module => 'sekret', :controller => 'posts'
305
- # #=> Sekret::PostsController
506
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
507
+ # # => Sekret::PostsController
306
508
  #
307
509
  # See <tt>Scoping#namespace</tt> for its scope equivalent.
308
510
  #
@@ -312,16 +514,17 @@ module ActionDispatch
312
514
  # [:via]
313
515
  # Allowed HTTP verb(s) for route.
314
516
  #
315
- # match 'path' => 'c#a', :via => :get
316
- # match 'path' => 'c#a', :via => [:get, :post]
517
+ # match 'path', to: 'c#a', via: :get
518
+ # match 'path', to: 'c#a', via: [:get, :post]
519
+ # match 'path', to: 'c#a', via: :all
317
520
  #
318
521
  # [:to]
319
522
  # Points to a +Rack+ endpoint. Can be an object that responds to
320
523
  # +call+ or a string representing a controller's action.
321
524
  #
322
- # match 'path', :to => 'controller#action'
323
- # match 'path', :to => lambda { |env| [200, {}, "Success!"] }
324
- # match 'path', :to => RackApp
525
+ # match 'path', to: 'controller#action', via: :get
526
+ # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
527
+ # match 'path', to: RackApp, via: :get
325
528
  #
326
529
  # [:on]
327
530
  # Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -329,27 +532,31 @@ module ActionDispatch
329
532
  # <tt>resource(s)</tt> block. For example:
330
533
  #
331
534
  # resource :bar do
332
- # match 'foo' => 'c#a', :on => :member, :via => [:get, :post]
535
+ # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
333
536
  # end
334
537
  #
335
538
  # Is equivalent to:
336
539
  #
337
540
  # resource :bar do
338
541
  # member do
339
- # match 'foo' => 'c#a', :via => [:get, :post]
542
+ # match 'foo', to: 'c#a', via: [:get, :post]
340
543
  # end
341
544
  # end
342
545
  #
343
546
  # [:constraints]
344
- # Constrains parameters with a hash of regular expressions or an
345
- # object that responds to <tt>matches?</tt>
547
+ # Constrains parameters with a hash of regular expressions
548
+ # or an object that responds to <tt>matches?</tt>. In addition, constraints
549
+ # other than path can also be specified with any object
550
+ # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
551
+ #
552
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
346
553
  #
347
- # match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ }
554
+ # match 'json_only', constraints: { format: 'json' }, via: :get
348
555
  #
349
- # class Blacklist
556
+ # class Whitelist
350
557
  # def matches?(request) request.remote_ip == '1.2.3.4' end
351
558
  # end
352
- # match 'path' => 'c#a', :constraints => Blacklist.new
559
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
353
560
  #
354
561
  # See <tt>Scoping#constraints</tt> for more examples with its scope
355
562
  # equivalent.
@@ -358,7 +565,7 @@ module ActionDispatch
358
565
  # Sets defaults for parameters
359
566
  #
360
567
  # # Sets params[:format] to 'jpg' by default
361
- # match 'path' => 'c#a', :defaults => { :format => 'jpg' }
568
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
362
569
  #
363
570
  # See <tt>Scoping#defaults</tt> for its scope equivalent.
364
571
  #
@@ -367,13 +574,17 @@ module ActionDispatch
367
574
  # false, the pattern matches any request prefixed with the given path.
368
575
  #
369
576
  # # Matches any request starting with 'path'
370
- # match 'path' => 'c#a', :anchor => false
371
- def match(path, options=nil)
577
+ # match 'path', to: 'c#a', anchor: false, via: :get
578
+ #
579
+ # [:format]
580
+ # Allows you to specify the default value for optional +format+
581
+ # segment or disable it by supplying +false+.
582
+ def match(path, options = nil)
372
583
  end
373
584
 
374
585
  # Mount a Rack-based application to be used within the application.
375
586
  #
376
- # mount SomeRackApp, :at => "some_route"
587
+ # mount SomeRackApp, at: "some_route"
377
588
  #
378
589
  # Alternatively:
379
590
  #
@@ -386,26 +597,37 @@ module ActionDispatch
386
597
  # the helper is either +some_rack_app_path+ or +some_rack_app_url+.
387
598
  # To customize this helper's name, use the +:as+ option:
388
599
  #
389
- # mount(SomeRackApp => "some_route", :as => "exciting")
600
+ # mount(SomeRackApp => "some_route", as: "exciting")
390
601
  #
391
602
  # This will generate the +exciting_path+ and +exciting_url+ helpers
392
603
  # which can be used to navigate to this mounted app.
393
604
  def mount(app, options = nil)
394
605
  if options
395
606
  path = options.delete(:at)
396
- else
607
+ elsif Hash === app
397
608
  options = app
398
- app, path = options.find { |k, v| k.respond_to?(:call) }
609
+ app, path = options.find { |k, _| k.respond_to?(:call) }
399
610
  options.delete(app) if app
400
611
  end
401
612
 
402
- 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
403
616
 
404
- options[:as] ||= app_name(app)
617
+ mount SomeRackApp, at: "some_route"
618
+ or
619
+ mount(SomeRackApp => "some_route")
620
+ MSG
405
621
 
406
- match(path, options.merge(:to => app, :anchor => false, :format => false))
622
+ rails_app = rails_app? app
623
+ options[:as] ||= app_name(app, rails_app)
407
624
 
408
- define_generate_prefix(app, options[:as])
625
+ target_as = name_for_action(options[:as], path)
626
+ options[:via] ||= :all
627
+
628
+ match(path, options.merge(to: app, anchor: false, format: false))
629
+
630
+ define_generate_prefix(app, target_as) if rails_app
409
631
  self
410
632
  end
411
633
 
@@ -420,84 +642,105 @@ module ActionDispatch
420
642
  end
421
643
  end
422
644
 
645
+ # Query if the following named route was already defined.
646
+ def has_named_route?(name)
647
+ @set.named_routes.key? name
648
+ end
649
+
423
650
  private
424
- def app_name(app)
425
- return unless app.respond_to?(:routes)
651
+ def rails_app?(app)
652
+ app.is_a?(Class) && app < Rails::Railtie
653
+ end
426
654
 
427
- if app.respond_to?(:railtie_name)
655
+ def app_name(app, rails_app)
656
+ if rails_app
428
657
  app.railtie_name
429
- else
430
- class_name = app.class.is_a?(Class) ? app.name : app.class.name
431
- ActiveSupport::Inflector.underscore(class_name).gsub("/", "_")
658
+ elsif app.is_a?(Class)
659
+ class_name = app.name
660
+ ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
432
661
  end
433
662
  end
434
663
 
435
664
  def define_generate_prefix(app, name)
436
- return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
437
-
438
- _route = @set.named_routes.routes[name.to_sym]
665
+ _route = @set.named_routes.get name
439
666
  _routes = @set
440
- app.routes.define_mounted_helper(name)
441
- app.routes.class_eval do
442
- define_method :_generate_prefix do |options|
443
- prefix_options = options.slice(*_route.segment_keys)
444
- # we must actually delete prefix segment keys to avoid passing them to next url_for
445
- _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
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))
449
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)
450
680
  end
681
+
682
+ app.routes.define_mounted_helper(name, script_namer)
683
+
684
+ app.routes.extend Module.new {
685
+ def optimize_routes_generation?; false; end
686
+
687
+ define_method :find_script_name do |options|
688
+ if options.key? :script_name
689
+ super(options)
690
+ else
691
+ script_namer.call(options)
692
+ end
693
+ end
694
+ }
451
695
  end
452
696
  end
453
697
 
454
698
  module HttpHelpers
455
699
  # Define a route that only recognizes HTTP GET.
456
- # For supported arguments, see <tt>Base#match</tt>.
457
- #
458
- # Example:
700
+ # For supported arguments, see match[rdoc-ref:Base#match]
459
701
  #
460
- # get 'bacon', :to => 'food#bacon'
702
+ # get 'bacon', to: 'food#bacon'
461
703
  def get(*args, &block)
462
- map_method(:get, *args, &block)
704
+ map_method(:get, args, &block)
463
705
  end
464
706
 
465
707
  # Define a route that only recognizes HTTP POST.
466
- # For supported arguments, see <tt>Base#match</tt>.
467
- #
468
- # Example:
708
+ # For supported arguments, see match[rdoc-ref:Base#match]
469
709
  #
470
- # post 'bacon', :to => 'food#bacon'
710
+ # post 'bacon', to: 'food#bacon'
471
711
  def post(*args, &block)
472
- map_method(:post, *args, &block)
712
+ map_method(:post, args, &block)
473
713
  end
474
714
 
475
- # Define a route that only recognizes HTTP PUT.
476
- # For supported arguments, see <tt>Base#match</tt>.
477
- #
478
- # Example:
715
+ # Define a route that only recognizes HTTP PATCH.
716
+ # For supported arguments, see match[rdoc-ref:Base#match]
479
717
  #
480
- # put 'bacon', :to => 'food#bacon'
481
- def put(*args, &block)
482
- map_method(:put, *args, &block)
718
+ # patch 'bacon', to: 'food#bacon'
719
+ def patch(*args, &block)
720
+ map_method(:patch, args, &block)
483
721
  end
484
722
 
485
723
  # Define a route that only recognizes HTTP PUT.
486
- # For supported arguments, see <tt>Base#match</tt>.
724
+ # For supported arguments, see match[rdoc-ref:Base#match]
487
725
  #
488
- # Example:
726
+ # put 'bacon', to: 'food#bacon'
727
+ def put(*args, &block)
728
+ map_method(:put, args, &block)
729
+ end
730
+
731
+ # Define a route that only recognizes HTTP DELETE.
732
+ # For supported arguments, see match[rdoc-ref:Base#match]
489
733
  #
490
- # delete 'broccoli', :to => 'food#broccoli'
734
+ # delete 'broccoli', to: 'food#broccoli'
491
735
  def delete(*args, &block)
492
- map_method(:delete, *args, &block)
736
+ map_method(:delete, args, &block)
493
737
  end
494
738
 
495
739
  private
496
- def map_method(method, *args, &block)
740
+ def map_method(method, args, &block)
497
741
  options = args.extract_options!
498
742
  options[:via] = method
499
- args.push(options)
500
- match(*args, &block)
743
+ match(*args, options, &block)
501
744
  self
502
745
  end
503
746
  end
@@ -515,27 +758,27 @@ module ActionDispatch
515
758
  # This will create a number of routes for each of the posts and comments
516
759
  # controller. For <tt>Admin::PostsController</tt>, Rails will create:
517
760
  #
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
761
+ # GET /admin/posts
762
+ # GET /admin/posts/new
763
+ # POST /admin/posts
764
+ # GET /admin/posts/1
765
+ # GET /admin/posts/1/edit
766
+ # PATCH/PUT /admin/posts/1
767
+ # DELETE /admin/posts/1
525
768
  #
526
769
  # If you want to route /posts (without the prefix /admin) to
527
770
  # <tt>Admin::PostsController</tt>, you could use
528
771
  #
529
- # scope :module => "admin" do
772
+ # scope module: "admin" do
530
773
  # resources :posts
531
774
  # end
532
775
  #
533
776
  # or, for a single case
534
777
  #
535
- # resources :posts, :module => "admin"
778
+ # resources :posts, module: "admin"
536
779
  #
537
780
  # If you want to route /admin/posts to +PostsController+
538
- # (without the Admin:: module prefix), you could use
781
+ # (without the <tt>Admin::</tt> module prefix), you could use
539
782
  #
540
783
  # scope "/admin" do
541
784
  # resources :posts
@@ -543,25 +786,25 @@ module ActionDispatch
543
786
  #
544
787
  # or, for a single case
545
788
  #
546
- # resources :posts, :path => "/admin/posts"
789
+ # resources :posts, path: "/admin/posts"
547
790
  #
548
791
  # In each of these cases, the named routes remain the same as if you did
549
792
  # not use scope. In the last case, the following paths map to
550
793
  # +PostsController+:
551
794
  #
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
795
+ # GET /admin/posts
796
+ # GET /admin/posts/new
797
+ # POST /admin/posts
798
+ # GET /admin/posts/1
799
+ # GET /admin/posts/1/edit
800
+ # PATCH/PUT /admin/posts/1
801
+ # DELETE /admin/posts/1
559
802
  module Scoping
560
803
  # Scopes a set of routes to the given default options.
561
804
  #
562
805
  # Take the following route definition as an example:
563
806
  #
564
- # scope :path => ":account_id", :as => "account" do
807
+ # scope path: ":account_id", as: "account" do
565
808
  # resources :projects
566
809
  # end
567
810
  #
@@ -573,67 +816,84 @@ module ActionDispatch
573
816
  #
574
817
  # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
575
818
  #
576
- # === Examples
577
- #
578
819
  # # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
579
- # scope :module => "admin" do
820
+ # scope module: "admin" do
580
821
  # resources :posts
581
822
  # end
582
823
  #
583
824
  # # prefix the posts resource's requests with '/admin'
584
- # scope :path => "/admin" do
825
+ # scope path: "/admin" do
585
826
  # resources :posts
586
827
  # end
587
828
  #
588
829
  # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
589
- # scope :as => "sekret" do
830
+ # scope as: "sekret" do
590
831
  # resources :posts
591
832
  # end
592
833
  def scope(*args)
593
- options = args.extract_options!
594
- options = options.dup
595
-
596
- options[:path] = args.first if args.first.is_a?(String)
597
- recover = {}
834
+ options = args.extract_options!.dup
835
+ scope = {}
598
836
 
837
+ options[:path] = args.flatten.join("/") if args.any?
599
838
  options[:constraints] ||= {}
600
- unless options[:constraints].is_a?(Hash)
601
- block, options[:constraints] = options[:constraints], {}
839
+
840
+ unless nested_scope?
841
+ options[:shallow_path] ||= options[:path] if options.key?(:path)
842
+ options[:shallow_prefix] ||= options[:as] if options.key?(:as)
602
843
  end
603
844
 
604
- scope_options.each do |option|
605
- if value = options.delete(option)
606
- recover[option] = @scope[option]
607
- @scope[option] = send("merge_#{option}_scope", @scope[option], value)
845
+ if options[:constraints].is_a?(Hash)
846
+ defaults = options[:constraints].select do |k, v|
847
+ URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
608
848
  end
849
+
850
+ options[:defaults] = defaults.merge(options[:defaults] || {})
851
+ else
852
+ block, options[:constraints] = options[:constraints], {}
609
853
  end
610
854
 
611
- recover[:block] = @scope[:blocks]
612
- @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
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
613
863
 
614
- recover[:options] = @scope[:options]
615
- @scope[:options] = merge_options_scope(@scope[:options], options)
864
+ @scope.options.each do |option|
865
+ if option == :blocks
866
+ value = block
867
+ elsif option == :options
868
+ value = options
869
+ else
870
+ value = options.delete(option) { POISON }
871
+ end
872
+
873
+ unless POISON == value
874
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
875
+ end
876
+ end
616
877
 
878
+ @scope = @scope.new scope
617
879
  yield
618
880
  self
619
881
  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]
882
+ @scope = @scope.parent
626
883
  end
627
884
 
885
+ POISON = Object.new # :nodoc:
886
+
628
887
  # Scopes routes to a specific controller
629
888
  #
630
- # Example:
631
889
  # controller "food" do
632
- # match "bacon", :action => "bacon"
890
+ # match "bacon", action: :bacon, via: :get
633
891
  # end
634
- def controller(controller, options={})
635
- options[:controller] = controller
636
- scope(options) { yield }
892
+ def controller(controller)
893
+ @scope = @scope.new(controller: controller)
894
+ yield
895
+ ensure
896
+ @scope = @scope.parent
637
897
  end
638
898
 
639
899
  # Scopes routes to a specific namespace. For example:
@@ -644,13 +904,13 @@ module ActionDispatch
644
904
  #
645
905
  # This generates the following routes:
646
906
  #
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
907
+ # admin_posts GET /admin/posts(.:format) admin/posts#index
908
+ # admin_posts POST /admin/posts(.:format) admin/posts#create
909
+ # new_admin_post GET /admin/posts/new(.:format) admin/posts#new
910
+ # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
911
+ # admin_post GET /admin/posts/:id(.:format) admin/posts#show
912
+ # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
913
+ # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
654
914
  #
655
915
  # === Options
656
916
  #
@@ -660,34 +920,40 @@ module ActionDispatch
660
920
  # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
661
921
  # <tt>Resources#resources</tt>.
662
922
  #
663
- # === Examples
664
- #
665
923
  # # accessible through /sekret/posts rather than /admin/posts
666
- # namespace :admin, :path => "sekret" do
924
+ # namespace :admin, path: "sekret" do
667
925
  # resources :posts
668
926
  # end
669
927
  #
670
928
  # # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
671
- # namespace :admin, :module => "sekret" do
929
+ # namespace :admin, module: "sekret" do
672
930
  # resources :posts
673
931
  # end
674
932
  #
675
933
  # # generates +sekret_posts_path+ rather than +admin_posts_path+
676
- # namespace :admin, :as => "sekret" do
934
+ # namespace :admin, as: "sekret" do
677
935
  # resources :posts
678
936
  # end
679
937
  def namespace(path, options = {})
680
938
  path = path.to_s
681
- options = { :path => path, :as => path, :module => path,
682
- :shallow_path => path, :shallow_prefix => path }.merge!(options)
683
- scope(options) { yield }
939
+
940
+ defaults = {
941
+ module: path,
942
+ as: options.fetch(:as, path),
943
+ shallow_path: options.fetch(:path, path),
944
+ shallow_prefix: options.fetch(:as, path)
945
+ }
946
+
947
+ path_scope(options.delete(:path) { path }) do
948
+ scope(defaults.merge!(options)) { yield }
949
+ end
684
950
  end
685
951
 
686
952
  # === Parameter Restriction
687
953
  # Allows you to constrain the nested routes based on a set of rules.
688
954
  # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
689
955
  #
690
- # constraints(:id => /\d+\.\d+/) do
956
+ # constraints(id: /\d+\.\d+/) do
691
957
  # resources :posts
692
958
  # end
693
959
  #
@@ -697,7 +963,7 @@ module ActionDispatch
697
963
  # You may use this to also restrict other parameters:
698
964
  #
699
965
  # resources :posts do
700
- # constraints(:post_id => /\d+\.\d+/) do
966
+ # constraints(post_id: /\d+\.\d+/) do
701
967
  # resources :comments
702
968
  # end
703
969
  # end
@@ -706,7 +972,7 @@ module ActionDispatch
706
972
  #
707
973
  # Routes can also be constrained to an IP or a certain range of IP addresses:
708
974
  #
709
- # constraints(:ip => /192.168.\d+.\d+/) do
975
+ # constraints(ip: /192\.168\.\d+\.\d+/) do
710
976
  # resources :posts
711
977
  # end
712
978
  #
@@ -717,7 +983,7 @@ module ActionDispatch
717
983
  #
718
984
  # Requests to routes can be constrained based on specific criteria:
719
985
  #
720
- # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
986
+ # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
721
987
  # resources :iphones
722
988
  # end
723
989
  #
@@ -739,79 +1005,86 @@ module ActionDispatch
739
1005
  # resources :iphones
740
1006
  # end
741
1007
  def constraints(constraints = {})
742
- scope(:constraints => constraints) { yield }
1008
+ scope(constraints: constraints) { yield }
743
1009
  end
744
1010
 
745
1011
  # 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'
1012
+ # defaults id: 'home' do
1013
+ # match 'scoped_pages/(:id)', to: 'pages#show'
748
1014
  # end
749
1015
  # Using this, the +:id+ parameter here will default to 'home'.
750
1016
  def defaults(defaults = {})
751
- scope(:defaults => defaults) { yield }
1017
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
1018
+ yield
1019
+ ensure
1020
+ @scope = @scope.parent
752
1021
  end
753
1022
 
754
1023
  private
755
- def scope_options #:nodoc:
756
- @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
757
- end
758
-
759
- def merge_path_scope(parent, child) #:nodoc:
1024
+ def merge_path_scope(parent, child)
760
1025
  Mapper.normalize_path("#{parent}/#{child}")
761
1026
  end
762
1027
 
763
- def merge_shallow_path_scope(parent, child) #:nodoc:
1028
+ def merge_shallow_path_scope(parent, child)
764
1029
  Mapper.normalize_path("#{parent}/#{child}")
765
1030
  end
766
1031
 
767
- def merge_as_scope(parent, child) #:nodoc:
1032
+ def merge_as_scope(parent, child)
768
1033
  parent ? "#{parent}_#{child}" : child
769
1034
  end
770
1035
 
771
- def merge_shallow_prefix_scope(parent, child) #:nodoc:
1036
+ def merge_shallow_prefix_scope(parent, child)
772
1037
  parent ? "#{parent}_#{child}" : child
773
1038
  end
774
1039
 
775
- def merge_module_scope(parent, child) #:nodoc:
1040
+ def merge_module_scope(parent, child)
776
1041
  parent ? "#{parent}/#{child}" : child
777
1042
  end
778
1043
 
779
- 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)
1049
+ child
1050
+ end
1051
+
1052
+ def merge_via_scope(parent, child)
780
1053
  child
781
1054
  end
782
1055
 
783
- def merge_action_scope(parent, child) #:nodoc:
1056
+ def merge_format_scope(parent, child)
784
1057
  child
785
1058
  end
786
1059
 
787
- def merge_path_names_scope(parent, child) #:nodoc:
1060
+ def merge_path_names_scope(parent, child)
788
1061
  merge_options_scope(parent, child)
789
1062
  end
790
1063
 
791
- def merge_constraints_scope(parent, child) #:nodoc:
1064
+ def merge_constraints_scope(parent, child)
792
1065
  merge_options_scope(parent, child)
793
1066
  end
794
1067
 
795
- def merge_defaults_scope(parent, child) #:nodoc:
1068
+ def merge_defaults_scope(parent, child)
796
1069
  merge_options_scope(parent, child)
797
1070
  end
798
1071
 
799
- def merge_blocks_scope(parent, child) #:nodoc:
1072
+ def merge_blocks_scope(parent, child)
800
1073
  merged = parent ? parent.dup : []
801
1074
  merged << child if child
802
1075
  merged
803
1076
  end
804
1077
 
805
- def merge_options_scope(parent, child) #:nodoc:
806
- (parent || {}).except(*override_keys(child)).merge(child)
1078
+ def merge_options_scope(parent, child)
1079
+ (parent || {}).merge(child)
807
1080
  end
808
1081
 
809
- def merge_shallow_scope(parent, child) #:nodoc:
1082
+ def merge_shallow_scope(parent, child)
810
1083
  child ? true : false
811
1084
  end
812
1085
 
813
- def override_keys(child) #:nodoc:
814
- child.key?(:only) || child.key?(:except) ? [:only, :except] : []
1086
+ def merge_to_scope(parent, child)
1087
+ child
815
1088
  end
816
1089
  end
817
1090
 
@@ -850,7 +1123,7 @@ module ActionDispatch
850
1123
  # use dots as part of the +:id+ parameter add a constraint which
851
1124
  # overrides this restriction, e.g:
852
1125
  #
853
- # resources :articles, :id => /[^\/]+/
1126
+ # resources :articles, id: /[^\/]+/
854
1127
  #
855
1128
  # This allows any character other than a slash as part of your +:id+.
856
1129
  #
@@ -858,29 +1131,38 @@ module ActionDispatch
858
1131
  # CANONICAL_ACTIONS holds all actions that does not need a prefix or
859
1132
  # a path appended since they fit properly in their scope level.
860
1133
  VALID_ON_OPTIONS = [:new, :collection, :member]
861
- RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except]
1134
+ RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
862
1135
  CANONICAL_ACTIONS = %w(index create new show update destroy)
863
1136
 
864
1137
  class Resource #:nodoc:
865
- attr_reader :controller, :path, :options
1138
+ attr_reader :controller, :path, :param
866
1139
 
867
- def initialize(entities, options = {})
1140
+ def initialize(entities, api_only, shallow, options = {})
868
1141
  @name = entities.to_s
869
1142
  @path = (options[:path] || @name).to_s
870
1143
  @controller = (options[:controller] || @name).to_s
871
1144
  @as = options[:as]
1145
+ @param = (options[:param] || :id).to_sym
872
1146
  @options = options
1147
+ @shallow = shallow
1148
+ @api_only = api_only
1149
+ @only = options.delete :only
1150
+ @except = options.delete :except
873
1151
  end
874
1152
 
875
1153
  def default_actions
876
- [: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
877
1159
  end
878
1160
 
879
1161
  def actions
880
- if only = @options[:only]
881
- Array(only).map(&:to_sym)
882
- elsif except = @options[:except]
883
- 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)
884
1166
  else
885
1167
  default_actions
886
1168
  end
@@ -907,27 +1189,38 @@ module ActionDispatch
907
1189
  end
908
1190
 
909
1191
  def resource_scope
910
- { :controller => controller }
1192
+ controller
911
1193
  end
912
1194
 
913
1195
  alias :collection_scope :path
914
1196
 
915
1197
  def member_scope
916
- "#{path}/:id"
1198
+ "#{path}/:#{param}"
917
1199
  end
918
1200
 
1201
+ alias :shallow_scope :member_scope
1202
+
919
1203
  def new_scope(new_path)
920
1204
  "#{path}/#{new_path}"
921
1205
  end
922
1206
 
1207
+ def nested_param
1208
+ :"#{singular}_#{param}"
1209
+ end
1210
+
923
1211
  def nested_scope
924
- "#{path}/:#{singular}_id"
1212
+ "#{path}/:#{nested_param}"
1213
+ end
1214
+
1215
+ def shallow?
1216
+ @shallow
925
1217
  end
926
1218
 
1219
+ def singleton?; false; end
927
1220
  end
928
1221
 
929
1222
  class SingletonResource < Resource #:nodoc:
930
- def initialize(entities, options)
1223
+ def initialize(entities, api_only, shallow, options)
931
1224
  super
932
1225
  @as = nil
933
1226
  @controller = (options[:controller] || plural).to_s
@@ -935,7 +1228,11 @@ module ActionDispatch
935
1228
  end
936
1229
 
937
1230
  def default_actions
938
- [: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
939
1236
  end
940
1237
 
941
1238
  def plural
@@ -951,6 +1248,8 @@ module ActionDispatch
951
1248
 
952
1249
  alias :member_scope :path
953
1250
  alias :nested_scope :path
1251
+
1252
+ def singleton?; true; end
954
1253
  end
955
1254
 
956
1255
  def resources_path_names(options)
@@ -963,21 +1262,21 @@ module ActionDispatch
963
1262
  # a singular resource to map /profile (rather than /profile/:id) to
964
1263
  # the show action:
965
1264
  #
966
- # resource :geocoder
1265
+ # resource :profile
967
1266
  #
968
- # creates six different routes in your application, all mapping to
969
- # the +GeoCoders+ controller (note that the controller is named after
1267
+ # This creates six different routes in your application, all mapping to
1268
+ # the +Profiles+ controller (note that the controller is named after
970
1269
  # the plural):
971
1270
  #
972
- # GET /geocoder/new
973
- # POST /geocoder
974
- # GET /geocoder
975
- # GET /geocoder/edit
976
- # PUT /geocoder
977
- # DELETE /geocoder
1271
+ # GET /profile/new
1272
+ # GET /profile
1273
+ # GET /profile/edit
1274
+ # PATCH/PUT /profile
1275
+ # DELETE /profile
1276
+ # POST /profile
978
1277
  #
979
1278
  # === Options
980
- # Takes same options as +resources+.
1279
+ # Takes same options as resources[rdoc-ref:#resources]
981
1280
  def resource(*resources, &block)
982
1281
  options = resources.extract_options!.dup
983
1282
 
@@ -985,22 +1284,22 @@ module ActionDispatch
985
1284
  return self
986
1285
  end
987
1286
 
988
- resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
989
- 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?
990
1291
 
991
- collection do
992
- post :create
993
- end if parent_resource.actions.include?(:create)
1292
+ concerns(options[:concerns]) if options[:concerns]
994
1293
 
995
- new do
996
- get :new
997
- end if parent_resource.actions.include?(:new)
1294
+ new do
1295
+ get :new
1296
+ end if parent_resource.actions.include?(:new)
998
1297
 
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)
1298
+ set_member_mappings_for_resource
1299
+
1300
+ collection do
1301
+ post :create
1302
+ end if parent_resource.actions.include?(:create)
1004
1303
  end
1005
1304
  end
1006
1305
 
@@ -1017,13 +1316,13 @@ module ActionDispatch
1017
1316
  # creates seven different routes in your application, all mapping to
1018
1317
  # the +Photos+ controller:
1019
1318
  #
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
1319
+ # GET /photos
1320
+ # GET /photos/new
1321
+ # POST /photos
1322
+ # GET /photos/:id
1323
+ # GET /photos/:id/edit
1324
+ # PATCH/PUT /photos/:id
1325
+ # DELETE /photos/:id
1027
1326
  #
1028
1327
  # Resources can also be nested infinitely by using this block syntax:
1029
1328
  #
@@ -1033,58 +1332,58 @@ module ActionDispatch
1033
1332
  #
1034
1333
  # This generates the following comments routes:
1035
1334
  #
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
1335
+ # GET /photos/:photo_id/comments
1336
+ # GET /photos/:photo_id/comments/new
1337
+ # POST /photos/:photo_id/comments
1338
+ # GET /photos/:photo_id/comments/:id
1339
+ # GET /photos/:photo_id/comments/:id/edit
1340
+ # PATCH/PUT /photos/:photo_id/comments/:id
1341
+ # DELETE /photos/:photo_id/comments/:id
1043
1342
  #
1044
1343
  # === Options
1045
- # Takes same options as <tt>Base#match</tt> as well as:
1344
+ # Takes same options as match[rdoc-ref:Base#match] as well as:
1046
1345
  #
1047
1346
  # [:path_names]
1048
1347
  # Allows you to change the segment component of the +edit+ and +new+ actions.
1049
1348
  # Actions not specified are not changed.
1050
1349
  #
1051
- # resources :posts, :path_names => { :new => "brand_new" }
1350
+ # resources :posts, path_names: { new: "brand_new" }
1052
1351
  #
1053
- # 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.
1054
1353
  #
1055
1354
  # [:path]
1056
1355
  # Allows you to change the path prefix for the resource.
1057
1356
  #
1058
- # resources :posts, :path => 'postings'
1357
+ # resources :posts, path: 'postings'
1059
1358
  #
1060
- # 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.
1061
1360
  #
1062
1361
  # [:only]
1063
1362
  # Only generate routes for the given actions.
1064
1363
  #
1065
- # resources :cows, :only => :show
1066
- # resources :cows, :only => [:show, :index]
1364
+ # resources :cows, only: :show
1365
+ # resources :cows, only: [:show, :index]
1067
1366
  #
1068
1367
  # [:except]
1069
1368
  # Generate all routes except for the given actions.
1070
1369
  #
1071
- # resources :cows, :except => :show
1072
- # resources :cows, :except => [:show, :index]
1370
+ # resources :cows, except: :show
1371
+ # resources :cows, except: [:show, :index]
1073
1372
  #
1074
1373
  # [:shallow]
1075
1374
  # Generates shallow routes for nested resource(s). When placed on a parent resource,
1076
1375
  # generates shallow routes for all nested resources.
1077
1376
  #
1078
- # resources :posts, :shallow => true do
1377
+ # resources :posts, shallow: true do
1079
1378
  # resources :comments
1080
1379
  # end
1081
1380
  #
1082
1381
  # Is the same as:
1083
1382
  #
1084
1383
  # resources :posts do
1085
- # resources :comments, :except => [:show, :edit, :update, :destroy]
1384
+ # resources :comments, except: [:show, :edit, :update, :destroy]
1086
1385
  # end
1087
- # resources :comments, :only => [:show, :edit, :update, :destroy]
1386
+ # resources :comments, only: [:show, :edit, :update, :destroy]
1088
1387
  #
1089
1388
  # This allows URLs for resources that otherwise would be deeply nested such
1090
1389
  # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
@@ -1093,29 +1392,52 @@ module ActionDispatch
1093
1392
  # [:shallow_path]
1094
1393
  # Prefixes nested shallow routes with the specified path.
1095
1394
  #
1096
- # scope :shallow_path => "sekret" do
1395
+ # scope shallow_path: "sekret" do
1097
1396
  # resources :posts do
1098
- # resources :comments, :shallow => true
1397
+ # resources :comments, shallow: true
1099
1398
  # end
1100
1399
  # end
1101
1400
  #
1102
1401
  # The +comments+ resource here will have the following routes generated for it:
1103
1402
  #
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)
1403
+ # post_comments GET /posts/:post_id/comments(.:format)
1404
+ # post_comments POST /posts/:post_id/comments(.:format)
1405
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
1406
+ # edit_comment GET /sekret/comments/:id/edit(.:format)
1407
+ # comment GET /sekret/comments/:id(.:format)
1408
+ # comment PATCH/PUT /sekret/comments/:id(.:format)
1409
+ # comment DELETE /sekret/comments/:id(.:format)
1410
+ #
1411
+ # [:shallow_prefix]
1412
+ # Prefixes nested shallow route names with specified prefix.
1413
+ #
1414
+ # scope shallow_prefix: "sekret" do
1415
+ # resources :posts do
1416
+ # resources :comments, shallow: true
1417
+ # end
1418
+ # end
1419
+ #
1420
+ # The +comments+ resource here will have the following routes generated for it:
1421
+ #
1422
+ # post_comments GET /posts/:post_id/comments(.:format)
1423
+ # post_comments POST /posts/:post_id/comments(.:format)
1424
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
1425
+ # edit_sekret_comment GET /comments/:id/edit(.:format)
1426
+ # sekret_comment GET /comments/:id(.:format)
1427
+ # sekret_comment PATCH/PUT /comments/:id(.:format)
1428
+ # sekret_comment DELETE /comments/:id(.:format)
1429
+ #
1430
+ # [:format]
1431
+ # Allows you to specify the default value for optional +format+
1432
+ # segment or disable it by supplying +false+.
1111
1433
  #
1112
1434
  # === Examples
1113
1435
  #
1114
1436
  # # routes call <tt>Admin::PostsController</tt>
1115
- # resources :posts, :module => "admin"
1437
+ # resources :posts, module: "admin"
1116
1438
  #
1117
1439
  # # resource actions are at /admin/posts.
1118
- # resources :posts, :path => "admin/posts"
1440
+ # resources :posts, path: "admin/posts"
1119
1441
  def resources(*resources, &block)
1120
1442
  options = resources.extract_options!.dup
1121
1443
 
@@ -1123,23 +1445,23 @@ module ActionDispatch
1123
1445
  return self
1124
1446
  end
1125
1447
 
1126
- resource_scope(:resources, Resource.new(resources.pop, options)) do
1127
- 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?
1128
1452
 
1129
- collection do
1130
- get :index if parent_resource.actions.include?(:index)
1131
- post :create if parent_resource.actions.include?(:create)
1132
- end
1453
+ concerns(options[:concerns]) if options[:concerns]
1133
1454
 
1134
- new do
1135
- get :new
1136
- end if parent_resource.actions.include?(:new)
1455
+ collection do
1456
+ get :index if parent_resource.actions.include?(:index)
1457
+ post :create if parent_resource.actions.include?(:create)
1458
+ end
1137
1459
 
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)
1460
+ new do
1461
+ get :new
1462
+ end if parent_resource.actions.include?(:new)
1463
+
1464
+ set_member_mappings_for_resource
1143
1465
  end
1144
1466
  end
1145
1467
 
@@ -1164,7 +1486,7 @@ module ActionDispatch
1164
1486
  end
1165
1487
 
1166
1488
  with_scope_level(:collection) do
1167
- scope(parent_resource.collection_scope) do
1489
+ path_scope(parent_resource.collection_scope) do
1168
1490
  yield
1169
1491
  end
1170
1492
  end
@@ -1187,8 +1509,12 @@ module ActionDispatch
1187
1509
  end
1188
1510
 
1189
1511
  with_scope_level(:member) do
1190
- scope(parent_resource.member_scope) do
1191
- yield
1512
+ if shallow?
1513
+ shallow_scope {
1514
+ path_scope(parent_resource.member_scope) { yield }
1515
+ }
1516
+ else
1517
+ path_scope(parent_resource.member_scope) { yield }
1192
1518
  end
1193
1519
  end
1194
1520
  end
@@ -1199,7 +1525,7 @@ module ActionDispatch
1199
1525
  end
1200
1526
 
1201
1527
  with_scope_level(:new) do
1202
- scope(parent_resource.new_scope(action_path(:new))) do
1528
+ path_scope(parent_resource.new_scope(action_path(:new))) do
1203
1529
  yield
1204
1530
  end
1205
1531
  end
@@ -1211,23 +1537,21 @@ module ActionDispatch
1211
1537
  end
1212
1538
 
1213
1539
  with_scope_level(:nested) do
1214
- if shallow?
1215
- with_exclusive_scope do
1216
- if @scope[:shallow_path].blank?
1217
- scope(parent_resource.nested_scope, nested_options) { yield }
1218
- else
1219
- scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1220
- scope(parent_resource.nested_scope, nested_options) { yield }
1221
- end
1540
+ if shallow? && shallow_nesting_depth >= 1
1541
+ shallow_scope do
1542
+ path_scope(parent_resource.nested_scope) do
1543
+ scope(nested_options) { yield }
1222
1544
  end
1223
1545
  end
1224
1546
  else
1225
- scope(parent_resource.nested_scope, nested_options) { yield }
1547
+ path_scope(parent_resource.nested_scope) do
1548
+ scope(nested_options) { yield }
1549
+ end
1226
1550
  end
1227
1551
  end
1228
1552
  end
1229
1553
 
1230
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
1554
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
1231
1555
  def namespace(path, options = {})
1232
1556
  if resource_scope?
1233
1557
  nested { super }
@@ -1237,20 +1561,42 @@ module ActionDispatch
1237
1561
  end
1238
1562
 
1239
1563
  def shallow
1240
- scope(:shallow => true, :shallow_path => @scope[:path]) do
1241
- yield
1242
- end
1564
+ @scope = @scope.new(shallow: true)
1565
+ yield
1566
+ ensure
1567
+ @scope = @scope.parent
1243
1568
  end
1244
1569
 
1245
1570
  def shallow?
1246
- parent_resource.instance_of?(Resource) && @scope[:shallow]
1571
+ !parent_resource.singleton? && @scope[:shallow]
1247
1572
  end
1248
1573
 
1249
- 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)
1250
1581
  if rest.empty? && Hash === path
1251
1582
  options = path
1252
- path, to = options.find { |name, value| name.is_a?(String) }
1253
- options[:to] = to
1583
+ path, to = options.find { |name, _value| name.is_a?(String) }
1584
+
1585
+ raise ArgumentError, "Route path not specified" if path.nil?
1586
+
1587
+ case to
1588
+ when Symbol
1589
+ options[:action] = to
1590
+ when String
1591
+ if to =~ /#/
1592
+ options[:to] = to
1593
+ else
1594
+ options[:controller] = to
1595
+ end
1596
+ else
1597
+ options[:to] = to
1598
+ end
1599
+
1254
1600
  options.delete(path)
1255
1601
  paths = [path]
1256
1602
  else
@@ -1258,89 +1604,65 @@ module ActionDispatch
1258
1604
  paths = [path] + rest
1259
1605
  end
1260
1606
 
1261
- if @scope[:controller] && @scope[:action]
1262
- options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
1263
- end
1264
-
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
1269
-
1270
- options[:anchor] = true unless options.key?(:anchor)
1271
-
1272
- if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
1273
- raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
1274
- end
1275
-
1276
- paths.each { |_path| decomposed_match(_path, options.dup) }
1277
- self
1278
- end
1279
-
1280
- def using_match_shorthand?(path, options)
1281
- path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
1282
- end
1283
-
1284
- def decomposed_match(path, options) # :nodoc:
1285
- if on = options.delete(:on)
1286
- send(on) { decomposed_match(path, options) }
1607
+ if options.key?(:defaults)
1608
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
1287
1609
  else
1288
- case @scope[:scope_level]
1289
- when :resources
1290
- nested { decomposed_match(path, options) }
1291
- when :resource
1292
- member { decomposed_match(path, options) }
1293
- else
1294
- add_route(path, options)
1295
- end
1610
+ map_match(paths, options, &block)
1296
1611
  end
1297
1612
  end
1298
1613
 
1299
- def add_route(action, options) # :nodoc:
1300
- path = path_for_action(action, options.delete(:path))
1301
- action = action.to_s.dup
1302
-
1303
- if action =~ /^[\w\/]+$/
1304
- options[:action] ||= action unless action.include?("/")
1305
- else
1306
- action = nil
1307
- end
1308
-
1309
- if !options.fetch(:as, true)
1310
- options.delete(:as)
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 = {})
1628
+ if path.is_a?(String)
1629
+ options[:to] = path
1630
+ elsif path.is_a?(Hash) && options.empty?
1631
+ options = path
1311
1632
  else
1312
- options[:as] = name_for_action(options[:as], action)
1633
+ raise ArgumentError, "must be called with a path and/or options"
1313
1634
  end
1314
1635
 
1315
- mapping = Mapping.new(@set, @scope, path, options)
1316
- app, conditions, requirements, defaults, as, anchor = mapping.to_route
1317
- @set.add_route(app, conditions, requirements, defaults, as, anchor)
1318
- end
1319
-
1320
- def root(options={})
1321
- if @scope[:scope_level] == :resources
1636
+ if @scope.resources?
1322
1637
  with_scope_level(:root) do
1323
- scope(parent_resource.path) do
1324
- super(options)
1638
+ path_scope(parent_resource.path) do
1639
+ match_root_route(options)
1325
1640
  end
1326
1641
  end
1327
1642
  else
1328
- super(options)
1643
+ match_root_route(options)
1329
1644
  end
1330
1645
  end
1331
1646
 
1332
- protected
1647
+ private
1333
1648
 
1334
- def parent_resource #:nodoc:
1649
+ def parent_resource
1335
1650
  @scope[:scope_level_resource]
1336
1651
  end
1337
1652
 
1338
- def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
1653
+ def apply_common_behavior_for(method, resources, options, &block)
1339
1654
  if resources.length > 1
1340
1655
  resources.each { |r| send(method, r, options, &block) }
1341
1656
  return true
1342
1657
  end
1343
1658
 
1659
+ if options.delete(:shallow)
1660
+ shallow do
1661
+ send(method, resources.pop, options, &block)
1662
+ end
1663
+ return true
1664
+ end
1665
+
1344
1666
  if resource_scope?
1345
1667
  nested { send(method, resources.pop, options, &block) }
1346
1668
  return true
@@ -1358,116 +1680,114 @@ module ActionDispatch
1358
1680
  return true
1359
1681
  end
1360
1682
 
1361
- unless action_options?(options)
1362
- options.merge!(scope_action_options) if scope_action_options?
1363
- end
1364
-
1365
1683
  false
1366
1684
  end
1367
1685
 
1368
- def action_options?(options) #:nodoc:
1369
- options[:only] || options[:except]
1686
+ def apply_action_options(options)
1687
+ return options if action_options? options
1688
+ options.merge scope_action_options
1370
1689
  end
1371
1690
 
1372
- def scope_action_options? #:nodoc:
1373
- @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
1691
+ def action_options?(options)
1692
+ options[:only] || options[:except]
1374
1693
  end
1375
1694
 
1376
- def scope_action_options #:nodoc:
1377
- @scope[:options].slice(:only, :except)
1695
+ def scope_action_options
1696
+ @scope[:action_options] || {}
1378
1697
  end
1379
1698
 
1380
- def resource_scope? #:nodoc:
1381
- [:resource, :resources].include? @scope[:scope_level]
1699
+ def resource_scope?
1700
+ @scope.resource_scope?
1382
1701
  end
1383
1702
 
1384
- def resource_method_scope? #:nodoc:
1385
- [:collection, :member, :new].include? @scope[:scope_level]
1703
+ def resource_method_scope?
1704
+ @scope.resource_method_scope?
1386
1705
  end
1387
1706
 
1388
- def with_exclusive_scope
1389
- begin
1390
- old_name_prefix, old_path = @scope[:as], @scope[:path]
1391
- @scope[:as], @scope[:path] = nil, nil
1392
-
1393
- with_scope_level(:exclusive) do
1394
- yield
1395
- end
1396
- ensure
1397
- @scope[:as], @scope[:path] = old_name_prefix, old_path
1398
- end
1707
+ def nested_scope?
1708
+ @scope.nested?
1399
1709
  end
1400
1710
 
1401
- def with_scope_level(kind, resource = parent_resource)
1402
- old, @scope[:scope_level] = @scope[:scope_level], kind
1403
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1711
+ def with_scope_level(kind) # :doc:
1712
+ @scope = @scope.new_level(kind)
1404
1713
  yield
1405
1714
  ensure
1406
- @scope[:scope_level] = old
1407
- @scope[:scope_level_resource] = old_resource
1715
+ @scope = @scope.parent
1408
1716
  end
1409
1717
 
1410
- def resource_scope(kind, resource) #:nodoc:
1411
- with_scope_level(kind, resource) do
1412
- scope(parent_resource.resource_scope) do
1413
- yield
1414
- end
1415
- end
1718
+ def resource_scope(resource)
1719
+ @scope = @scope.new(scope_level_resource: resource)
1720
+
1721
+ controller(resource.resource_scope) { yield }
1722
+ ensure
1723
+ @scope = @scope.parent
1416
1724
  end
1417
1725
 
1418
- def nested_options #:nodoc:
1419
- options = { :as => parent_resource.member_name }
1726
+ def nested_options
1727
+ options = { as: parent_resource.member_name }
1420
1728
  options[:constraints] = {
1421
- :"#{parent_resource.singular}_id" => id_constraint
1422
- } if id_constraint?
1729
+ parent_resource.nested_param => param_constraint
1730
+ } if param_constraint?
1423
1731
 
1424
1732
  options
1425
1733
  end
1426
1734
 
1427
- def id_constraint? #:nodoc:
1428
- @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
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? }
1429
1739
  end
1430
1740
 
1431
- def id_constraint #:nodoc:
1432
- @scope[:constraints][:id]
1741
+ def param_constraint?
1742
+ @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1433
1743
  end
1434
1744
 
1435
- def canonical_action?(action, flag) #:nodoc:
1436
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1745
+ def param_constraint
1746
+ @scope[:constraints][parent_resource.param]
1437
1747
  end
1438
1748
 
1439
- def shallow_scoping? #:nodoc:
1440
- shallow? && @scope[:scope_level] == :member
1749
+ def canonical_action?(action)
1750
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1751
+ end
1752
+
1753
+ def shallow_scope
1754
+ scope = { as: @scope[:shallow_prefix],
1755
+ path: @scope[:shallow_path] }
1756
+ @scope = @scope.new scope
1757
+
1758
+ yield
1759
+ ensure
1760
+ @scope = @scope.parent
1441
1761
  end
1442
1762
 
1443
- def path_for_action(action, path) #:nodoc:
1444
- prefix = shallow_scoping? ?
1445
- "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]
1763
+ def path_for_action(action, path)
1764
+ return "#{@scope[:path]}/#{path}" if path
1446
1765
 
1447
- path = if canonical_action?(action, path.blank?)
1448
- prefix.to_s
1766
+ if canonical_action?(action)
1767
+ @scope[:path].to_s
1449
1768
  else
1450
- "#{prefix}/#{action_path(action, path)}"
1769
+ "#{@scope[:path]}/#{action_path(action)}"
1451
1770
  end
1452
1771
  end
1453
1772
 
1454
- 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?
1457
- path || @scope[:path_names][name] || name.to_s
1773
+ def action_path(name)
1774
+ @scope[:path_names][name.to_sym] || name
1458
1775
  end
1459
1776
 
1460
- def prefix_name_for_action(as, action) #:nodoc:
1777
+ def prefix_name_for_action(as, action)
1461
1778
  if as
1462
- as.to_s
1463
- elsif !canonical_action?(action, @scope[:scope_level])
1464
- action.to_s
1779
+ prefix = as
1780
+ elsif !canonical_action?(action)
1781
+ prefix = action
1782
+ end
1783
+
1784
+ if prefix && prefix != "/" && !prefix.empty?
1785
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
1465
1786
  end
1466
1787
  end
1467
1788
 
1468
- def name_for_action(as, action) #:nodoc:
1789
+ def name_for_action(as, action)
1469
1790
  prefix = prefix_name_for_action(as, action)
1470
- prefix = Mapper.normalize_name(prefix) if prefix
1471
1791
  name_prefix = @scope[:as]
1472
1792
 
1473
1793
  if parent_resource
@@ -1477,44 +1797,471 @@ module ActionDispatch
1477
1797
  member_name = parent_resource.member_name
1478
1798
  end
1479
1799
 
1480
- name = case @scope[:scope_level]
1481
- when :nested
1482
- [name_prefix, prefix]
1483
- when :collection
1484
- [prefix, name_prefix, collection_name]
1485
- when :new
1486
- [prefix, :new, name_prefix, member_name]
1487
- when :member
1488
- [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
1489
- when :root
1490
- [name_prefix, collection_name, prefix]
1491
- else
1492
- [name_prefix, member_name, prefix]
1493
- end
1800
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1801
+ candidate = action_name.select(&:present?).join("_")
1494
1802
 
1495
- if candidate = name.select(&:present?).join("_").presence
1803
+ unless candidate.empty?
1496
1804
  # If a name was not explicitly given, we check if it is valid
1497
1805
  # and return nil in case it isn't. Otherwise, we pass the invalid name
1498
1806
  # forward so the underlying router engine treats it and raises an exception.
1499
1807
  if as.nil?
1500
- candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
1808
+ candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
1501
1809
  else
1502
1810
  candidate
1503
1811
  end
1504
1812
  end
1505
1813
  end
1814
+
1815
+ def set_member_mappings_for_resource # :doc:
1816
+ member do
1817
+ get :edit if parent_resource.actions.include?(:edit)
1818
+ get :show if parent_resource.actions.include?(:show)
1819
+ if parent_resource.actions.include?(:update)
1820
+ patch :update
1821
+ put :update
1822
+ end
1823
+ delete :destroy if parent_resource.actions.include?(:destroy)
1824
+ end
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
1942
+ end
1943
+
1944
+ # Routing Concerns allow you to declare common routes that can be reused
1945
+ # inside others resources and routes.
1946
+ #
1947
+ # concern :commentable do
1948
+ # resources :comments
1949
+ # end
1950
+ #
1951
+ # concern :image_attachable do
1952
+ # resources :images, only: :index
1953
+ # end
1954
+ #
1955
+ # These concerns are used in Resources routing:
1956
+ #
1957
+ # resources :messages, concerns: [:commentable, :image_attachable]
1958
+ #
1959
+ # or in a scope or namespace:
1960
+ #
1961
+ # namespace :posts do
1962
+ # concerns :commentable
1963
+ # end
1964
+ module Concerns
1965
+ # Define a routing concern using a name.
1966
+ #
1967
+ # Concerns may be defined inline, using a block, or handled by
1968
+ # another object, by passing that object as the second parameter.
1969
+ #
1970
+ # The concern object, if supplied, should respond to <tt>call</tt>,
1971
+ # which will receive two parameters:
1972
+ #
1973
+ # * The current mapper
1974
+ # * A hash of options which the concern object may use
1975
+ #
1976
+ # Options may also be used by concerns defined in a block by accepting
1977
+ # a block parameter. So, using a block, you might do something as
1978
+ # simple as limit the actions available on certain resources, passing
1979
+ # standard resource options through the concern:
1980
+ #
1981
+ # concern :commentable do |options|
1982
+ # resources :comments, options
1983
+ # end
1984
+ #
1985
+ # resources :posts, concerns: :commentable
1986
+ # resources :archived_posts do
1987
+ # # Don't allow comments on archived posts
1988
+ # concerns :commentable, only: [:index, :show]
1989
+ # end
1990
+ #
1991
+ # Or, using a callable object, you might implement something more
1992
+ # specific to your application, which would be out of place in your
1993
+ # routes file.
1994
+ #
1995
+ # # purchasable.rb
1996
+ # class Purchasable
1997
+ # def initialize(defaults = {})
1998
+ # @defaults = defaults
1999
+ # end
2000
+ #
2001
+ # def call(mapper, options = {})
2002
+ # options = @defaults.merge(options)
2003
+ # mapper.resources :purchases
2004
+ # mapper.resources :receipts
2005
+ # mapper.resources :returns if options[:returnable]
2006
+ # end
2007
+ # end
2008
+ #
2009
+ # # routes.rb
2010
+ # concern :purchasable, Purchasable.new(returnable: true)
2011
+ #
2012
+ # resources :toys, concerns: :purchasable
2013
+ # resources :electronics, concerns: :purchasable
2014
+ # resources :pets do
2015
+ # concerns :purchasable, returnable: false
2016
+ # end
2017
+ #
2018
+ # Any routing helpers can be used inside a concern. If using a
2019
+ # callable, they're accessible from the Mapper that's passed to
2020
+ # <tt>call</tt>.
2021
+ def concern(name, callable = nil, &block)
2022
+ callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
2023
+ @concerns[name] = callable
2024
+ end
2025
+
2026
+ # Use the named concerns
2027
+ #
2028
+ # resources :posts do
2029
+ # concerns :commentable
2030
+ # end
2031
+ #
2032
+ # Concerns also work in any routes helper that you want to use:
2033
+ #
2034
+ # namespace :posts do
2035
+ # concerns :commentable
2036
+ # end
2037
+ def concerns(*args)
2038
+ options = args.extract_options!
2039
+ args.flatten.each do |name|
2040
+ if concern = @concerns[name]
2041
+ concern.call(self, options)
2042
+ else
2043
+ raise ArgumentError, "No concern named #{name} was found!"
2044
+ end
2045
+ end
2046
+ end
2047
+ end
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
+
2163
+ class Scope # :nodoc:
2164
+ OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
2165
+ :controller, :action, :path_names, :constraints,
2166
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
2167
+
2168
+ RESOURCE_SCOPES = [:resource, :resources]
2169
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
2170
+
2171
+ attr_reader :parent, :scope_level
2172
+
2173
+ def initialize(hash, parent = NULL, scope_level = nil)
2174
+ @hash = hash
2175
+ @parent = parent
2176
+ @scope_level = scope_level
2177
+ end
2178
+
2179
+ def nested?
2180
+ scope_level == :nested
2181
+ end
2182
+
2183
+ def null?
2184
+ @hash.nil? && @parent.nil?
2185
+ end
2186
+
2187
+ def root?
2188
+ @parent.null?
2189
+ end
2190
+
2191
+ def resources?
2192
+ scope_level == :resources
2193
+ end
2194
+
2195
+ def resource_method_scope?
2196
+ RESOURCE_METHOD_SCOPES.include? scope_level
2197
+ end
2198
+
2199
+ def action_name(name_prefix, prefix, collection_name, member_name)
2200
+ case scope_level
2201
+ when :nested
2202
+ [name_prefix, prefix]
2203
+ when :collection
2204
+ [prefix, name_prefix, collection_name]
2205
+ when :new
2206
+ [prefix, :new, name_prefix, member_name]
2207
+ when :member
2208
+ [prefix, name_prefix, member_name]
2209
+ when :root
2210
+ [name_prefix, collection_name, prefix]
2211
+ else
2212
+ [name_prefix, member_name, prefix]
2213
+ end
2214
+ end
2215
+
2216
+ def resource_scope?
2217
+ RESOURCE_SCOPES.include? scope_level
2218
+ end
2219
+
2220
+ def options
2221
+ OPTIONS
2222
+ end
2223
+
2224
+ def new(hash)
2225
+ self.class.new hash, self, scope_level
2226
+ end
2227
+
2228
+ def new_level(level)
2229
+ self.class.new(frame, self, level)
2230
+ end
2231
+
2232
+ def [](key)
2233
+ scope = find { |node| node.frame.key? key }
2234
+ scope && scope.frame[key]
2235
+ end
2236
+
2237
+ include Enumerable
2238
+
2239
+ def each
2240
+ node = self
2241
+ until node.equal? NULL
2242
+ yield node
2243
+ node = node.parent
2244
+ end
2245
+ end
2246
+
2247
+ def frame; @hash; end
2248
+
2249
+ NULL = Scope.new(nil, nil)
1506
2250
  end
1507
2251
 
1508
2252
  def initialize(set) #:nodoc:
1509
2253
  @set = set
1510
- @scope = { :path_names => @set.resources_path_names }
2254
+ @scope = Scope.new(path_names: @set.resources_path_names)
2255
+ @concerns = {}
1511
2256
  end
1512
2257
 
1513
2258
  include Base
1514
2259
  include HttpHelpers
1515
2260
  include Redirection
1516
2261
  include Scoping
2262
+ include Concerns
1517
2263
  include Resources
2264
+ include CustomUrls
1518
2265
  end
1519
2266
  end
1520
2267
  end