actionpack 4.0.1 → 4.2.11.1

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 (241) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +402 -1173
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -7
  5. data/lib/abstract_controller/base.rb +39 -7
  6. data/lib/abstract_controller/callbacks.rb +32 -53
  7. data/lib/abstract_controller/collector.rb +11 -1
  8. data/lib/abstract_controller/helpers.rb +26 -16
  9. data/lib/abstract_controller/railties/routes_helpers.rb +3 -3
  10. data/lib/abstract_controller/rendering.rb +57 -127
  11. data/lib/abstract_controller/url_for.rb +1 -1
  12. data/lib/abstract_controller.rb +1 -2
  13. data/lib/action_controller/base.rb +19 -10
  14. data/lib/action_controller/caching/fragments.rb +7 -1
  15. data/lib/action_controller/caching.rb +2 -12
  16. data/lib/action_controller/log_subscriber.rb +29 -20
  17. data/lib/action_controller/metal/conditional_get.rb +37 -12
  18. data/lib/action_controller/metal/data_streaming.rb +1 -1
  19. data/lib/action_controller/metal/etag_with_template_digest.rb +50 -0
  20. data/lib/action_controller/metal/exceptions.rb +1 -1
  21. data/lib/action_controller/metal/flash.rb +17 -0
  22. data/lib/action_controller/metal/force_ssl.rb +2 -2
  23. data/lib/action_controller/metal/head.rb +8 -6
  24. data/lib/action_controller/metal/helpers.rb +6 -2
  25. data/lib/action_controller/metal/http_authentication.rb +45 -23
  26. data/lib/action_controller/metal/instrumentation.rb +9 -6
  27. data/lib/action_controller/metal/live.rb +173 -20
  28. data/lib/action_controller/metal/mime_responds.rb +127 -232
  29. data/lib/action_controller/metal/params_wrapper.rb +16 -9
  30. data/lib/action_controller/metal/rack_delegation.rb +1 -1
  31. data/lib/action_controller/metal/redirecting.rb +34 -26
  32. data/lib/action_controller/metal/renderers.rb +39 -12
  33. data/lib/action_controller/metal/rendering.rb +41 -14
  34. data/lib/action_controller/metal/request_forgery_protection.rb +147 -19
  35. data/lib/action_controller/metal/streaming.rb +19 -21
  36. data/lib/action_controller/metal/strong_parameters.rb +166 -22
  37. data/lib/action_controller/metal/testing.rb +0 -1
  38. data/lib/action_controller/metal/url_for.rb +11 -12
  39. data/lib/action_controller/metal.rb +14 -8
  40. data/lib/action_controller/model_naming.rb +1 -1
  41. data/lib/action_controller/railtie.rb +5 -1
  42. data/lib/action_controller/test_case.rb +160 -94
  43. data/lib/action_controller.rb +2 -18
  44. data/lib/action_dispatch/http/cache.rb +5 -4
  45. data/lib/action_dispatch/http/filter_parameters.rb +2 -2
  46. data/lib/action_dispatch/http/filter_redirect.rb +5 -4
  47. data/lib/action_dispatch/http/headers.rb +46 -10
  48. data/lib/action_dispatch/http/mime_negotiation.rb +31 -4
  49. data/lib/action_dispatch/http/mime_type.rb +25 -26
  50. data/lib/action_dispatch/http/mime_types.rb +1 -0
  51. data/lib/action_dispatch/http/parameter_filter.rb +1 -1
  52. data/lib/action_dispatch/http/parameters.rb +25 -41
  53. data/lib/action_dispatch/http/request.rb +49 -32
  54. data/lib/action_dispatch/http/response.rb +127 -25
  55. data/lib/action_dispatch/http/upload.rb +9 -21
  56. data/lib/action_dispatch/http/url.rb +97 -70
  57. data/lib/action_dispatch/journey/formatter.rb +35 -19
  58. data/lib/action_dispatch/journey/gtg/builder.rb +3 -3
  59. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -7
  60. data/lib/action_dispatch/journey/gtg/transition_table.rb +23 -33
  61. data/lib/action_dispatch/journey/nfa/dot.rb +2 -2
  62. data/lib/action_dispatch/journey/nfa/simulator.rb +1 -1
  63. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -5
  64. data/lib/action_dispatch/journey/nodes/node.rb +4 -0
  65. data/lib/action_dispatch/journey/parser.rb +51 -59
  66. data/lib/action_dispatch/journey/parser.y +12 -10
  67. data/lib/action_dispatch/journey/path/pattern.rb +16 -19
  68. data/lib/action_dispatch/journey/route.rb +8 -19
  69. data/lib/action_dispatch/journey/router/strexp.rb +9 -6
  70. data/lib/action_dispatch/journey/router/utils.rb +54 -18
  71. data/lib/action_dispatch/journey/router.rb +53 -75
  72. data/lib/action_dispatch/journey/routes.rb +4 -0
  73. data/lib/action_dispatch/journey/scanner.rb +5 -5
  74. data/lib/action_dispatch/journey/visitors.rb +81 -60
  75. data/lib/action_dispatch/journey/visualizer/fsm.css +0 -4
  76. data/lib/action_dispatch/journey/visualizer/index.html.erb +2 -2
  77. data/lib/action_dispatch/middleware/callbacks.rb +7 -7
  78. data/lib/action_dispatch/middleware/cookies.rb +119 -43
  79. data/lib/action_dispatch/middleware/debug_exceptions.rb +32 -13
  80. data/lib/action_dispatch/middleware/exception_wrapper.rb +60 -20
  81. data/lib/action_dispatch/middleware/flash.rb +37 -24
  82. data/lib/action_dispatch/middleware/params_parser.rb +2 -2
  83. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -3
  84. data/lib/action_dispatch/middleware/reloader.rb +11 -2
  85. data/lib/action_dispatch/middleware/remote_ip.rb +40 -54
  86. data/lib/action_dispatch/middleware/request_id.rb +1 -1
  87. data/lib/action_dispatch/middleware/session/cache_store.rb +3 -3
  88. data/lib/action_dispatch/middleware/session/cookie_store.rb +8 -7
  89. data/lib/action_dispatch/middleware/show_exceptions.rb +6 -2
  90. data/lib/action_dispatch/middleware/ssl.rb +10 -7
  91. data/lib/action_dispatch/middleware/static.rb +79 -23
  92. data/lib/action_dispatch/middleware/templates/rescues/{_request_and_response.erb → _request_and_response.html.erb} +0 -0
  93. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  94. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +21 -19
  95. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  96. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  97. data/lib/action_dispatch/middleware/templates/rescues/{diagnostics.erb → diagnostics.html.erb} +1 -1
  98. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  99. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +6 -0
  100. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  101. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  102. data/lib/action_dispatch/middleware/templates/rescues/{routing_error.erb → routing_error.html.erb} +3 -1
  103. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  104. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  105. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  106. data/lib/action_dispatch/middleware/templates/rescues/{unknown_action.erb → unknown_action.html.erb} +1 -1
  107. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  108. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +120 -64
  109. data/lib/action_dispatch/railtie.rb +5 -2
  110. data/lib/action_dispatch/request/session.rb +12 -0
  111. data/lib/action_dispatch/request/utils.rb +35 -0
  112. data/lib/action_dispatch/routing/endpoint.rb +10 -0
  113. data/lib/action_dispatch/routing/inspector.rb +11 -17
  114. data/lib/action_dispatch/routing/mapper.rb +519 -312
  115. data/lib/action_dispatch/routing/polymorphic_routes.rb +204 -79
  116. data/lib/action_dispatch/routing/redirection.rb +51 -26
  117. data/lib/action_dispatch/routing/route_set.rb +331 -206
  118. data/lib/action_dispatch/routing/routes_proxy.rb +5 -4
  119. data/lib/action_dispatch/routing/url_for.rb +19 -5
  120. data/lib/action_dispatch/routing.rb +9 -6
  121. data/lib/action_dispatch/testing/assertions/dom.rb +2 -26
  122. data/lib/action_dispatch/testing/assertions/response.rb +9 -15
  123. data/lib/action_dispatch/testing/assertions/routing.rb +22 -22
  124. data/lib/action_dispatch/testing/assertions/selector.rb +2 -429
  125. data/lib/action_dispatch/testing/assertions/tag.rb +2 -134
  126. data/lib/action_dispatch/testing/assertions.rb +11 -7
  127. data/lib/action_dispatch/testing/integration.rb +31 -29
  128. data/lib/action_dispatch/testing/test_request.rb +1 -1
  129. data/lib/action_dispatch/testing/test_response.rb +1 -5
  130. data/lib/action_dispatch.rb +5 -8
  131. data/lib/action_pack/gem_version.rb +15 -0
  132. data/lib/action_pack/version.rb +4 -7
  133. data/lib/action_pack.rb +1 -1
  134. metadata +77 -159
  135. data/lib/abstract_controller/layouts.rb +0 -423
  136. data/lib/abstract_controller/view_paths.rb +0 -96
  137. data/lib/action_controller/deprecated/integration_test.rb +0 -5
  138. data/lib/action_controller/deprecated.rb +0 -7
  139. data/lib/action_controller/metal/responder.rb +0 -287
  140. data/lib/action_controller/record_identifier.rb +0 -31
  141. data/lib/action_controller/vendor/html-scanner.rb +0 -5
  142. data/lib/action_dispatch/middleware/templates/rescues/_trace.erb +0 -24
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_template.erb +0 -7
  144. data/lib/action_dispatch/middleware/templates/rescues/template_error.erb +0 -43
  145. data/lib/action_view/base.rb +0 -201
  146. data/lib/action_view/buffers.rb +0 -49
  147. data/lib/action_view/context.rb +0 -36
  148. data/lib/action_view/dependency_tracker.rb +0 -93
  149. data/lib/action_view/digestor.rb +0 -113
  150. data/lib/action_view/flows.rb +0 -76
  151. data/lib/action_view/helpers/active_model_helper.rb +0 -49
  152. data/lib/action_view/helpers/asset_tag_helper.rb +0 -320
  153. data/lib/action_view/helpers/asset_url_helper.rb +0 -355
  154. data/lib/action_view/helpers/atom_feed_helper.rb +0 -203
  155. data/lib/action_view/helpers/cache_helper.rb +0 -196
  156. data/lib/action_view/helpers/capture_helper.rb +0 -216
  157. data/lib/action_view/helpers/controller_helper.rb +0 -25
  158. data/lib/action_view/helpers/csrf_helper.rb +0 -30
  159. data/lib/action_view/helpers/date_helper.rb +0 -1083
  160. data/lib/action_view/helpers/debug_helper.rb +0 -39
  161. data/lib/action_view/helpers/form_helper.rb +0 -1880
  162. data/lib/action_view/helpers/form_options_helper.rb +0 -838
  163. data/lib/action_view/helpers/form_tag_helper.rb +0 -785
  164. data/lib/action_view/helpers/javascript_helper.rb +0 -117
  165. data/lib/action_view/helpers/number_helper.rb +0 -441
  166. data/lib/action_view/helpers/output_safety_helper.rb +0 -38
  167. data/lib/action_view/helpers/record_tag_helper.rb +0 -106
  168. data/lib/action_view/helpers/rendering_helper.rb +0 -90
  169. data/lib/action_view/helpers/sanitize_helper.rb +0 -256
  170. data/lib/action_view/helpers/tag_helper.rb +0 -173
  171. data/lib/action_view/helpers/tags/base.rb +0 -148
  172. data/lib/action_view/helpers/tags/check_box.rb +0 -64
  173. data/lib/action_view/helpers/tags/checkable.rb +0 -16
  174. data/lib/action_view/helpers/tags/collection_check_boxes.rb +0 -44
  175. data/lib/action_view/helpers/tags/collection_helpers.rb +0 -84
  176. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +0 -36
  177. data/lib/action_view/helpers/tags/collection_select.rb +0 -28
  178. data/lib/action_view/helpers/tags/color_field.rb +0 -25
  179. data/lib/action_view/helpers/tags/date_field.rb +0 -13
  180. data/lib/action_view/helpers/tags/date_select.rb +0 -72
  181. data/lib/action_view/helpers/tags/datetime_field.rb +0 -22
  182. data/lib/action_view/helpers/tags/datetime_local_field.rb +0 -19
  183. data/lib/action_view/helpers/tags/datetime_select.rb +0 -8
  184. data/lib/action_view/helpers/tags/email_field.rb +0 -8
  185. data/lib/action_view/helpers/tags/file_field.rb +0 -8
  186. data/lib/action_view/helpers/tags/grouped_collection_select.rb +0 -29
  187. data/lib/action_view/helpers/tags/hidden_field.rb +0 -8
  188. data/lib/action_view/helpers/tags/label.rb +0 -66
  189. data/lib/action_view/helpers/tags/month_field.rb +0 -13
  190. data/lib/action_view/helpers/tags/number_field.rb +0 -18
  191. data/lib/action_view/helpers/tags/password_field.rb +0 -12
  192. data/lib/action_view/helpers/tags/radio_button.rb +0 -31
  193. data/lib/action_view/helpers/tags/range_field.rb +0 -8
  194. data/lib/action_view/helpers/tags/search_field.rb +0 -24
  195. data/lib/action_view/helpers/tags/select.rb +0 -40
  196. data/lib/action_view/helpers/tags/tel_field.rb +0 -8
  197. data/lib/action_view/helpers/tags/text_area.rb +0 -18
  198. data/lib/action_view/helpers/tags/text_field.rb +0 -29
  199. data/lib/action_view/helpers/tags/time_field.rb +0 -13
  200. data/lib/action_view/helpers/tags/time_select.rb +0 -8
  201. data/lib/action_view/helpers/tags/time_zone_select.rb +0 -20
  202. data/lib/action_view/helpers/tags/url_field.rb +0 -8
  203. data/lib/action_view/helpers/tags/week_field.rb +0 -13
  204. data/lib/action_view/helpers/tags.rb +0 -39
  205. data/lib/action_view/helpers/text_helper.rb +0 -443
  206. data/lib/action_view/helpers/translation_helper.rb +0 -107
  207. data/lib/action_view/helpers/url_helper.rb +0 -635
  208. data/lib/action_view/helpers.rb +0 -58
  209. data/lib/action_view/locale/en.yml +0 -56
  210. data/lib/action_view/log_subscriber.rb +0 -30
  211. data/lib/action_view/lookup_context.rb +0 -241
  212. data/lib/action_view/model_naming.rb +0 -12
  213. data/lib/action_view/path_set.rb +0 -77
  214. data/lib/action_view/railtie.rb +0 -43
  215. data/lib/action_view/record_identifier.rb +0 -84
  216. data/lib/action_view/renderer/abstract_renderer.rb +0 -47
  217. data/lib/action_view/renderer/partial_renderer.rb +0 -492
  218. data/lib/action_view/renderer/renderer.rb +0 -50
  219. data/lib/action_view/renderer/streaming_template_renderer.rb +0 -103
  220. data/lib/action_view/renderer/template_renderer.rb +0 -96
  221. data/lib/action_view/routing_url_for.rb +0 -107
  222. data/lib/action_view/tasks/dependencies.rake +0 -17
  223. data/lib/action_view/template/error.rb +0 -138
  224. data/lib/action_view/template/handlers/builder.rb +0 -26
  225. data/lib/action_view/template/handlers/erb.rb +0 -146
  226. data/lib/action_view/template/handlers/raw.rb +0 -11
  227. data/lib/action_view/template/handlers.rb +0 -53
  228. data/lib/action_view/template/resolver.rb +0 -326
  229. data/lib/action_view/template/text.rb +0 -34
  230. data/lib/action_view/template/types.rb +0 -57
  231. data/lib/action_view/template.rb +0 -339
  232. data/lib/action_view/test_case.rb +0 -270
  233. data/lib/action_view/testing/resolvers.rb +0 -50
  234. data/lib/action_view/vendor/html-scanner/html/document.rb +0 -68
  235. data/lib/action_view/vendor/html-scanner/html/node.rb +0 -532
  236. data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +0 -188
  237. data/lib/action_view/vendor/html-scanner/html/selector.rb +0 -830
  238. data/lib/action_view/vendor/html-scanner/html/tokenizer.rb +0 -107
  239. data/lib/action_view/vendor/html-scanner/html/version.rb +0 -11
  240. data/lib/action_view/vendor/html-scanner.rb +0 -20
  241. data/lib/action_view.rb +0 -93
