actionpack 4.1.7 → 4.2.11

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 (112) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +404 -451
  3. data/README.rdoc +7 -2
  4. data/lib/abstract_controller/base.rb +16 -6
  5. data/lib/abstract_controller/callbacks.rb +28 -51
  6. data/lib/abstract_controller/helpers.rb +11 -4
  7. data/lib/abstract_controller/railties/routes_helpers.rb +3 -3
  8. data/lib/abstract_controller/rendering.rb +7 -1
  9. data/lib/abstract_controller/url_for.rb +1 -1
  10. data/lib/action_controller/base.rb +3 -2
  11. data/lib/action_controller/caching/fragments.rb +7 -1
  12. data/lib/action_controller/caching.rb +1 -1
  13. data/lib/action_controller/log_subscriber.rb +26 -26
  14. data/lib/action_controller/metal/conditional_get.rb +37 -12
  15. data/lib/action_controller/metal/etag_with_template_digest.rb +50 -0
  16. data/lib/action_controller/metal/exceptions.rb +1 -1
  17. data/lib/action_controller/metal/force_ssl.rb +1 -1
  18. data/lib/action_controller/metal/head.rb +7 -3
  19. data/lib/action_controller/metal/http_authentication.rb +20 -10
  20. data/lib/action_controller/metal/instrumentation.rb +8 -5
  21. data/lib/action_controller/metal/live.rb +57 -6
  22. data/lib/action_controller/metal/mime_responds.rb +25 -246
  23. data/lib/action_controller/metal/params_wrapper.rb +5 -5
  24. data/lib/action_controller/metal/rack_delegation.rb +1 -1
  25. data/lib/action_controller/metal/redirecting.rb +14 -8
  26. data/lib/action_controller/metal/renderers.rb +29 -11
  27. data/lib/action_controller/metal/rendering.rb +2 -6
  28. data/lib/action_controller/metal/request_forgery_protection.rb +78 -7
  29. data/lib/action_controller/metal/streaming.rb +1 -1
  30. data/lib/action_controller/metal/strong_parameters.rb +129 -14
  31. data/lib/action_controller/metal/url_for.rb +11 -12
  32. data/lib/action_controller/metal.rb +12 -11
  33. data/lib/action_controller/model_naming.rb +1 -1
  34. data/lib/action_controller/railtie.rb +4 -0
  35. data/lib/action_controller/test_case.rb +119 -75
  36. data/lib/action_controller.rb +1 -1
  37. data/lib/action_dispatch/http/cache.rb +5 -4
  38. data/lib/action_dispatch/http/filter_parameters.rb +2 -2
  39. data/lib/action_dispatch/http/headers.rb +43 -9
  40. data/lib/action_dispatch/http/mime_negotiation.rb +10 -3
  41. data/lib/action_dispatch/http/mime_type.rb +18 -4
  42. data/lib/action_dispatch/http/parameter_filter.rb +1 -1
  43. data/lib/action_dispatch/http/parameters.rb +11 -26
  44. data/lib/action_dispatch/http/request.rb +37 -11
  45. data/lib/action_dispatch/http/response.rb +74 -23
  46. data/lib/action_dispatch/http/upload.rb +9 -8
  47. data/lib/action_dispatch/http/url.rb +89 -70
  48. data/lib/action_dispatch/journey/formatter.rb +34 -18
  49. data/lib/action_dispatch/journey/gtg/builder.rb +3 -3
  50. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -7
  51. data/lib/action_dispatch/journey/gtg/transition_table.rb +20 -28
  52. data/lib/action_dispatch/journey/nfa/dot.rb +2 -2
  53. data/lib/action_dispatch/journey/nfa/simulator.rb +1 -1
  54. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -5
  55. data/lib/action_dispatch/journey/nodes/node.rb +4 -0
  56. data/lib/action_dispatch/journey/parser.rb +52 -60
  57. data/lib/action_dispatch/journey/parser.y +11 -10
  58. data/lib/action_dispatch/journey/path/pattern.rb +16 -19
  59. data/lib/action_dispatch/journey/route.rb +4 -19
  60. data/lib/action_dispatch/journey/router/strexp.rb +9 -6
  61. data/lib/action_dispatch/journey/router/utils.rb +1 -1
  62. data/lib/action_dispatch/journey/router.rb +53 -77
  63. data/lib/action_dispatch/journey/routes.rb +4 -0
  64. data/lib/action_dispatch/journey/scanner.rb +5 -5
  65. data/lib/action_dispatch/journey/visitors.rb +81 -92
  66. data/lib/action_dispatch/journey/visualizer/fsm.css +0 -4
  67. data/lib/action_dispatch/journey/visualizer/index.html.erb +2 -2
  68. data/lib/action_dispatch/middleware/callbacks.rb +1 -1
  69. data/lib/action_dispatch/middleware/cookies.rb +34 -34
  70. data/lib/action_dispatch/middleware/debug_exceptions.rb +15 -4
  71. data/lib/action_dispatch/middleware/exception_wrapper.rb +50 -18
  72. data/lib/action_dispatch/middleware/flash.rb +13 -7
  73. data/lib/action_dispatch/middleware/params_parser.rb +1 -1
  74. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -3
  75. data/lib/action_dispatch/middleware/remote_ip.rb +40 -54
  76. data/lib/action_dispatch/middleware/request_id.rb +1 -1
  77. data/lib/action_dispatch/middleware/session/cookie_store.rb +1 -1
  78. data/lib/action_dispatch/middleware/show_exceptions.rb +1 -0
  79. data/lib/action_dispatch/middleware/ssl.rb +1 -1
  80. data/lib/action_dispatch/middleware/static.rb +75 -39
  81. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +21 -19
  82. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +37 -9
  83. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +2 -8
  84. data/lib/action_dispatch/middleware/templates/rescues/{diagnostics.erb → diagnostics.html.erb} +0 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  86. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +6 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +4 -0
  88. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +2 -0
  89. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -24
  90. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +0 -1
  91. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +120 -64
  92. data/lib/action_dispatch/railtie.rb +2 -0
  93. data/lib/action_dispatch/routing/endpoint.rb +10 -0
  94. data/lib/action_dispatch/routing/inspector.rb +5 -12
  95. data/lib/action_dispatch/routing/mapper.rb +414 -283
  96. data/lib/action_dispatch/routing/polymorphic_routes.rb +191 -79
  97. data/lib/action_dispatch/routing/redirection.rb +10 -12
  98. data/lib/action_dispatch/routing/route_set.rb +300 -173
  99. data/lib/action_dispatch/routing/routes_proxy.rb +5 -4
  100. data/lib/action_dispatch/routing/url_for.rb +17 -5
  101. data/lib/action_dispatch/testing/assertions/dom.rb +2 -26
  102. data/lib/action_dispatch/testing/assertions/response.rb +2 -7
  103. data/lib/action_dispatch/testing/assertions/routing.rb +22 -22
  104. data/lib/action_dispatch/testing/assertions/selector.rb +2 -429
  105. data/lib/action_dispatch/testing/assertions/tag.rb +2 -134
  106. data/lib/action_dispatch/testing/assertions.rb +11 -7
  107. data/lib/action_dispatch/testing/integration.rb +28 -20
  108. data/lib/action_dispatch/testing/test_request.rb +1 -1
  109. data/lib/action_dispatch/testing/test_response.rb +1 -5
  110. data/lib/action_pack/gem_version.rb +3 -3
  111. metadata +55 -13
  112. data/lib/action_controller/metal/responder.rb +0 -297
@@ -0,0 +1,9 @@
1
+ <%= @exception.class.to_s %><%
2
+ if @request.parameters['controller']
3
+ %> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
4
+ <% end %>
5
+
6
+ <%= @exception.message %>
7
+ <%= render template: "rescues/_source" %>
8
+ <%= render template: "rescues/_trace" %>
9
+ <%= render template: "rescues/_request_and_response" %>
@@ -116,9 +116,15 @@
116
116
  background-color: #FFCCCC;
117
117
  }
118
118
 
119
+ .hidden {
120
+ display: none;
121
+ }
122
+
119
123
  a { color: #980905; }
120
124
  a:visited { color: #666; }
125
+ a.trace-frames { color: #666; }
121
126
  a:hover { color: #C52F24; }
127
+ a.trace-frames.selected { color: #C52F24 }
122
128
 
123
129
  <%= yield :style %>
124
130
  </style>
@@ -4,4 +4,8 @@
4
4
 
5
5
  <div id="container">
6
6
  <h2><%= h @exception.message %></h2>
7
+
8
+ <%= render template: "rescues/_source" %>
9
+ <%= render template: "rescues/_trace" %>
10
+ <%= render template: "rescues/_request_and_response" %>
7
11
  </div>
@@ -27,4 +27,6 @@
27
27
 
28
28
  <%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %>
29
29
  <% end %>
30
+
31
+ <%= render template: "rescues/_request_and_response" %>
30
32
  </div>
@@ -1,4 +1,3 @@
1
- <% @source_extract = @exception.source_extract(0, :html) %>
2
1
  <header>
3
2
  <h1>
4
3
  <%= @exception.original_exception.class.to_s %> in
@@ -12,29 +11,7 @@
12
11
  </p>
13
12
  <pre><code><%= h @exception.message %></code></pre>
14
13
 
15
- <div class="source">
16
- <div class="info">
17
- <p>Extracted source (around line <strong>#<%= @exception.line_number %></strong>):</p>
18
- </div>
19
- <div class="data">
20
- <table cellpadding="0" cellspacing="0" class="lines">
21
- <tr>
22
- <td>
23
- <pre class="line_numbers">
24
- <% @source_extract.keys.each do |line_number| %>
25
- <span><%= line_number -%></span>
26
- <% end %>
27
- </pre>
28
- </td>
29
- <td width="100%">
30
- <pre>
31
- <% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @exception.line_number -%>"><%= source -%></div><% end -%>
32
- </pre>
33
- </td>
34
- </tr>
35
- </table>
36
- </div>
37
- </div>
14
+ <%= render template: "rescues/_source" %>
38
15
 
39
16
  <p><%= @exception.sub_template_message %></p>
40
17
 
@@ -1,4 +1,3 @@
1
- <% @source_extract = @exception.source_extract(0, :html) %>
2
1
  <%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
3
2
 
4
3
  Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised:
@@ -1,24 +1,44 @@
1
1
  <% content_for :style do %>
2
2
  #route_table {
3
- margin: 0 auto 0;
3
+ margin: 0;
4
4
  border-collapse: collapse;
5
5
  }
6
6
 
7
- #route_table td {
8
- padding: 0 30px;
7
+ #route_table thead tr {
8
+ border-bottom: 2px solid #ddd;
9
+ }
10
+
11
+ #route_table thead tr.bottom {
12
+ border-bottom: none;
9
13
  }
10
14
 
11
- #route_table tr.bottom th {
12
- padding-bottom: 10px;
15
+ #route_table thead tr.bottom th {
16
+ padding: 10px 0;
13
17
  line-height: 15px;
14
18
  }
15
19
 
16
- #route_table .matched_paths {
20
+ #route_table tbody tr {
21
+ border-bottom: 1px solid #ddd;
22
+ }
23
+
24
+ #route_table tbody tr:nth-child(odd) {
25
+ background: #f2f2f2;
26
+ }
27
+
28
+ #route_table tbody.exact_matches,
29
+ #route_table tbody.fuzzy_matches {
17
30
  background-color: LightGoldenRodYellow;
31
+ border-bottom: solid 2px SlateGrey;
18
32
  }
19
33
 
20
- #route_table .matched_paths {
21
- border-bottom: solid 3px SlateGrey;
34
+ #route_table tbody.exact_matches tr,
35
+ #route_table tbody.fuzzy_matches tr {
36
+ background: none;
37
+ border-bottom: none;
38
+ }
39
+
40
+ #route_table td {
41
+ padding: 4px 30px;
22
42
  }
23
43
 
24
44
  #path_search {
@@ -45,13 +65,15 @@
45
65
  <th><%# HTTP Verb %>
46
66
  </th>
47
67
  <th><%# Path %>
48
- <%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %>
68
+ <%= search_field(:path, nil, id: 'search', placeholder: "Path Match") %>
49
69
  </th>
50
70
  <th><%# Controller#action %>
51
71
  </th>
52
72
  </tr>
53
73
  </thead>
54
- <tbody class='matched_paths' id='matched_paths'>
74
+ <tbody class='exact_matches' id='exact_matches'>
75
+ </tbody>
76
+ <tbody class='fuzzy_matches' id='fuzzy_matches'>
55
77
  </tbody>