@@ -3,132 +3,203 @@ require 'active_support/core_ext/hash/reverse_merge'
3
3
  require 'active_support/core_ext/hash/slice'
4
4
  require 'active_support/core_ext/enumerable'
5
5
  require 'active_support/core_ext/array/extract_options'
6
+ require 'active_support/core_ext/module/remove_method'
7
+ require 'active_support/core_ext/string/filters'
6
8
  require 'active_support/inflector'
7
9
  require 'action_dispatch/routing/redirection'
10
+ require 'action_dispatch/routing/endpoint'
11
+ require 'active_support/deprecation'
8
12
 
9
13
  module ActionDispatch
10
14
  module Routing
11
15
  class Mapper
12
16
  URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
13
- SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
14
- :controller, :action, :path_names, :constraints,
15
- :shallow, :blocks, :defaults, :options]
16
-
17
- class Constraints #:nodoc:
18
- def self.new(app, constraints, request = Rack::Request)
19
- if constraints.any?
20
- super(app, constraints, request)
21
- else
22
- app
23
- end
24
- end
25
17
 
18
+ class Constraints < Endpoint #:nodoc:
26
19
  attr_reader :app, :constraints
27
20
 
28
- def initialize(app, constraints, request)
29
- @app, @constraints, @request = app, constraints, request
21
+ def initialize(app, constraints, dispatcher_p)
22
+ # Unwrap Constraints objects. I don't actually think it's possible
23
+ # to pass a Constraints object to this constructor, but there were
24
+ # multiple places that kept testing children of this object. I
25
+ # *think* they were just being defensive, but I have no idea.
26
+ if app.is_a?(self.class)
27
+ constraints += app.constraints
28
+ app = app.app
29
+ end
30
+
31
+ @dispatcher = dispatcher_p
32
+
33
+ @app, @constraints, = app, constraints
30
34
  end