56
78
  <tbody>
57
79
  <%= yield %>
@@ -59,6 +81,7 @@
59
81
  </table>
60
82
 
61
83
  <script type='text/javascript'>
84
+ // Iterates each element through a function
62
85
  function each(elems, func) {
63
86
  if (!elems instanceof Array) { elems = [elems]; }
64
87
  for (var i = 0, len = elems.length; i < len; i++) {
@@ -66,77 +89,110 @@
66
89
  }
67
90
  }
68
91
 
69
- function setValOn(elems, val) {
70
- each(elems, function(elem) {
71
- elem.innerHTML = val;
72
- });
92
+ // Sets innerHTML for an element
93
+ function setContent(elem, text) {
94
+ elem.innerHTML = text;
73
95
  }
74
96
 
75
- function onClick(elems, func) {
76
- each(elems, function(elem) {
77
- elem.onclick = func;
78
- });
79
- }
97
+ // Enables path search functionality
98
+ function setupMatchPaths() {
99
+ // Check if the user input (sanitized as a path) matches the regexp data attribute
100
+ function checkExactMatch(section, elem, value) {
101
+ var string = sanitizePath(value),
102
+ regexp = elem.getAttribute("data-regexp");
80
103
 
81
- // Enables functionality to toggle between `_path` and `_url` helper suffixes
82
- function setupRouteToggleHelperLinks() {
83
- var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
84
- onClick(toggleLinks, function(){
85
- var helperTxt = this.getAttribute("data-route-helper"),
86
- helperElems = document.querySelectorAll('[data-route-name] span.helper');
87
- setValOn(helperElems, helperTxt);
88
- });
89
- }
104
+ showMatch(string, regexp, section, elem);
105
+ }
90
106
 
91
- // takes an array of elements with a data-regexp attribute and
92
- // passes their parent <tr> into the callback function
93
- // if the regexp matches a given path
94
- function eachElemsForPath(elems, path, func) {
95
- each(elems, function(e){
96
- var reg = e.getAttribute("data-regexp");
97
- if (path.match(RegExp(reg))) {
98
- func(e.parentNode.cloneNode(true));
99
- }
100
- })
101
- }
107
+ // Check if the route path data attribute contains the user input
108
+ function checkFuzzyMatch(section, elem, value) {
109
+ var string = elem.getAttribute("data-route-path"),
110
+ regexp = value;
102
111
 
103
- // Ensure path always starts with a slash "/" and remove params or fragments
104
- function sanitizePath(path) {
105
- var path = path.charAt(0) == '/' ? path : "/" + path;
106
- return path.replace(/\#.*|\?.*/, '');
107
- }
112
+ showMatch(string, regexp, section, elem);
113
+ }
108
114
 
109
- // Enables path search functionality
110
- function setupMatchPaths() {
111
- var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
112
- pathElem = document.querySelector('#path_search'),
113
- selectedSection = document.querySelector('#matched_paths'),
114
- noMatchText = '<tr><th colspan="4">None</th></tr>';
115
+ // Display the parent <tr> element in the appropriate section when there's a match
116
+ function showMatch(string, regexp, section, elem) {
117
+ if(string.match(RegExp(regexp))) {
118
+ section.appendChild(elem.parentNode.cloneNode(true));
119
+ }
120
+ }
121
+
122
+ // Check if there are any matched results in a section
123
+ function checkNoMatch(section, defaultText, noMatchText) {
124
+ if (section.innerHTML === defaultText) {
125
+ setContent(section, defaultText + noMatchText);
126
+ }
127
+ }
115
128
 
129
+ // Ensure path always starts with a slash "/" and remove params or fragments
130
+ function sanitizePath(path) {
131
+ var path = path.charAt(0) == '/' ? path : "/" + path;
132
+ return path.replace(/\#.*|\?.*/, '');
133
+ }
116
134
 
117
- // Remove matches if no path is present
118
- pathElem.onblur = function(e) {
119
- if (pathElem.value === "") selectedSection.innerHTML = "";
135
+ var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
136
+ searchElem = document.querySelector('#search'),
137
+ exactMatches = document.querySelector('#exact_matches'),
138
+ fuzzyMatches = document.querySelector('#fuzzy_matches');
139
+
140
+ // Remove matches when no search value is present
141
+ searchElem.onblur = function(e) {
142
+ if (searchElem.value === "") {
143
+ setContent(exactMatches, "");
144
+ setContent(fuzzyMatches, "");
145
+ }
120
146
  }
121
147
 
122
148
  // On key press perform a search for matching paths
123
- pathElem.onkeyup = function(e){
124
- var path = sanitizePath(pathElem.value),
125
- defaultText = '<tr><th colspan="4">Paths Matching (' + escape(path) + '):</th></tr>';
149
+ searchElem.onkeyup = function(e){
150
+ var userInput = searchElem.value,
151
+ defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>',
152
+ defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>',
153
+ noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>',
154
+ noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>';
126
155
 
127
156
  // Clear out results section
128
- selectedSection.innerHTML= defaultText;
157
+ setContent(exactMatches, defaultExactMatch);
158
+ setContent(fuzzyMatches, defaultFuzzyMatch);
159
+
160
+ // Display exact matches and fuzzy matches
161
+ each(regexpElems, function(elem) {
162
+ checkExactMatch(exactMatches, elem, userInput);
163
+ checkFuzzyMatch(fuzzyMatches, elem, userInput);
164
+ })
165
+
166
+ // Display 'No Matches' message when no matches are found
167
+ checkNoMatch(exactMatches, defaultExactMatch, noExactMatch);
168
+ checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch);
169
+ }
170
+ }
129
171
 
130
- // Display matches if they exist
131
- eachElemsForPath(regexpElems, path, function(e){
132
- selectedSection.appendChild(e);
172
+ // Enables functionality to toggle between `_path` and `_url` helper suffixes
173
+ function setupRouteToggleHelperLinks() {
174
+
175
+ // Sets content for each element
176
+ function setValOn(elems, val) {
177
+ each(elems, function(elem) {
178
+ setContent(elem, val);
133
179
  });
180
+ }
134
181
 
135
- // If no match present, tell the user
136
- if (selectedSection.innerHTML === defaultText) {
137
- selectedSection.innerHTML = selectedSection.innerHTML + noMatchText;
138
- }
182
+ // Sets onClick event for each element
183
+ function onClick(elems, func) {
184
+ each(elems, function(elem) {
185
+ elem.onclick = func;
186
+ });
139
187
  }
188
+
189
+ var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
190
+ onClick(toggleLinks, function(){
191
+ var helperTxt = this.getAttribute("data-route-helper"),
192
+ helperElems = document.querySelectorAll('[data-route-name] span.helper');
193
+
194
+ setValOn(helperElems, helperTxt);
195
+ });
140
196
  }
141
197
 
142
198
  setupMatchPaths();
@@ -40,6 +40,8 @@ module ActionDispatch
40
40
  ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
41
41
 
42
42
  ActionDispatch.test_app = app
43
+
44
+ ActionDispatch::Routing::RouteSet.relative_url_root = app.config.relative_url_root
43
45
  end
44
46
  end
45
47
  end
@@ -0,0 +1,10 @@
1
+ module ActionDispatch
2
+ module Routing
3
+ class Endpoint # :nodoc:
4
+ def dispatcher?; false; end
5
+ def redirect?; false; end
6
+ def matches?(req); true; end
7
+ def app; self; end
8
+ end
9
+ end
10
+ end
@@ -5,22 +5,15 @@ module ActionDispatch
5
5
  module Routing
6
6
  class RouteWrapper < SimpleDelegator
7
7
  def endpoint
8
- rack_app ? rack_app.inspect : "#{controller}##{action}"
8
+ app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
9
9
  end
10
10
 
11
11
  def constraints
12
12
  requirements.except(:controller, :action)
13
13
  end
14
14
 
15
- def rack_app(app = self.app)
16
- @rack_app ||= begin
17
- class_name = app.class.name.to_s
18
- if class_name == "ActionDispatch::Routing::Mapper::Constraints"
19
- rack_app(app.app)
20
- elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
21
- app
22
- end
23
- end
15
+ def rack_app
16
+ app.app
24
17
  end
25
18
 
26
19
  def verb
@@ -55,7 +48,7 @@ module ActionDispatch
55
48
  def reqs
56
49
  @reqs ||= begin
57
50
  reqs = endpoint
58
- reqs += " #{constraints.to_s}" unless constraints.empty?
51
+ reqs += " #{constraints}" unless constraints.empty?
59
52
  reqs
60
53
  end
61
54
  end
@@ -73,7 +66,7 @@ module ActionDispatch
73
66
  end
74
67
 
75
68
  def engine?
76
- rack_app && rack_app.respond_to?(:routes)
69
+ rack_app.respond_to?(:routes)
77
70
  end
78
71
  end
79
72
 
@@ -4,132 +4,202 @@ 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
6
  require 'active_support/core_ext/module/remove_method'
7
+ require 'active_support/core_ext/string/filters'
7
8
  require 'active_support/inflector'
8
9
  require 'action_dispatch/routing/redirection'
10
+ require 'action_dispatch/routing/endpoint'
11
+ require 'active_support/deprecation'
9
12
 
10
13
  module ActionDispatch
11
14
  module Routing
12
15
  class Mapper
13
16
  URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
14
- SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
15
- :controller, :action, :path_names, :constraints,
16
- :shallow, :blocks, :defaults, :options]
17
-
18
- class Constraints #:nodoc:
19
- def self.new(app, constraints, request = Rack::Request)
20
- if constraints.any?
21
- super(app, constraints, request)
22
- else
23
- app
24
- end
25
- end
26
17
 
18
+ class Constraints < Endpoint #:nodoc:
27
19
  attr_reader :app, :constraints
28
20
 
29
- def initialize(app, constraints, request)
30
- @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
31
34
  end
32
35
 
33
- def matches?(env)
34
- req = @request.new(env)
36
+ def dispatcher?; @dispatcher; end
35
37
 
38
+ def matches?(req)
36
39
  @constraints.all? do |constraint|
37
40
  (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
38
41
  (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
39
42
  end
40
- ensure
41
- req.reset_parameters
42
43
  end
43
44
 
44
- def call(env)
45
- 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
46
53
  end
47
54
 
48
55
  private
49
56
  def constraint_args(constraint, request)
50
- constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
57
+ constraint.arity == 1 ? [request] : [request.path_parameters, request]
51
58
  end
52
59
  end
53
60
 
54
61
  class Mapping #:nodoc:
55
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
56
62
  ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
57
- WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
63
+ OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
58
64
 
59
- attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
65
+ attr_reader :requirements, :conditions, :defaults
66
+ attr_reader :to, :default_controller, :default_action, :as, :anchor
60
67
 
61
- def initialize(set, scope, path, options)
62
- @set, @scope, @path, @options = set, scope, path, options
63
- @requirements, @conditions, @defaults = {}, {}, {}
68
+ def self.build(scope, set, path, as, options)
69
+ options = scope[:options].merge(options) if scope[:options]
64
70
 
65
- normalize_options!
66
- normalize_path!
67
- normalize_requirements!
68
- normalize_conditions!
69
- normalize_defaults!
71
+ options.delete :only
72
+ options.delete :except
73
+ options.delete :shallow_path
74
+ options.delete :shallow_prefix
75
+ options.delete :shallow
76
+
77
+ defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
78
+
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)
70
127
  end
71
128
 
72
129
  def to_route
73
- [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
130
+ [ app(@blocks), conditions, requirements, defaults, as, anchor ]
74
131
  end
75
132
 
76
133
  private
77
134
 
78
- def normalize_path!
79
- raise ArgumentError, "path is required" if @path.blank?
80
- @path = Mapper.normalize_path(@path)
135
+ def normalize_path!(path, format)
136
+ path = Mapper.normalize_path(path)
81
137
 
82
- if required_format?
83
- @path = "#{@path}.:format"
84
- elsif optional_format?
85
- @path = "#{@path}(.:format)"
138
+ if format == true
139
+ "#{path}.:format"
140
+ elsif optional_format?(path, format)
141
+ "#{path}(.:format)"
142
+ else
143
+ path
86
144
  end
87
145
  end
88
146
 
89
- def required_format?
90
- options[:format] == true
147
+ def optional_format?(path, format)
148
+ format != false && path !~ OPTIONAL_FORMAT_REGEX
91
149
  end
92
150
 
93
- def optional_format?
94
- options[:format] != false && !path.include?(':format') && !path.end_with?('/')
95
- end
96
-
97
- def normalize_options!
98
- @options.reverse_merge!(scope[:options]) if scope[:options]
99
- path_without_format = path.sub(/\(\.:format\)$/, '')
100
-
151
+ def normalize_options!(options, formatted, path_params, path_ast, modyoule)
101
152
  # Add a constraint for wildcard route to make it non-greedy and match the
102
153
  # optional format part of the route by default
103
- if path_without_format.match(WILDCARD_PATH) && @options[:format] != false
104
- @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
105
158
  end
106
159
 
107
- if path_without_format.match(':controller')
108
- 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
109
162
 
110
163
  # Add a default constraint for :controller path segments that matches namespaced
111
164
  # controllers with default routes like :controller/:action/:id(.:format), e.g:
112
165
  # GET /admin/products/show/1
113
166
  # => { controller: 'admin/products', action: 'show', id: '1' }
114
- @options[:controller] ||= /.+?/
167
+ options[:controller] ||= /.+?/
115
168
  end
116
169
 
117
- @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
118
181
  end
119
182
 
120
- def normalize_requirements!
121
- constraints.each do |key, requirement|
122
- next unless segment_keys.include?(key) || key == :controller
123
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
124
- @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
125
191
  end
192
+ end
126
193
 
127
- if options[:format] == true
194
+ def normalize_format!(formatted)
195
+ if formatted == true
128
196
  @requirements[:format] ||= /.+/
129
- elsif Regexp === options[:format]
130
- @requirements[:format] = options[:format]
131
- elsif String === options[:format]
132
- @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
133
203
  end
134
204
  end
135
205
 
@@ -143,169 +213,147 @@ module ActionDispatch
143
213
  end
144
214
  end
145
215
 
146
- def normalize_defaults!
147
- @defaults.merge!(scope[:defaults]) if scope[:defaults]
148
- @defaults.merge!(options[:defaults]) if options[:defaults]
149
-
150
- options.each do |key, default|
151
- unless Regexp === default || IGNORE_OPTIONS.include?(key)
216
+ def normalize_defaults!(options)
217
+ options.each_pair do |key, default|
218
+ unless Regexp === default
152
219
  @defaults[key] = default
153
220
  end
154
221
  end
155
-
156
- if options[:constraints].is_a?(Hash)
157
- options[:constraints].each do |key, default|
158
- if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
159
- @defaults[key] ||= default
160
- end
161
- end
162
- end
163
-
164
- if Regexp === options[:format]
165
- @defaults[:format] = nil
166
- elsif String === options[:format]
167
- @defaults[:format] = options[:format]
168
- end
169
222
  end
170
223
 
171
- def normalize_conditions!
172
- @conditions[:path_info] = path
173
-
174
- constraints.each do |key, condition|
175
- unless segment_keys.include?(key) || key == :controller
176
- @conditions[key] = condition
177
- end
178
- end
179
-
180
- required_defaults = []
181
- options.each do |key, required_default|
182
- unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
183
- required_defaults << key
184
- end
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?"
185
227
  end
186
- @conditions[:required_defaults] = required_defaults
228
+ end
187
229
 
188
- via_all = options.delete(:via) if options[:via] == :all
230
+ def add_request_method(via, conditions)
231
+ return if via == [:all]
189
232
 
190
- if !via_all && options[:via].blank?
233
+ if via.empty?
191
234
  msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
192
235
  "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
193
236
  "If you want to expose your action to GET, use `get` in the router:\n" \
194
237
  " Instead of: match \"controller#action\"\n" \
195
238
  " Do: get \"controller#action\""
196
- raise msg
239
+ raise ArgumentError, msg
197
240
  end
198
241
 
199
- if via = options[:via]
200
- @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase }
201
- end
242
+ conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
202
243
  end
203
244
 
204
- def app
205
- Constraints.new(endpoint, blocks, @set.request_class)
206
- end
207
-
208
- def default_controller_and_action
245
+ def app(blocks)
209
246
  if to.respond_to?(:call)
210
- { }
247
+ Constraints.new(to, blocks, false)
248
+ elsif blocks.any?
249
+ Constraints.new(dispatcher(defaults), blocks, true)
211
250
  else
212
- if to.is_a?(String)
213
- controller, action = to.split('#')
214
- elsif to.is_a?(Symbol)
215
- action = to.to_s
216
- end
217
-
218
- controller ||= default_controller
219
- action ||= default_action
220
-
221
- if @scope[:module] && !controller.is_a?(Regexp)
222
- if controller =~ %r{\A/}
223
- controller = controller[1..-1]
224
- else
225
- controller = [@scope[:module], controller].compact.join("/").presence
226
- end
227
- end
228
-
229
- if controller.is_a?(String) && controller =~ %r{\A/}
230
- raise ArgumentError, "controller name should not start with a slash"
231
- end
251
+ dispatcher(defaults)
252
+ end
253
+ end
232
254
 
233
- controller = controller.to_s unless controller.is_a?(Regexp)
234
- 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"
235
260
 
236
- if controller.blank? && segment_keys.exclude?(:controller)
237
- message = "Missing :controller key on routes definition, please check your routes."
238
261
  raise ArgumentError, message
239
- end
262
+ }
263
+ end
240
264
 
241
- if action.blank? && segment_keys.exclude?(:action)
242
- message = "Missing :action key on routes definition, please check your routes."
243
- raise ArgumentError, message
244
- end
265
+ check_part(:action, action, path_params, hash) { |part|
266
+ part.is_a?(Regexp) ? part : part.to_s
267
+ }
268
+ end
245
269
 
246
- if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
247
- message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems."
248
- 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."
249
276
  raise ArgumentError, message
250
277
  end
251
-
252
- hash = {}
253
- hash[:controller] = controller unless controller.blank?
254
- hash[:action] = action unless action.blank?
255
- hash
256
278
  end
257
- end
258
-
259
- def blocks
260
- if options[:constraints].present? && !options[:constraints].is_a?(Hash)
261
- [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]
262
299
  else
263
- scope[:blocks] || []
300
+ []
264
301
  end
265
302
  end
266
303
 
267
- def constraints
268
- @constraints ||= {}.tap do |constraints|
269
- constraints.merge!(scope[:constraints]) if scope[:constraints]
270
-
271
- options.except(*IGNORE_OPTIONS).each do |key, option|
272
- 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("/")
273
310
  end
274
-
275
- constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
311
+ else
312
+ controller
276
313
  end
277
314
  end
278
315
 
279
- def segment_keys
280
- @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
281
- end
282
-
283
- def path_pattern
284
- Journey::Path::Pattern.new(strexp)
285
- 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/
286
319
 
287
- def strexp
288
- Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
320
+ yield
289
321
  end
290
322
 
291
- def endpoint
292
- 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
293
330
  end
294
331
 
295
- def dispatcher
296
- 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
297
344
  end
298
345
 
299
- def to
300
- options[:to]
346
+ def path_params(ast)
347
+ ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
301
348
  end
302
349
 
303
- def default_controller
304
- options[:controller] || scope[:controller]
350
+ def path_ast(path)
351
+ parser = Journey::Parser.new
352
+ parser.parse path
305
353
  end
306
354
 
307
- def default_action
308
- options[:action] || scope[:action]
355
+ def dispatcher(defaults)
356
+ @set.dispatcher defaults
309
357
  end
310
358
  end
311
359
 
@@ -342,19 +390,18 @@ module ActionDispatch
342
390
 
343
391
  # Matches a url pattern to one or more routes.
344
392
  #
345
- # You should not use the `match` method in your router
393
+ # You should not use the +match+ method in your router
346
394
  # without specifying an HTTP method.
347
395
  #
348
396
  # If you want to expose your action to both GET and POST, use:
349
- #
397
+ #
350
398
  # # sets :controller, :action and :id in params
351
399
  # match ':controller/:action/:id', via: [:get, :post]
352
400
  #
353
- # Note that +:controller+, +:action+, +:id+ are interpreted as url query
354
- # parameters and thus available as +params+
355
- # in an action.
401
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as url
402
+ # query parameters and thus available through +params+ in an action.
356
403
  #
357
- # If you want to expose your action to GET, use `get` in the router:
404
+ # If you want to expose your action to GET, use +get+ in the router:
358
405
  #
359
406
  # Instead of:
360
407
  #
@@ -374,24 +421,28 @@ module ActionDispatch
374
421
  # # params[:category] = 'rock/classic'
375
422
  # # params[:title] = 'stairway-to-heaven'
376
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
+ #
377
428
  # When a pattern points to an internal route, the route's +:action+ and
378
429
  # +:controller+ should be set in options or hash shorthand. Examples:
379
430
  #
380
- # match 'photos/:id' => 'photos#show', via: [:get]
381
- # match 'photos/:id', to: 'photos#show', via: [:get]
382
- # match 'photos/:id', controller: 'photos', action: 'show', via: [:get]
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
383
434
  #
384
435
  # A pattern can also point to a +Rack+ endpoint i.e. anything that
385
436
  # responds to +call+:
386
437
  #
387
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: [:get]
388
- # match 'photos/:id', to: PhotoRackApp, via: [:get]
438
+ # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
439
+ # match 'photos/:id', to: PhotoRackApp, via: :get
389
440
  # # Yes, controller actions are just rack endpoints
390
- # match 'photos/:id', to: PhotosController.action(:show), via: [:get]
441
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
391
442
  #
392
443
  # Because requesting various HTTP verbs with a single action has security
393
444
  # implications, you must either specify the actions in
394
- # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers]
445
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
395
446
  # instead +match+
396
447
  #
397
448
  # === Options
@@ -405,7 +456,7 @@ module ActionDispatch
405
456
  # The route's action.
406
457
  #
407
458
  # [:param]
408
- # Overrides the default resource identifier `:id` (name of the
459
+ # Overrides the default resource identifier +:id+ (name of the
409
460
  # dynamic segment used to generate the routes).
410
461
  # You can access that segment from your controller using
411
462
  # <tt>params[<:param>]</tt>.
@@ -416,7 +467,7 @@ module ActionDispatch
416
467
  # [:module]
417
468
  # The namespace for :controller.
418
469
  #
419
- # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: [:get]
470
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
420
471
  # # => Sekret::PostsController
421
472
  #
422
473
  # See <tt>Scoping#namespace</tt> for its scope equivalent.
@@ -435,9 +486,9 @@ module ActionDispatch
435
486
  # Points to a +Rack+ endpoint. Can be an object that responds to
436
487
  # +call+ or a string representing a controller's action.
437
488
  #
438
- # match 'path', to: 'controller#action', via: [:get]
439
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: [:get]
440
- # match 'path', to: RackApp, via: [:get]
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
441
492
  #
442
493
  # [:on]
443
494
  # Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -462,14 +513,14 @@ module ActionDispatch
462
513
  # other than path can also be specified with any object
463
514
  # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
464
515
  #
465
- # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: [:get]
516
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
466
517
  #
467
- # match 'json_only', constraints: { format: 'json' }, via: [:get]
518
+ # match 'json_only', constraints: { format: 'json' }, via: :get
468
519
  #
469
520
  # class Whitelist
470
521
  # def matches?(request) request.remote_ip == '1.2.3.4' end
471
522
  # end
472
- # match 'path', to: 'c#a', constraints: Whitelist.new, via: [:get]
523
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
473
524
  #
474
525
  # See <tt>Scoping#constraints</tt> for more examples with its scope
475
526
  # equivalent.
@@ -478,7 +529,7 @@ module ActionDispatch
478
529
  # Sets defaults for parameters
479
530
  #
480
531
  # # Sets params[:format] to 'jpg' by default
481
- # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: [:get]
532
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
482
533
  #
483
534
  # See <tt>Scoping#defaults</tt> for its scope equivalent.
484
535
  #
@@ -487,7 +538,7 @@ module ActionDispatch
487
538
  # false, the pattern matches any request prefixed with the given path.
488
539
  #
489
540
  # # Matches any request starting with 'path'
490
- # match 'path', to: 'c#a', anchor: false, via: [:get]
541
+ # match 'path', to: 'c#a', anchor: false, via: :get
491
542
  #
492
543
  # [:format]
493
544
  # Allows you to specify the default value for optional +format+
@@ -529,13 +580,15 @@ module ActionDispatch
529
580
 
530
581
  raise "A rack application must be specified" unless path
531
582
 
532
- options[:as] ||= app_name(app)
583
+ rails_app = rails_app? app
584
+ options[:as] ||= app_name(app, rails_app)
585
+
533
586
  target_as = name_for_action(options[:as], path)
534
587
  options[:via] ||= :all
535
588
 
536
589
  match(path, options.merge(:to => app, :anchor => false, :format => false))
537
590
 
538
- define_generate_prefix(app, target_as)
591
+ define_generate_prefix(app, target_as) if rails_app
539
592
  self
540
593
  end
541
594
 
@@ -556,35 +609,37 @@ module ActionDispatch
556
609
  end
557
610
 
558
611
  private
559
- def app_name(app)
560
- return unless app.respond_to?(:routes)
612
+ def rails_app?(app)
613
+ app.is_a?(Class) && app < Rails::Railtie
614
+ end
561
615
 
562
- if app.respond_to?(:railtie_name)
616
+ def app_name(app, rails_app)
617
+ if rails_app
563
618
  app.railtie_name
564
- else
565
- class_name = app.class.is_a?(Class) ? app.name : app.class.name
619
+ elsif app.is_a?(Class)
620
+ class_name = app.name
566
621
  ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
567
622
  end
568
623
  end
569
624
 
570
625
  def define_generate_prefix(app, name)
571
- return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
572
-
573
- _route = @set.named_routes.routes[name.to_sym]
626
+ _route = @set.named_routes.get name
574
627
  _routes = @set
575
628
  app.routes.define_mounted_helper(name)
576
- app.routes.singleton_class.class_eval do
577
- redefine_method :mounted? do
578
- true
579
- end
580
-
581
- redefine_method :_generate_prefix do |options|
582
- prefix_options = options.slice(*_route.segment_keys)
583
- # we must actually delete prefix segment keys to avoid passing them to next url_for
584
- _route.segment_keys.each { |k| options.delete(k) }
585
- _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
586
641
  end
587
- end
642
+ }
588
643
  end
589
644
  end
590
645
 
@@ -671,7 +726,7 @@ module ActionDispatch
671
726
  # resources :posts, module: "admin"
672
727
  #
673
728
  # If you want to route /admin/posts to +PostsController+
674
- # (without the Admin:: module prefix), you could use
729
+ # (without the <tt>Admin::</tt> module prefix), you could use
675
730
  #
676
731
  # scope "/admin" do
677
732
  # resources :posts
@@ -725,7 +780,7 @@ module ActionDispatch
725
780
  # end
726
781
  def scope(*args)
727
782
  options = args.extract_options!.dup
728
- recover = {}
783
+ scope = {}
729
784
 
730
785
  options[:path] = args.flatten.join('/') if args.any?
731
786
  options[:constraints] ||= {}
@@ -736,8 +791,8 @@ module ActionDispatch
736
791
  end
737
792
 
738
793
  if options[:constraints].is_a?(Hash)
739
- defaults = options[:constraints].select do
740
- |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))
741
796
  end
742
797
 
743
798
  (options[:defaults] ||= {}).reverse_merge!(defaults)
@@ -745,7 +800,7 @@ module ActionDispatch
745
800
  block, options[:constraints] = options[:constraints], {}
746
801
  end
747
802
 
748
- SCOPE_OPTIONS.each do |option|
803
+ @scope.options.each do |option|
749
804
  if option == :blocks
750
805
  value = block
751
806
  elsif option == :options
@@ -755,15 +810,15 @@ module ActionDispatch
755
810
  end
756
811
 
757
812
  if value
758
- recover[option] = @scope[option]
759
- @scope[option] = send("merge_#{option}_scope", @scope[option], value)
813
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
760
814
  end
761
815
  end
762
816
 
817
+ @scope = @scope.new scope
763
818
  yield
764
819
  self
765
820
  ensure
766
- @scope.merge!(recover)
821
+ @scope = @scope.parent
767
822
  end
768
823
 
769
824
  # Scopes routes to a specific controller
@@ -1001,8 +1056,6 @@ module ActionDispatch
1001
1056
  VALID_ON_OPTIONS = [:new, :collection, :member]
1002
1057
  RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
1003
1058
  CANONICAL_ACTIONS = %w(index create new show update destroy)
1004
- RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
1005
- RESOURCE_SCOPES = [:resource, :resources]
1006
1059
 
1007
1060
  class Resource #:nodoc:
1008
1061
  attr_reader :controller, :path, :options, :param
@@ -1422,7 +1475,20 @@ module ActionDispatch
1422
1475
  if rest.empty? && Hash === path
1423
1476
  options = path
1424
1477
  path, to = options.find { |name, _value| name.is_a?(String) }
1425
- 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
+
1426
1492
  options.delete(path)
1427
1493
  paths = [path]
1428
1494
  else
@@ -1456,14 +1522,14 @@ module ActionDispatch
1456
1522
  end
1457
1523
 
1458
1524
  def using_match_shorthand?(path, options)
1459
- path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
1525
+ path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$}
1460
1526
  end
1461
1527
 
1462
1528
  def decomposed_match(path, options) # :nodoc:
1463
1529
  if on = options.delete(:on)
1464
1530
  send(on) { decomposed_match(path, options) }
1465
1531
  else
1466
- case @scope[:scope_level]
1532
+ case @scope.scope_level
1467
1533
  when :resources
1468
1534
  nested { decomposed_match(path, options) }
1469
1535
  when :resource
@@ -1476,6 +1542,8 @@ module ActionDispatch
1476
1542
 
1477
1543
  def add_route(action, options) # :nodoc:
1478
1544
  path = path_for_action(action, options.delete(:path))
1545
+ raise ArgumentError, "path is required" if path.blank?
1546
+
1479
1547
  action = action.to_s.dup
1480
1548
 
1481
1549
  if action =~ /^[\w\-\/]+$/
@@ -1484,13 +1552,13 @@ module ActionDispatch
1484
1552
  action = nil
1485
1553
  end
1486
1554
 
1487
- if !options.fetch(:as, true)
1488
- options.delete(:as)
1489
- else
1490
- options[:as] = name_for_action(options[:as], action)
1491
- 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
1492
1560
 
1493
- mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
1561
+ mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
1494
1562
  app, conditions, requirements, defaults, as, anchor = mapping.to_route
1495
1563
  @set.add_route(app, conditions, requirements, defaults, as, anchor)
1496
1564
  end
@@ -1504,7 +1572,7 @@ module ActionDispatch
1504
1572
  raise ArgumentError, "must be called with a path and/or options"
1505
1573
  end
1506
1574
 
1507
- if @scope[:scope_level] == :resources
1575
+ if @scope.resources?
1508
1576
  with_scope_level(:root) do
1509
1577
  scope(parent_resource.path) do
1510
1578
  super(options)
@@ -1571,40 +1639,39 @@ module ActionDispatch
1571
1639
  end
1572
1640
 
1573
1641
  def resource_scope? #:nodoc:
1574
- RESOURCE_SCOPES.include? @scope[:scope_level]
1642
+ @scope.resource_scope?
1575
1643
  end
1576
1644
 
1577
1645
  def resource_method_scope? #:nodoc:
1578
- RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
1646
+ @scope.resource_method_scope?
1579
1647
  end
1580
1648
 
1581
1649
  def nested_scope? #:nodoc:
1582
- @scope[:scope_level] == :nested
1650
+ @scope.nested?
1583
1651
  end
1584
1652
 
1585
1653
  def with_exclusive_scope
1586
1654
  begin
1587
- old_name_prefix, old_path = @scope[:as], @scope[:path]
1588
- @scope[:as], @scope[:path] = nil, nil
1655
+ @scope = @scope.new(:as => nil, :path => nil)
1589
1656
 
1590
1657
  with_scope_level(:exclusive) do
1591
1658
  yield
1592
1659
  end
1593
1660
  ensure
1594
- @scope[:as], @scope[:path] = old_name_prefix, old_path
1661
+ @scope = @scope.parent
1595
1662
  end
1596
1663
  end
1597
1664
 
1598
1665
  def with_scope_level(kind)
1599
- old, @scope[:scope_level] = @scope[:scope_level], kind
1666
+ @scope = @scope.new_level(kind)
1600
1667
  yield
1601
1668
  ensure
1602
- @scope[:scope_level] = old
1669
+ @scope = @scope.parent
1603
1670
  end
1604
1671
 
1605
1672
  def resource_scope(kind, resource) #:nodoc:
1606
1673
  resource.shallow = @scope[:shallow]
1607
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
1674
+ @scope = @scope.new(:scope_level_resource => resource)
1608
1675
  @nesting.push(resource)
1609
1676
 
1610
1677
  with_scope_level(kind) do
@@ -1612,7 +1679,7 @@ module ActionDispatch
1612
1679
  end
1613
1680
  ensure
1614
1681
  @nesting.pop
1615
- @scope[:scope_level_resource] = old_resource
1682
+ @scope = @scope.parent
1616
1683
  end
1617
1684
 
1618
1685
  def nested_options #:nodoc:
@@ -1640,21 +1707,22 @@ module ActionDispatch
1640
1707
  @scope[:constraints][parent_resource.param]
1641
1708
  end
1642
1709
 
1643
- def canonical_action?(action, flag) #:nodoc:
1644
- 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)
1645
1712
  end
1646
1713
 
1647
1714
  def shallow_scope(path, options = {}) #:nodoc:
1648
- old_name_prefix, old_path = @scope[:as], @scope[:path]
1649
- @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path]
1715
+ scope = { :as => @scope[:shallow_prefix],
1716
+ :path => @scope[:shallow_path] }
1717
+ @scope = @scope.new scope
1650
1718
 
1651
1719
  scope(path, options) { yield }
1652
1720
  ensure
1653
- @scope[:as], @scope[:path] = old_name_prefix, old_path
1721
+ @scope = @scope.parent
1654
1722
  end
1655
1723
 
1656
1724
  def path_for_action(action, path) #:nodoc:
1657
- if canonical_action?(action, path.blank?)
1725
+ if path.blank? && canonical_action?(action)
1658
1726
  @scope[:path].to_s
1659
1727
  else
1660
1728
  "#{@scope[:path]}/#{action_path(action, path)}"
@@ -1669,15 +1737,17 @@ module ActionDispatch
1669
1737
  def prefix_name_for_action(as, action) #:nodoc:
1670
1738
  if as
1671
1739
  prefix = as
1672
- elsif !canonical_action?(action, @scope[:scope_level])
1740
+ elsif !canonical_action?(action)
1673
1741
  prefix = action
1674
1742
  end
1675
- prefix.to_s.tr('-', '_') if prefix
1743
+
1744
+ if prefix && prefix != '/' && !prefix.empty?
1745
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
1746
+ end
1676
1747
  end
1677
1748
 
1678
1749
  def name_for_action(as, action) #:nodoc:
1679
1750
  prefix = prefix_name_for_action(as, action)
1680
- prefix = Mapper.normalize_name(prefix) if prefix
1681
1751
  name_prefix = @scope[:as]