31
35
 
32
- def matches?(env)
33
- req = @request.new(env)
36
+ def dispatcher?; @dispatcher; end
34
37
 
38
+ def matches?(req)
35
39
  @constraints.all? do |constraint|
36
40
  (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
37
41
  (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
38
42
  end
39
- ensure
40
- req.reset_parameters
41
43
  end
42
44
 
43
- def call(env)
44
- matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
45
+ def serve(req)
46
+ return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
47
+
48
+ if dispatcher?
49
+ @app.serve req
50
+ else
51
+ @app.call req.env
52
+ end
45
53
  end
46
54
 
47
55
  private
48
56
  def constraint_args(constraint, request)
49
- constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
57
+ constraint.arity == 1 ? [request] : [request.path_parameters, request]
50
58
  end
51
59
  end
52
60
 
53
61
  class Mapping #:nodoc:
54
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
55
62
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
56
- WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
63
+ OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
64
+
65
+ attr_reader :requirements, :conditions, :defaults
66
+ attr_reader :to, :default_controller, :default_action, :as, :anchor
67
+
68
+ def self.build(scope, set, path, as, options)
69
+ options = scope[:options].merge(options) if scope[:options]
57
70
 
58
- attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
71
+ options.delete :only
72
+ options.delete :except
73
+ options.delete :shallow_path
74
+ options.delete :shallow_prefix
75
+ options.delete :shallow
59
76
 
60
- def initialize(set, scope, path, options)
61
- @set, @scope, @path, @options = set, scope, path, options
62
- @requirements, @conditions, @defaults = {}, {}, {}
77
+ defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
63
78
 
64
- normalize_options!
65
- normalize_path!
66
- normalize_requirements!
67
- normalize_conditions!
68
- normalize_defaults!
79
+ new scope, set, path, defaults, as, options
80
+ end
81
+
82
+ def initialize(scope, set, path, defaults, as, options)
83
+ @requirements, @conditions = {}, {}
84
+ @defaults = defaults
85
+ @set = set
86
+
87
+ @to = options.delete :to
88
+ @default_controller = options.delete(:controller) || scope[:controller]
89
+ @default_action = options.delete(:action) || scope[:action]
90
+ @as = as
91
+ @anchor = options.delete :anchor
92
+
93
+ formatted = options.delete :format
94
+ via = Array(options.delete(:via) { [] })
95
+ options_constraints = options.delete :constraints
96
+
97
+ path = normalize_path! path, formatted
98
+ ast = path_ast path
99
+ path_params = path_params ast
100
+
101
+ options = normalize_options!(options, formatted, path_params, ast, scope[:module])
102
+
103
+
104
+ split_constraints(path_params, scope[:constraints]) if scope[:constraints]
105
+ constraints = constraints(options, path_params)
106
+
107
+ split_constraints path_params, constraints
108
+
109
+ @blocks = blocks(options_constraints, scope[:blocks])
110
+
111
+ if options_constraints.is_a?(Hash)
112
+ split_constraints path_params, options_constraints
113
+ options_constraints.each do |key, default|
114
+ if URL_OPTIONS.include?(key) && (String === default || Integer === default)
115
+ @defaults[key] ||= default
116
+ end
117
+ end
118
+ end
119
+
120
+ normalize_format!(formatted)
121
+
122
+ @conditions[:path_info] = path
123
+ @conditions[:parsed_path_info] = ast
124
+
125
+ add_request_method(via, @conditions)
126
+ normalize_defaults!(options)
69
127
  end
70
128
 
71
129
  def to_route
72
- [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
130
+ [ app(@blocks), conditions, requirements, defaults, as, anchor ]
73
131
  end
74
132
 
75
133
  private
76
134
 
77
- def normalize_path!
78
- raise ArgumentError, "path is required" if @path.blank?
79
- @path = Mapper.normalize_path(@path)
135
+ def normalize_path!(path, format)
136
+ path = Mapper.normalize_path(path)
80
137
 
81
- if required_format?
82
- @path = "#{@path}.:format"
83
- elsif optional_format?
84
- @path = "#{@path}(.:format)"
138
+ if format == true
139
+ "#{path}.:format"
140
+ elsif optional_format?(path, format)
141
+ "#{path}(.:format)"
142
+ else
143
+ path
85
144
  end
86
145
  end
87
146
 
88
- def required_format?
89
- options[:format] == true
90
- end
91
-
92
- def optional_format?
93
- options[:format] != false && !path.include?(':format') && !path.end_with?('/')
147
+ def optional_format?(path, format)
148
+ format != false && path !~ OPTIONAL_FORMAT_REGEX
94
149
  end
95
150
 
96
- def normalize_options!
97
- @options.reverse_merge!(scope[:options]) if scope[:options]
98
- path_without_format = path.sub(/\(\.:format\)$/, '')
99
-
151
+ def normalize_options!(options, formatted, path_params, path_ast, modyoule)
100
152
  # Add a constraint for wildcard route to make it non-greedy and match the
101
153
  # optional format part of the route by default
102
- if path_without_format.match(WILDCARD_PATH) && @options[:format] != false
103
- @options[$1.to_sym] ||= /.+?/
154
+ if formatted != false
155
+ path_ast.grep(Journey::Nodes::Star) do |node|
156
+ options[node.name.to_sym] ||= /.+?/
157
+ end
104
158
  end
105
159
 
106
- if path_without_format.match(':controller')
107
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]
160
+ if path_params.include?(:controller)
161
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
108
162
 
109
163
  # Add a default constraint for :controller path segments that matches namespaced
110
164
  # controllers with default routes like :controller/:action/:id(.:format), e.g:
111
165
  # GET /admin/products/show/1
112
166
  # => { controller: 'admin/products', action: 'show', id: '1' }
113
- @options[:controller] ||= /.+?/
167
+ options[:controller] ||= /.+?/
114
168
  end
115
169
 
116
- @options.merge!(default_controller_and_action)
170
+ if to.respond_to? :call
171
+ options
172
+ else
173
+ to_endpoint = split_to to
174
+ controller = to_endpoint[0] || default_controller
175
+ action = to_endpoint[1] || default_action
176
+
177
+ controller = add_controller_module(controller, modyoule)
178
+
179
+ options.merge! check_controller_and_action(path_params, controller, action)
180
+ end
117
181
  end
118
182
 
119
- def normalize_requirements!
120
- constraints.each do |key, requirement|
121
- next unless segment_keys.include?(key) || key == :controller
122
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
123
- @requirements[key] = requirement
183
+ def split_constraints(path_params, constraints)
184
+ constraints.each_pair do |key, requirement|
185
+ if path_params.include?(key) || key == :controller
186
+ verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
187
+ @requirements[key] = requirement
188
+ else
189
+ @conditions[key] = requirement
190
+ end
124
191
  end
192
+ end
125
193
 
126
- if options[:format] == true
194
+ def normalize_format!(formatted)
195
+ if formatted == true
127
196
  @requirements[:format] ||= /.+/
128
- elsif Regexp === options[:format]
129
- @requirements[:format] = options[:format]
130
- elsif String === options[:format]
131
- @requirements[:format] = Regexp.compile(options[:format])
197
+ elsif Regexp === formatted
198
+ @requirements[:format] = formatted
199
+ @defaults[:format] = nil
200
+ elsif String === formatted
201
+ @requirements[:format] = Regexp.compile(formatted)
202
+ @defaults[:format] = formatted
132
203
  end
133
204
  end
134
205
 
@@ -142,160 +213,147 @@ module ActionDispatch
142
213
  end
143
214
  end
144
215
 
145
- def normalize_defaults!
146
- @defaults.merge!(scope[:defaults]) if scope[:defaults]
147
- @defaults.merge!(options[:defaults]) if options[:defaults]
148
-
149
- options.each do |key, default|
150
- next if Regexp === default || IGNORE_OPTIONS.include?(key)
151
- @defaults[key] = default
152
- end
153
-
154
- if options[:constraints].is_a?(Hash)
155
- options[:constraints].each do |key, default|
156
- next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
157
- @defaults[key] ||= default
216
+ def normalize_defaults!(options)
217
+ options.each_pair do |key, default|
218
+ unless Regexp === default
219
+ @defaults[key] = default
158
220
  end
159
221
  end
160
-
161
- if Regexp === options[:format]
162
- @defaults[:format] = nil
163
- elsif String === options[:format]
164
- @defaults[:format] = options[:format]
165
- end
166
222
  end
167
223
 
168
- def normalize_conditions!
169
- @conditions.merge!(:path_info => path)
170
-
171
- constraints.each do |key, condition|
172
- next if segment_keys.include?(key) || key == :controller
173
- @conditions[key] = condition
174
- end
175
-
176
- @conditions[:required_defaults] = []
177
- options.each do |key, required_default|
178
- next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key)
179
- next if Regexp === required_default
180
- @conditions[:required_defaults] << key
224
+ def verify_callable_constraint(callable_constraint)
225
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
226
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
181
227
  end
228
+ end
182
229
 
183
- via_all = options.delete(:via) if options[:via] == :all
230
+ def add_request_method(via, conditions)
231
+ return if via == [:all]
184
232
 
185
- if !via_all && options[:via].blank?
233
+ if via.empty?
186
234
  msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
187
235
  "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
188
236
  "If you want to expose your action to GET, use `get` in the router:\n" \
189
237
  " Instead of: match \"controller#action\"\n" \
190
238
  " Do: get \"controller#action\""
191
- raise msg
192
- end
193
-
194
- if via = options[:via]
195
- list = Array(via).map { |m| m.to_s.dasherize.upcase }
196
- @conditions.merge!(:request_method => list)
239
+ raise ArgumentError, msg
197
240
  end
198
- end
199
241
 
200
- def app
201
- Constraints.new(endpoint, blocks, @set.request_class)
242
+ conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
202
243
  end
203
244
 
204
- def default_controller_and_action
245
+ def app(blocks)
205
246
  if to.respond_to?(:call)
206
- { }
247
+ Constraints.new(to, blocks, false)
248
+ elsif blocks.any?
249
+ Constraints.new(dispatcher(defaults), blocks, true)
207
250
  else
208
- if to.is_a?(String)
209
- controller, action = to.split('#')
210
- elsif to.is_a?(Symbol)
211
- action = to.to_s
212
- end
213
-
214
- controller ||= default_controller
215
- action ||= default_action
216
-
217
- unless controller.is_a?(Regexp)
218
- controller = [@scope[:module], controller].compact.join("/").presence
219
- end
220
-
221
- if controller.is_a?(String) && controller =~ %r{\A/}
222
- raise ArgumentError, "controller name should not start with a slash"
223
- end
251
+ dispatcher(defaults)
252
+ end
253
+ end
224
254
 
225
- controller = controller.to_s unless controller.is_a?(Regexp)
226
- action = action.to_s unless action.is_a?(Regexp)
255
+ def check_controller_and_action(path_params, controller, action)
256
+ hash = check_part(:controller, controller, path_params, {}) do |part|
257
+ translate_controller(part) {
258
+ message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
259
+ message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
227
260
 
228
- if controller.blank? && segment_keys.exclude?(:controller)
229
- raise ArgumentError, "missing :controller"
230
- end
261
+ raise ArgumentError, message
262
+ }
263
+ end
231
264
 
232
- if action.blank? && segment_keys.exclude?(:action)
233
- raise ArgumentError, "missing :action"
234
- end
265
+ check_part(:action, action, path_params, hash) { |part|
266
+ part.is_a?(Regexp) ? part : part.to_s
267
+ }
268
+ end
235
269
 
236
- if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
237
- message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems."
238
- message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
270
+ def check_part(name, part, path_params, hash)
271
+ if part
272
+ hash[name] = yield(part)
273
+ else
274
+ unless path_params.include?(name)
275
+ message = "Missing :#{name} key on routes definition, please check your routes."
239
276
  raise ArgumentError, message
240
277
  end
241
-
242
- hash = {}
243
- hash[:controller] = controller unless controller.blank?
244
- hash[:action] = action unless action.blank?
245
- hash
246
278
  end
247
- end
248
-
249
- def blocks
250
- if options[:constraints].present? && !options[:constraints].is_a?(Hash)
251
- [options[:constraints]]
279
+ hash
280
+ end
281
+
282
+ def split_to(to)
283
+ case to
284
+ when Symbol
285
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
286
+ Defining a route where `to` is a symbol is deprecated.
287
+ Please change `to: :#{to}` to `action: :#{to}`.
288
+ MSG
289
+
290
+ [nil, to.to_s]
291
+ when /#/ then to.split('#')
292
+ when String
293
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
294
+ Defining a route where `to` is a controller without an action is deprecated.
295
+ Please change `to: '#{to}'` to `controller: '#{to}'`.
296
+ MSG
297
+
298
+ [to, nil]
252
299
  else
253
- scope[:blocks] || []
300
+ []
254
301
  end
255
302
  end
256
303
 
257
- def constraints
258
- @constraints ||= {}.tap do |constraints|
259
- constraints.merge!(scope[:constraints]) if scope[:constraints]
260
-
261
- options.except(*IGNORE_OPTIONS).each do |key, option|
262
- constraints[key] = option if Regexp === option
304
+ def add_controller_module(controller, modyoule)
305
+ if modyoule && !controller.is_a?(Regexp)
306
+ if controller =~ %r{\A/}
307
+ controller[1..-1]
308
+ else
309
+ [modyoule, controller].compact.join("/")
263
310
  end
264
-
265
- constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
311
+ else
312
+ controller
266
313
  end
267
314
  end
268
315
 
269
- def segment_keys
270
- @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
271
- end
272
-
273
- def path_pattern
274
- Journey::Path::Pattern.new(strexp)
275
- end
316
+ def translate_controller(controller)
317
+ return controller if Regexp === controller
318
+ return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
276
319
 
277
- def strexp
278
- Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
320
+ yield
279
321
  end
280
322
 
281
- def endpoint
282
- to.respond_to?(:call) ? to : dispatcher
323
+ def blocks(options_constraints, scope_blocks)
324
+ if options_constraints && !options_constraints.is_a?(Hash)
325
+ verify_callable_constraint(options_constraints)
326
+ [options_constraints]
327
+ else
328
+ scope_blocks || []
329
+ end
283
330
  end
284
331
 
285
- def dispatcher
286
- Routing::RouteSet::Dispatcher.new(:defaults => defaults)
332
+ def constraints(options, path_params)
333
+ constraints = {}
334
+ required_defaults = []
335
+ options.each_pair do |key, option|
336
+ if Regexp === option
337
+ constraints[key] = option
338
+ else
339
+ required_defaults << key unless path_params.include?(key)
340
+ end
341
+ end
342
+ @conditions[:required_defaults] = required_defaults
343
+ constraints
287
344
  end
288
345
 
289
- def to
290
- options[:to]
346
+ def path_params(ast)
347
+ ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
291
348
  end
292
349
 
293
- def default_controller
294
- options[:controller] || scope[:controller]
350
+ def path_ast(path)
351
+ parser = Journey::Parser.new
352
+ parser.parse path
295
353
  end
296
354
 
297
- def default_action
298
- options[:action] || scope[:action]
355
+ def dispatcher(defaults)
356
+ @set.dispatcher defaults
299
357
  end
300
358
  end
301
359
 
@@ -330,40 +388,61 @@ module ActionDispatch
330
388
  match '/', { :as => :root, :via => :get }.merge!(options)
331
389
  end
332
390
 
333
- # Matches a url pattern to one or more routes. Any symbols in a pattern
334
- # are interpreted as url query parameters and thus available as +params+
335
- # in an action:
391
+ # Matches a url pattern to one or more routes.
392
+ #
393
+ # You should not use the +match+ method in your router
394
+ # without specifying an HTTP method.
395
+ #
396
+ # If you want to expose your action to both GET and POST, use:
336
397
  #
337
398
  # # sets :controller, :action and :id in params
338
- # match ':controller/:action/:id'
399
+ # match ':controller/:action/:id', via: [:get, :post]
400
+ #
401
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as url
402
+ # query parameters and thus available through +params+ in an action.
403
+ #
404
+ # If you want to expose your action to GET, use +get+ in the router:
405
+ #
406
+ # Instead of:
407
+ #
408
+ # match ":controller/:action/:id"
409
+ #
410
+ # Do:
411
+ #
412
+ # get ":controller/:action/:id"
339
413
  #
340
414
  # Two of these symbols are special, +:controller+ maps to the controller
341
415
  # and +:action+ to the controller's action. A pattern can also map
342
416
  # wildcard segments (globs) to params:
343
417
  #
344
- # match 'songs/*category/:title', to: 'songs#show'
418
+ # get 'songs/*category/:title', to: 'songs#show'
345
419
  #
346
420
  # # 'songs/rock/classic/stairway-to-heaven' sets
347
421
  # # params[:category] = 'rock/classic'
348
422
  # # params[:title] = 'stairway-to-heaven'
349
423
  #
424
+ # To match a wildcard parameter, it must have a name assigned to it.
425
+ # Without a variable name to attach the glob parameter to, the route
426
+ # can't be parsed.
427
+ #
350
428
  # When a pattern points to an internal route, the route's +:action+ and
351
429
  # +:controller+ should be set in options or hash shorthand. Examples:
352
430
  #
353
- # match 'photos/:id' => 'photos#show'
354
- # match 'photos/:id', to: 'photos#show'
355
- # match 'photos/:id', controller: 'photos', action: 'show'
431
+ # match 'photos/:id' => 'photos#show', via: :get
432
+ # match 'photos/:id', to: 'photos#show', via: :get
433
+ # match 'photos/:id', controller: 'photos', action: 'show', via: :get
356
434
  #
357
435
  # A pattern can also point to a +Rack+ endpoint i.e. anything that
358
436
  # responds to +call+:
359
437
  #
360
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }
361
- # match 'photos/:id', to: PhotoRackApp
438
+ # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
439
+ # match 'photos/:id', to: PhotoRackApp, via: :get
362
440
  # # Yes, controller actions are just rack endpoints
363
- # match 'photos/:id', to: PhotosController.action(:show)
441
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
364
442
  #
365
- # Because request various HTTP verbs with a single action has security
366
- # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers]
443
+ # Because requesting various HTTP verbs with a single action has security
444
+ # implications, you must either specify the actions in
445
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
367
446
  # instead +match+
368
447
  #
369
448
  # === Options
@@ -376,14 +455,20 @@ module ActionDispatch
376
455
  # [:action]
377
456
  # The route's action.
378
457
  #
458
+ # [:param]
459
+ # Overrides the default resource identifier +:id+ (name of the
460
+ # dynamic segment used to generate the routes).
461
+ # You can access that segment from your controller using
462
+ # <tt>params[<:param>]</tt>.
463
+ #
379
464
  # [:path]
380
465
  # The path prefix for the routes.
381
466
  #
382
467
  # [:module]
383
468
  # The namespace for :controller.
384
469
  #
385
- # match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
386
- # #=> Sekret::PostsController
470
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
471
+ # # => Sekret::PostsController
387
472
  #
388
473
  # See <tt>Scoping#namespace</tt> for its scope equivalent.
389
474
  #
@@ -401,9 +486,9 @@ module ActionDispatch
401
486
  # Points to a +Rack+ endpoint. Can be an object that responds to