1682
1752
 
1683
1753
  if parent_resource
@@ -1687,27 +1757,14 @@ module ActionDispatch
1687
1757
  member_name = parent_resource.member_name
1688
1758
  end
1689
1759
 
1690
- name = case @scope[:scope_level]
1691
- when :nested
1692
- [name_prefix, prefix]
1693
- when :collection
1694
- [prefix, name_prefix, collection_name]
1695
- when :new
1696
- [prefix, :new, name_prefix, member_name]
1697
- when :member
1698
- [prefix, name_prefix, member_name]
1699
- when :root
1700
- [name_prefix, collection_name, prefix]
1701
- else
1702
- [name_prefix, member_name, prefix]
1703
- end
1760
+ name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
1704
1761
 
1705
- if candidate = name.select(&:present?).join("_").presence
1762
+ if candidate = name.compact.join("_").presence
1706
1763
  # If a name was not explicitly given, we check if it is valid
1707
1764
  # and return nil in case it isn't. Otherwise, we pass the invalid name
1708
1765
  # forward so the underlying router engine treats it and raises an exception.
1709
1766
  if as.nil?
1710
- 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)
1711
1768
  else
1712
1769
  candidate
1713
1770
  end
@@ -1832,9 +1889,83 @@ module ActionDispatch
1832
1889
  end
1833
1890
  end
1834
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
+
1835
1966
  def initialize(set) #:nodoc:
1836
1967
  @set = set
1837
- @scope = { :path_names => @set.resources_path_names }
1968
+ @scope = Scope.new({ :path_names => @set.resources_path_names })
1838
1969
  @concerns = {}
1839
1970
  @nesting = []
1840
1971
  end