402
487
  # +call+ or a string representing a controller's action.
403
488
  #
404
- # match 'path', to: 'controller#action'
405
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }
406
- # match 'path', to: RackApp
489
+ # match 'path', to: 'controller#action', via: :get
490
+ # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
491
+ # match 'path', to: RackApp, via: :get
407
492
  #
408
493
  # [:on]
409
494
  # Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -428,14 +513,14 @@ module ActionDispatch
428
513
  # other than path can also be specified with any object
429
514
  # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
430
515
  #
431
- # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }
516
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
432
517
  #
433
- # match 'json_only', constraints: { format: 'json' }
518
+ # match 'json_only', constraints: { format: 'json' }, via: :get
434
519
  #
435
- # class Blacklist
520
+ # class Whitelist
436
521
  # def matches?(request) request.remote_ip == '1.2.3.4' end
437
522
  # end
438
- # match 'path', to: 'c#a', constraints: Blacklist.new
523
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
439
524
  #
440
525
  # See <tt>Scoping#constraints</tt> for more examples with its scope
441
526
  # equivalent.
@@ -444,7 +529,7 @@ module ActionDispatch
444
529
  # Sets defaults for parameters
445
530
  #
446
531
  # # Sets params[:format] to 'jpg' by default
447
- # match 'path', to: 'c#a', defaults: { format: 'jpg' }
532
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
448
533
  #
449
534
  # See <tt>Scoping#defaults</tt> for its scope equivalent.
450
535
  #
@@ -453,7 +538,7 @@ module ActionDispatch
453
538
  # false, the pattern matches any request prefixed with the given path.
454
539
  #
455
540
  # # Matches any request starting with 'path'
456
- # match 'path', to: 'c#a', anchor: false
541
+ # match 'path', to: 'c#a', anchor: false, via: :get
457
542
  #
458
543
  # [:format]
459
544
  # Allows you to specify the default value for optional +format+
@@ -495,12 +580,15 @@ module ActionDispatch
495
580
 
496
581
  raise "A rack application must be specified" unless path
497
582
 
498
- options[:as] ||= app_name(app)
583
+ rails_app = rails_app? app
584
+ options[:as] ||= app_name(app, rails_app)
585
+
586
+ target_as = name_for_action(options[:as], path)
499
587
  options[:via] ||= :all
500
588
 
501
589
  match(path, options.merge(:to => app, :anchor => false, :format => false))
502
590
 
503
- define_generate_prefix(app, options[:as])
591
+ define_generate_prefix(app, target_as) if rails_app
504
592
  self
505
593
  end
506
594
 
@@ -521,35 +609,37 @@ module ActionDispatch
521
609
  end
522
610
 
523
611
  private
524
- def app_name(app)
525
- return unless app.respond_to?(:routes)
612
+ def rails_app?(app)
613
+ app.is_a?(Class) && app < Rails::Railtie
614
+ end
526
615
 
527
- if app.respond_to?(:railtie_name)
616
+ def app_name(app, rails_app)
617
+ if rails_app
528
618
  app.railtie_name
529
- else
530
- class_name = app.class.is_a?(Class) ? app.name : app.class.name
619
+ elsif app.is_a?(Class)
620
+ class_name = app.name
531
621
  ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
532
622
  end
533
623
  end
534
624
 
535
625
  def define_generate_prefix(app, name)
536
- return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
537
-
538
- _route = @set.named_routes.routes[name.to_sym]
626
+ _route = @set.named_routes.get name
539
627
  _routes = @set
540
628
  app.routes.define_mounted_helper(name)
541
- app.routes.singleton_class.class_eval do
542
- define_method :mounted? do
543
- true
544
- end
545
-
546
- define_method :_generate_prefix do |options|
547
- prefix_options = options.slice(*_route.segment_keys)
548
- # we must actually delete prefix segment keys to avoid passing them to next url_for
549
- _route.segment_keys.each { |k| options.delete(k) }
550
- _routes.url_helpers.send("#{name}_path", prefix_options)
629
+ app.routes.extend Module.new {
630
+ def optimize_routes_generation?; false; end
631
+ define_method :find_script_name do |options|
632
+ if options.key? :script_name
633
+ super(options)
634
+ else
635
+ prefix_options = options.slice(*_route.segment_keys)
636
+ prefix_options[:relative_url_root] = ''.freeze
637
+ # we must actually delete prefix segment keys to avoid passing them to next url_for
638
+ _route.segment_keys.each { |k| options.delete(k) }
639
+ _routes.url_helpers.send("#{name}_path", prefix_options)
640
+ end
551
641
  end
552
- end
642
+ }
553
643
  end
554
644
  end
555
645
 
@@ -636,7 +726,7 @@ module ActionDispatch
636
726
  # resources :posts, module: "admin"
637
727
  #
638
728
  # If you want to route /admin/posts to +PostsController+
639
- # (without the Admin:: module prefix), you could use
729
+ # (without the <tt>Admin::</tt> module prefix), you could use
640
730
  #
641
731
  # scope "/admin" do
642
732
  # resources :posts
@@ -690,14 +780,19 @@ module ActionDispatch
690
780
  # end
691
781
  def scope(*args)
692
782
  options = args.extract_options!.dup
693
- recover = {}
783
+ scope = {}
694
784
 
695
785
  options[:path] = args.flatten.join('/') if args.any?
696
786
  options[:constraints] ||= {}
697
787
 
788
+ unless nested_scope?
789
+ options[:shallow_path] ||= options[:path] if options.key?(:path)
790
+ options[:shallow_prefix] ||= options[:as] if options.key?(:as)
791
+ end
792
+
698
793
  if options[:constraints].is_a?(Hash)
699
- defaults = options[:constraints].select do
700
- |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
794
+ defaults = options[:constraints].select do |k, v|
795
+ URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
701
796
  end
702
797
 
703
798
  (options[:defaults] ||= {}).reverse_merge!(defaults)
@@ -705,7 +800,7 @@ module ActionDispatch
705
800
  block, options[:constraints] = options[:constraints], {}
706
801
  end
707
802
 
708
- SCOPE_OPTIONS.each do |option|
803
+ @scope.options.each do |option|
709
804
  if option == :blocks
710
805
  value = block
711
806
  elsif option == :options
@@ -715,15 +810,15 @@ module ActionDispatch
715
810
  end
716
811
 
717
812
  if value
718
- recover[option] = @scope[option]
719
- @scope[option] = send("merge_#{option}_scope", @scope[option], value)
813
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
720
814
  end
721
815
  end
722
816
 
817
+ @scope = @scope.new scope
723
818
  yield
724
819
  self
725
820
  ensure
726
- @scope.merge!(recover)
821
+ @scope = @scope.parent
727
822
  end
728
823
 
729
824
  # Scopes routes to a specific controller
@@ -776,9 +871,16 @@ module ActionDispatch
776
871
  # end
777
872
  def namespace(path, options = {})
778
873
  path = path.to_s
779
- options = { :path => path, :as => path, :module => path,
780
- :shallow_path => path, :shallow_prefix => path }.merge!(options)
781
- scope(options) { yield }
874
+
875
+ defaults = {
876
+ module: path,
877
+ path: options.fetch(:path, path),
878
+ as: options.fetch(:as, path),
879
+ shallow_path: options.fetch(:path, path),
880
+ shallow_prefix: options.fetch(:as, path)
881
+ }
882
+
883
+ scope(defaults.merge!(options)) { yield }
782
884
  end
783
885
 
784
886
  # === Parameter Restriction
@@ -954,8 +1056,6 @@ module ActionDispatch
954
1056
  VALID_ON_OPTIONS = [:new, :collection, :member]
955
1057
  RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
956
1058
  CANONICAL_ACTIONS = %w(index create new show update destroy)
957
- RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
958
- RESOURCE_SCOPES = [:resource, :resources]
959
1059
 
960
1060
  class Resource #:nodoc:
961
1061
  attr_reader :controller, :path, :options, :param
@@ -967,6 +1067,7 @@ module ActionDispatch
967
1067
  @as = options[:as]
968
1068
  @param = (options[:param] || :id).to_sym
969
1069
  @options = options
1070
+ @shallow = false
970
1071
  end
971
1072
 
972
1073
  def default_actions
@@ -1027,6 +1128,13 @@ module ActionDispatch
1027
1128
  "#{path}/:#{nested_param}"
1028
1129
  end
1029
1130
 
1131
+ def shallow=(value)
1132
+ @shallow = value
1133
+ end
1134
+
1135
+ def shallow?
1136
+ @shallow
1137
+ end
1030
1138
  end
1031
1139
 
1032
1140
  class SingletonResource < Resource #:nodoc:
@@ -1066,18 +1174,18 @@ module ActionDispatch
1066
1174
  # a singular resource to map /profile (rather than /profile/:id) to
1067
1175
  # the show action:
1068
1176
  #
1069
- # resource :geocoder
1177
+ # resource :profile
1070
1178
  #
1071
1179
  # creates six different routes in your application, all mapping to
1072
- # the +GeoCoders+ controller (note that the controller is named after
1180
+ # the +Profiles+ controller (note that the controller is named after
1073
1181
  # the plural):
1074
1182
  #
1075
- # GET /geocoder/new
1076
- # POST /geocoder
1077
- # GET /geocoder
1078
- # GET /geocoder/edit
1079
- # PATCH/PUT /geocoder
1080
- # DELETE /geocoder
1183
+ # GET /profile/new
1184
+ # POST /profile
1185
+ # GET /profile
1186
+ # GET /profile/edit
1187
+ # PATCH/PUT /profile
1188
+ # DELETE /profile
1081
1189
  #
1082
1190
  # === Options
1083
1191
  # Takes same options as +resources+.
@@ -1307,8 +1415,10 @@ module ActionDispatch
1307
1415
  end
1308
1416
 
1309
1417
  with_scope_level(:member) do
1310
- scope(parent_resource.member_scope) do
1311
- yield
1418
+ if shallow?
1419
+ shallow_scope(parent_resource.member_scope) { yield }
1420
+ else
1421
+ scope(parent_resource.member_scope) { yield }
1312
1422
  end
1313
1423
  end
1314
1424
  end
@@ -1331,16 +1441,8 @@ module ActionDispatch
1331
1441
  end
1332
1442
 
1333
1443
  with_scope_level(:nested) do
1334
- if shallow?
1335
- with_exclusive_scope do
1336
- if @scope[:shallow_path].blank?
1337
- scope(parent_resource.nested_scope, nested_options) { yield }
1338
- else
1339
- scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
1340
- scope(parent_resource.nested_scope, nested_options) { yield }
1341
- end
1342
- end
1343
- end
1444
+ if shallow? && shallow_nesting_depth >= 1
1445
+ shallow_scope(parent_resource.nested_scope, nested_options) { yield }
1344
1446
  else
1345
1447
  scope(parent_resource.nested_scope, nested_options) { yield }
1346
1448
  end
@@ -1357,7 +1459,7 @@ module ActionDispatch
1357
1459
  end
1358
1460
 
1359
1461
  def shallow
1360
- scope(:shallow => true, :shallow_path => @scope[:path]) do
1462
+ scope(:shallow => true) do
1361
1463
  yield
1362
1464
  end
1363
1465
  end
@@ -1373,7 +1475,20 @@ module ActionDispatch
1373
1475
  if rest.empty? && Hash === path
1374
1476
  options = path
1375
1477
  path, to = options.find { |name, _value| name.is_a?(String) }
1376
- options[:to] = to
1478
+
1479
+ case to
1480
+ when Symbol
1481
+ options[:action] = to
1482
+ when String
1483
+ if to =~ /#/
1484
+ options[:to] = to
1485
+ else
1486
+ options[:controller] = to
1487
+ end
1488
+ else
1489
+ options[:to] = to
1490
+ end
1491
+
1377
1492
  options.delete(path)
1378
1493
  paths = [path]
1379
1494
  else
@@ -1398,6 +1513,7 @@ module ActionDispatch
1398
1513
  path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
1399
1514
  if using_match_shorthand?(path_without_format, route_options)
1400
1515
  route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
1516
+ route_options[:to].tr!("-", "_")
1401
1517
  end
1402
1518
 
1403
1519
  decomposed_match(_path, route_options)
@@ -1406,14 +1522,14 @@ module ActionDispatch
1406
1522
  end
1407
1523
 
1408
1524
  def using_match_shorthand?(path, options)
1409
- path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
1525
+ path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
1410
1526
  end
1411
1527
 
1412
1528
  def decomposed_match(path, options) # :nodoc:
1413
1529
  if on = options.delete(:on)
1414
1530
  send(on) { decomposed_match(path, options) }
1415
1531
  else
1416
- case @scope[:scope_level]
1532
+ case @scope.scope_level
1417
1533
  when :resources
1418
1534
  nested { decomposed_match(path, options) }
1419
1535
  when :resource
@@ -1426,21 +1542,23 @@ module ActionDispatch
1426
1542
 
1427
1543
  def add_route(action, options) # :nodoc:
1428
1544
  path = path_for_action(action, options.delete(:path))
1545
+ raise ArgumentError, "path is required" if path.blank?
1546
+
1429
1547
  action = action.to_s.dup
1430
1548
 
1431
- if action =~ /^[\w\/]+$/
1432
- options[:action] ||= action unless action.include?("/")
1549
+ if action =~ /^[\w\-\/]+$/
1550
+ options[:action] ||= action.tr('-', '_') unless action.include?("/")
1433
1551
  else
1434
1552
  action = nil
1435
1553
  end
1436
1554
 
1437
- if !options.fetch(:as, true)
1438
- options.delete(:as)
1439
- else
1440
- options[:as] = name_for_action(options[:as], action)
1441
- end
1555
+ as = if !options.fetch(:as, true) # if it's set to nil or false
1556
+ options.delete(:as)
1557
+ else
1558
+ name_for_action(options.delete(:as), action)
1559
+ end
1442
1560
 
1443
- mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1561
+ mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
1444
1562
  app, conditions, requirements, defaults, as, anchor = mapping.to_route
1445
1563
  @set.add_route(app, conditions, requirements, defaults, as, anchor)
1446
1564
  end
@@ -1454,7 +1572,7 @@ module ActionDispatch
1454
1572
  raise ArgumentError, "must be called with a path and/or options"
1455
1573
  end
1456
1574
 
1457
- if @scope[:scope_level] == :resources
1575
+ if @scope.resources?
1458
1576
  with_scope_level(:root) do
1459
1577
  scope(parent_resource.path) do
1460
1578
  super(options)
@@ -1477,6 +1595,13 @@ module ActionDispatch
1477
1595
  return true
1478
1596
  end
1479
1597
 
1598
+ if options.delete(:shallow)
1599
+ shallow do
1600
+ send(method, resources.pop, options, &block)
1601
+ end
1602
+ return true
1603
+ end
1604
+
1480
1605
  if resource_scope?
1481
1606
  nested { send(method, resources.pop, options, &block) }
1482
1607
  return true
@@ -1514,41 +1639,47 @@ module ActionDispatch
1514
1639
  end
1515
1640
 
1516
1641
  def resource_scope? #:nodoc:
1517
- RESOURCE_SCOPES.include? @scope[:scope_level]
1642
+ @scope.resource_scope?
1518
1643
  end
1519
1644
 
1520
1645
  def resource_method_scope? #:nodoc:
1521
- RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1646
+ @scope.resource_method_scope?
1647
+ end
1648
+
1649
+ def nested_scope? #:nodoc:
1650
+ @scope.nested?
1522
1651
  end
1523
1652
 
1524
1653
  def with_exclusive_scope
1525
1654
  begin
1526
- old_name_prefix, old_path = @scope[:as], @scope[:path]
1527
- @scope[:as], @scope[:path] = nil, nil
1655
+ @scope = @scope.new(:as => nil, :path => nil)
1528
1656
 
1529
1657
  with_scope_level(:exclusive) do
1530
1658
  yield
1531
1659
  end
1532
1660
  ensure
1533
- @scope[:as], @scope[:path] = old_name_prefix, old_path
1661
+ @scope = @scope.parent
1534
1662
  end
1535
1663
  end
1536
1664
 
1537
- def with_scope_level(kind, resource = parent_resource)
1538
- old, @scope[:scope_level] = @scope[:scope_level], kind
1539
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1665
+ def with_scope_level(kind)
1666
+ @scope = @scope.new_level(kind)
1540
1667
  yield
1541
1668
  ensure
1542
- @scope[:scope_level] = old
1543
- @scope[:scope_level_resource] = old_resource
1669
+ @scope = @scope.parent
1544
1670
  end
1545
1671
 
1546
1672
  def resource_scope(kind, resource) #:nodoc:
1547
- with_scope_level(kind, resource) do
1548
- scope(parent_resource.resource_scope) do
1549
- yield
1550
- end
1673
+ resource.shallow = @scope[:shallow]
1674
+ @scope = @scope.new(:scope_level_resource => resource)
1675
+ @nesting.push(resource)
1676
+
1677
+ with_scope_level(kind) do
1678
+ scope(parent_resource.resource_scope) { yield }
1551
1679
  end
1680
+ ensure
1681
+ @nesting.pop
1682
+ @scope = @scope.parent
1552
1683
  end
1553
1684
 
1554
1685
  def nested_options #:nodoc:
@@ -1560,6 +1691,14 @@ module ActionDispatch
1560
1691
  options
1561
1692
  end
1562
1693
 
1694
+ def nesting_depth #:nodoc:
1695
+ @nesting.size
1696
+ end
1697
+
1698
+ def shallow_nesting_depth #:nodoc:
1699
+ @nesting.select(&:shallow?).size
1700
+ end
1701
+
1563
1702
  def param_constraint? #:nodoc:
1564
1703
  @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
1565
1704
  end
@@ -1568,22 +1707,25 @@ module ActionDispatch
1568
1707
  @scope[:constraints][parent_resource.param]
1569
1708
  end
1570
1709
 
1571
- def canonical_action?(action, flag) #:nodoc:
1572
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1710
+ def canonical_action?(action) #:nodoc:
1711
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
1573
1712
  end
1574
1713
 
1575
- def shallow_scoping? #:nodoc:
1576
- shallow? && @scope[:scope_level] == :member
1714
+ def shallow_scope(path, options = {}) #:nodoc:
1715
+ scope = { :as => @scope[:shallow_prefix],
1716
+ :path => @scope[:shallow_path] }
1717
+ @scope = @scope.new scope
1718
+
1719
+ scope(path, options) { yield }
1720
+ ensure
1721
+ @scope = @scope.parent
1577
1722
  end
1578
1723
 
1579
1724
  def path_for_action(action, path) #:nodoc:
1580
- prefix = shallow_scoping? ?
1581
- "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
1582
-
1583
- if canonical_action?(action, path.blank?)
1584
- prefix.to_s
1725
+ if path.blank? && canonical_action?(action)
1726
+ @scope[:path].to_s
1585
1727
  else
1586
- "#{prefix}/#{action_path(action, path)}"
1728
+ "#{@scope[:path]}/#{action_path(action, path)}"
1587
1729
  end
1588
1730
  end
1589
1731
 
@@ -1594,15 +1736,18 @@ module ActionDispatch
1594
1736
 
1595
1737
  def prefix_name_for_action(as, action) #:nodoc:
1596
1738
  if as
1597
- as.to_s
1598
- elsif !canonical_action?(action, @scope[:scope_level])
1599
- action.to_s
1739
+ prefix = as
1740
+ elsif !canonical_action?(action)
1741
+ prefix = action
1742
+ end
1743
+
1744
+ if prefix && prefix != '/' && !prefix.empty?
1745
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
1600
1746
  end
1601
1747
  end
1602
1748
 
1603
1749
  def name_for_action(as, action) #:nodoc:
1604
1750
  prefix = prefix_name_for_action(as, action)
1605
- prefix = Mapper.normalize_name(prefix) if prefix
1606
1751
  name_prefix = @scope[:as]
1607
1752
 
1608
1753
  if parent_resource
@@ -1612,27 +1757,14 @@ module ActionDispatch
1612
1757
  member_name = parent_resource.member_name
1613
1758
  end
1614
1759
 
1615
- name = case @scope[:scope_level]
1616
- when :nested
1617
- [name_prefix, prefix]
1618
- when :collection
1619
- [prefix, name_prefix, collection_name]
1620
- when :new
1621
- [prefix, :new, name_prefix, member_name]
1622
- when :member
1623
- [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
1624
- when :root
1625
- [name_prefix, collection_name, prefix]
1626
- else
1627
- [name_prefix, member_name, prefix]
1628
- end
1760
+ name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1629
1761
 
1630
- if candidate = name.select(&:present?).join("_").presence
1762
+ if candidate = name.compact.join("_").presence
1631
1763
  # If a name was not explicitly given, we check if it is valid
1632
1764
  # and return nil in case it isn't. Otherwise, we pass the invalid name
1633
1765
  # forward so the underlying router engine treats it and raises an exception.
1634
1766
  if as.nil?
1635
- candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
1767
+ candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
1636
1768
  else
1637
1769
  candidate
1638
1770
  end
@@ -1757,10 +1889,85 @@ module ActionDispatch
1757
1889
  end
1758
1890
  end
1759
1891
 
1892
+ class Scope # :nodoc:
1893
+ OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
1894
+ :controller, :action, :path_names, :constraints,
1895
+ :shallow, :blocks, :defaults, :options]
1896
+
1897
+ RESOURCE_SCOPES = [:resource, :resources]
1898
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1899
+
1900
+ attr_reader :parent, :scope_level
1901
+
1902
+ def initialize(hash, parent = {}, scope_level = nil)
1903
+ @hash = hash
1904
+ @parent = parent
1905
+ @scope_level = scope_level
1906
+ end
1907
+
1908
+ def nested?
1909
+ scope_level == :nested
1910
+ end
1911
+
1912
+ def resources?
1913
+ scope_level == :resources
1914
+ end
1915
+
1916
+ def resource_method_scope?
1917
+ RESOURCE_METHOD_SCOPES.include? scope_level
1918
+ end
1919
+
1920
+ def action_name(name_prefix, prefix, collection_name, member_name)
1921
+ case scope_level
1922
+ when :nested
1923
+ [name_prefix, prefix]
1924
+ when :collection
1925
+ [prefix, name_prefix, collection_name]
1926
+ when :new
1927
+ [prefix, :new, name_prefix, member_name]
1928
+ when :member
1929
+ [prefix, name_prefix, member_name]
1930
+ when :root
1931
+ [name_prefix, collection_name, prefix]
1932
+ else
1933
+ [name_prefix, member_name, prefix]
1934
+ end
1935
+ end
1936
+
1937
+ def resource_scope?
1938
+ RESOURCE_SCOPES.include? scope_level
1939
+ end
1940
+
1941
+ def options
1942
+ OPTIONS
1943
+ end
1944
+
1945
+ def new(hash)
1946
+ self.class.new hash, self, scope_level
1947
+ end
1948
+
1949
+ def new_level(level)
1950
+ self.class.new(self, self, level)
1951
+ end
1952
+
1953
+ def fetch(key, &block)
1954
+ @hash.fetch(key, &block)
1955
+ end
1956
+
1957
+ def [](key)
1958
+ @hash.fetch(key) { @parent[key] }
1959
+ end
1960
+
1961
+ def []=(k,v)
1962
+ @hash[k] = v
1963
+ end
1964
+ end
1965
+
1760
1966
  def initialize(set) #:nodoc:
1761
1967
  @set = set
1762
- @scope = { :path_names => @set.resources_path_names }
1968
+ @scope = Scope.new({ :path_names => @set.resources_path_names })
1763
1969
  @concerns = {}
1970
+ @nesting = []
1764
1971
  end
1765
1972
 
1766
1973
  include Base