actionpack 1.12.5 → 1.13.0

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 (179) hide show
  1. data/CHANGELOG +517 -15
  2. data/MIT-LICENSE +1 -1
  3. data/README +18 -20
  4. data/Rakefile +7 -4
  5. data/examples/address_book_controller.rb +3 -3
  6. data/examples/blog_controller.cgi +3 -3
  7. data/examples/debate_controller.cgi +5 -5
  8. data/lib/action_controller.rb +2 -2
  9. data/lib/action_controller/assertions.rb +73 -311
  10. data/lib/action_controller/{deprecated_assertions.rb → assertions/deprecated_assertions.rb} +32 -8
  11. data/lib/action_controller/assertions/dom_assertions.rb +25 -0
  12. data/lib/action_controller/assertions/model_assertions.rb +12 -0
  13. data/lib/action_controller/assertions/response_assertions.rb +140 -0
  14. data/lib/action_controller/assertions/routing_assertions.rb +82 -0
  15. data/lib/action_controller/assertions/selector_assertions.rb +571 -0
  16. data/lib/action_controller/assertions/tag_assertions.rb +117 -0
  17. data/lib/action_controller/base.rb +334 -163
  18. data/lib/action_controller/benchmarking.rb +3 -6
  19. data/lib/action_controller/caching.rb +83 -22
  20. data/lib/action_controller/cgi_ext/cgi_ext.rb +0 -7
  21. data/lib/action_controller/cgi_ext/cgi_methods.rb +167 -173
  22. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +43 -22
  23. data/lib/action_controller/cgi_process.rb +50 -27
  24. data/lib/action_controller/components.rb +21 -25
  25. data/lib/action_controller/cookies.rb +10 -9
  26. data/lib/action_controller/{dependencies.rb → deprecated_dependencies.rb} +9 -27
  27. data/lib/action_controller/filters.rb +448 -225
  28. data/lib/action_controller/flash.rb +24 -20
  29. data/lib/action_controller/helpers.rb +2 -5
  30. data/lib/action_controller/integration.rb +40 -16
  31. data/lib/action_controller/layout.rb +11 -8
  32. data/lib/action_controller/macros/auto_complete.rb +3 -2
  33. data/lib/action_controller/macros/in_place_editing.rb +3 -2
  34. data/lib/action_controller/mime_responds.rb +41 -29
  35. data/lib/action_controller/mime_type.rb +68 -10
  36. data/lib/action_controller/pagination.rb +4 -3
  37. data/lib/action_controller/request.rb +22 -14
  38. data/lib/action_controller/rescue.rb +25 -22
  39. data/lib/action_controller/resources.rb +302 -0
  40. data/lib/action_controller/response.rb +20 -2
  41. data/lib/action_controller/response.rb.rej +17 -0
  42. data/lib/action_controller/routing.rb +1165 -567
  43. data/lib/action_controller/scaffolding.rb +30 -31
  44. data/lib/action_controller/session/active_record_store.rb +2 -0
  45. data/lib/action_controller/session/drb_store.rb +4 -0
  46. data/lib/action_controller/session/mem_cache_store.rb +4 -0
  47. data/lib/action_controller/session_management.rb +6 -9
  48. data/lib/action_controller/status_codes.rb +89 -0
  49. data/lib/action_controller/streaming.rb +6 -15
  50. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +5 -5
  51. data/lib/action_controller/templates/rescues/diagnostics.rhtml +2 -2
  52. data/lib/action_controller/templates/rescues/routing_error.rhtml +4 -4
  53. data/lib/action_controller/templates/rescues/template_error.rhtml +1 -1
  54. data/lib/action_controller/templates/scaffolds/list.rhtml +1 -1
  55. data/lib/action_controller/test_process.rb +52 -30
  56. data/lib/action_controller/url_rewriter.rb +63 -29
  57. data/lib/action_controller/vendor/html-scanner/html/document.rb +1 -0
  58. data/lib/action_controller/vendor/html-scanner/html/node.rb +3 -4
  59. data/lib/action_controller/vendor/html-scanner/html/selector.rb +822 -0
  60. data/lib/action_controller/verification.rb +22 -11
  61. data/lib/action_pack.rb +1 -1
  62. data/lib/action_pack/version.rb +2 -2
  63. data/lib/action_view.rb +1 -1
  64. data/lib/action_view/base.rb +46 -43
  65. data/lib/action_view/compiled_templates.rb +1 -1
  66. data/lib/action_view/helpers/active_record_helper.rb +54 -17
  67. data/lib/action_view/helpers/asset_tag_helper.rb +97 -46
  68. data/lib/action_view/helpers/capture_helper.rb +1 -1
  69. data/lib/action_view/helpers/date_helper.rb +258 -136
  70. data/lib/action_view/helpers/debug_helper.rb +1 -1
  71. data/lib/action_view/helpers/deprecated_helper.rb +34 -0
  72. data/lib/action_view/helpers/form_helper.rb +75 -35
  73. data/lib/action_view/helpers/form_options_helper.rb +7 -5
  74. data/lib/action_view/helpers/form_tag_helper.rb +44 -6
  75. data/lib/action_view/helpers/java_script_macros_helper.rb +59 -46
  76. data/lib/action_view/helpers/javascript_helper.rb +71 -10
  77. data/lib/action_view/helpers/javascripts/controls.js +41 -23
  78. data/lib/action_view/helpers/javascripts/dragdrop.js +105 -76
  79. data/lib/action_view/helpers/javascripts/effects.js +293 -163
  80. data/lib/action_view/helpers/javascripts/prototype.js +897 -389
  81. data/lib/action_view/helpers/javascripts/prototype.js.rej +561 -0
  82. data/lib/action_view/helpers/number_helper.rb +111 -65
  83. data/lib/action_view/helpers/prototype_helper.rb +84 -109
  84. data/lib/action_view/helpers/scriptaculous_helper.rb +5 -0
  85. data/lib/action_view/helpers/tag_helper.rb +69 -16
  86. data/lib/action_view/helpers/text_helper.rb +149 -112
  87. data/lib/action_view/helpers/url_helper.rb +200 -107
  88. data/lib/action_view/template_error.rb +66 -42
  89. data/test/abstract_unit.rb +4 -2
  90. data/test/active_record_unit.rb +84 -56
  91. data/test/activerecord/active_record_assertions_test.rb +26 -18
  92. data/test/activerecord/active_record_store_test.rb +4 -36
  93. data/test/activerecord/pagination_test.rb +1 -6
  94. data/test/controller/action_pack_assertions_test.rb +230 -113
  95. data/test/controller/addresses_render_test.rb +2 -6
  96. data/test/controller/assert_select_test.rb +576 -0
  97. data/test/controller/base_test.rb +73 -3
  98. data/test/controller/caching_test.rb +228 -0
  99. data/test/controller/capture_test.rb +12 -10
  100. data/test/controller/cgi_test.rb +89 -12
  101. data/test/controller/components_test.rb +24 -2
  102. data/test/controller/content_type_test.rb +139 -0
  103. data/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb +0 -0
  104. data/test/controller/controller_fixtures/app/controllers/user_controller.rb +0 -0
  105. data/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb +0 -0
  106. data/test/controller/cookie_test.rb +33 -25
  107. data/test/controller/deprecated_instance_variables_test.rb +48 -0
  108. data/test/controller/deprecation/deprecated_base_methods_test.rb +60 -0
  109. data/test/controller/fake_controllers.rb +0 -1
  110. data/test/controller/filters_test.rb +301 -16
  111. data/test/controller/flash_test.rb +19 -2
  112. data/test/controller/helper_test.rb +2 -2
  113. data/test/controller/integration_test.rb +154 -0
  114. data/test/controller/layout_test.rb +115 -1
  115. data/test/controller/mime_responds_test.rb +94 -0
  116. data/test/controller/mime_type_test.rb +9 -0
  117. data/test/controller/new_render_test.rb +161 -11
  118. data/test/controller/raw_post_test.rb +52 -15
  119. data/test/controller/redirect_test.rb +27 -14
  120. data/test/controller/render_test.rb +76 -29
  121. data/test/controller/request_test.rb +55 -4
  122. data/test/controller/resources_test.rb +274 -0
  123. data/test/controller/routing_test.rb +1533 -824
  124. data/test/controller/selector_test.rb +628 -0
  125. data/test/controller/send_file_test.rb +9 -1
  126. data/test/controller/session_management_test.rb +51 -0
  127. data/test/controller/test_test.rb +113 -29
  128. data/test/controller/url_rewriter_test.rb +86 -17
  129. data/test/controller/verification_test.rb +19 -17
  130. data/test/controller/webservice_test.rb +0 -7
  131. data/test/fixtures/content_type/render_default_content_types_for_respond_to.rhtml +1 -0
  132. data/test/fixtures/content_type/render_default_for_rhtml.rhtml +1 -0
  133. data/test/fixtures/content_type/render_default_for_rjs.rjs +1 -0
  134. data/test/fixtures/content_type/render_default_for_rxml.rxml +1 -0
  135. data/test/fixtures/deprecated_instance_variables/_cookies_ivar.rhtml +1 -0
  136. data/test/fixtures/deprecated_instance_variables/_cookies_method.rhtml +1 -0
  137. data/test/fixtures/deprecated_instance_variables/_flash_ivar.rhtml +1 -0
  138. data/test/fixtures/deprecated_instance_variables/_flash_method.rhtml +1 -0
  139. data/test/fixtures/deprecated_instance_variables/_headers_ivar.rhtml +1 -0
  140. data/test/fixtures/deprecated_instance_variables/_headers_method.rhtml +1 -0
  141. data/test/fixtures/deprecated_instance_variables/_params_ivar.rhtml +1 -0
  142. data/test/fixtures/deprecated_instance_variables/_params_method.rhtml +1 -0
  143. data/test/fixtures/deprecated_instance_variables/_request_ivar.rhtml +1 -0
  144. data/test/fixtures/deprecated_instance_variables/_request_method.rhtml +1 -0
  145. data/test/fixtures/deprecated_instance_variables/_response_ivar.rhtml +1 -0
  146. data/test/fixtures/deprecated_instance_variables/_response_method.rhtml +1 -0
  147. data/test/fixtures/deprecated_instance_variables/_session_ivar.rhtml +1 -0
  148. data/test/fixtures/deprecated_instance_variables/_session_method.rhtml +1 -0
  149. data/test/fixtures/multipart/binary_file +0 -0
  150. data/test/fixtures/public/javascripts/application.js +1 -0
  151. data/test/fixtures/test/_hello.rxml +1 -0
  152. data/test/fixtures/test/hello_world_container.rxml +3 -0
  153. data/test/fixtures/topic.rb +2 -2
  154. data/test/template/active_record_helper_test.rb +83 -12
  155. data/test/template/asset_tag_helper_test.rb +75 -95
  156. data/test/template/compiled_templates_test.rb +1 -0
  157. data/test/template/date_helper_test.rb +873 -181
  158. data/test/template/deprecated_helper_test.rb +36 -0
  159. data/test/template/deprecated_instance_variables_test.rb +43 -0
  160. data/test/template/form_helper_test.rb +77 -1
  161. data/test/template/form_options_helper_test.rb +4 -0
  162. data/test/template/form_tag_helper_test.rb +66 -2
  163. data/test/template/java_script_macros_helper_test.rb +4 -1
  164. data/test/template/javascript_helper_test.rb +29 -0
  165. data/test/template/number_helper_test.rb +63 -27
  166. data/test/template/prototype_helper_test.rb +77 -34
  167. data/test/template/tag_helper_test.rb +34 -6
  168. data/test/template/text_helper_test.rb +69 -34
  169. data/test/template/url_helper_test.rb +168 -16
  170. data/test/testing_sandbox.rb +7 -22
  171. metadata +66 -20
  172. data/filler.txt +0 -50
  173. data/lib/action_controller/code_generation.rb +0 -235
  174. data/lib/action_controller/vendor/xml_simple.rb +0 -1019
  175. data/test/controller/caching_filestore.rb +0 -74
  176. data/test/fixtures/application_root/app/controllers/a_class_that_contains_a_controller/poorly_placed_controller.rb +0 -7
  177. data/test/fixtures/application_root/app/controllers/module_that_holds_controllers/nested_controller.rb +0 -3
  178. data/test/fixtures/application_root/app/models/a_class_that_contains_a_controller.rb +0 -7
  179. data/test/fixtures/dont_load.rb +0 -3
@@ -1,15 +1,33 @@
1
1
  module ActionController
2
2
  class AbstractResponse #:nodoc:
3
3
  DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
4
- attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params
4
+ attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout
5
5
 
6
6
  def initialize
7
7
  @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
8
8
  end
9
9
 
10
+ def content_type=(mime_type)
11
+ @headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type
12
+ end
13
+
14
+ def content_type
15
+ content_type = String(@headers["Content-Type"]).split(";")[0]
16
+ content_type.blank? ? nil : content_type
17
+ end
18
+
19
+ def charset=(encoding)
20
+ @headers["Content-Type"] = "#{content_type || "text/html"}; charset=#{encoding}"
21
+ end
22
+
23
+ def charset
24
+ charset = String(@headers["Content-Type"]).split(";")[1]
25
+ charset.blank? ? nil : charset.strip.split("=")[1]
26
+ end
27
+
10
28
  def redirect(to_url, permanently = false)
11
29
  @headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
12
- @headers["location"] = to_url
30
+ @headers["Location"] = to_url
13
31
 
14
32
  @body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
15
33
  end
@@ -0,0 +1,17 @@
1
+ ***************
2
+ *** 27,33 ****
3
+
4
+ def redirect(to_url, permanently = false)
5
+ @headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
6
+ - @headers["location"] = to_url
7
+
8
+ @body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
9
+ end
10
+ --- 27,33 ----
11
+
12
+ def redirect(to_url, permanently = false)
13
+ @headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
14
+ + @headers["Location"] = to_url
15
+
16
+ @body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
17
+ end
@@ -1,716 +1,1314 @@
1
+ require 'cgi'
2
+
3
+ class Object
4
+ def to_param
5
+ to_s
6
+ end
7
+ end
8
+
9
+ class TrueClass
10
+ def to_param
11
+ self
12
+ end
13
+ end
14
+
15
+ class FalseClass
16
+ def to_param
17
+ self
18
+ end
19
+ end
20
+
21
+ class NilClass
22
+ def to_param
23
+ self
24
+ end
25
+ end
26
+
27
+ class Regexp
28
+ def number_of_captures
29
+ Regexp.new("|#{source}").match('').captures.length
30
+ end
31
+
32
+ class << self
33
+ def optionalize(pattern)
34
+ case unoptionalize(pattern)
35
+ when /\A(.|\(.*\))\Z/ then "#{pattern}?"
36
+ else "(?:#{pattern})?"
37
+ end
38
+ end
39
+
40
+ def unoptionalize(pattern)
41
+ [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp|
42
+ return $1 if regexp =~ pattern
43
+ end
44
+ return pattern
45
+ end
46
+ end
47
+ end
48
+
1
49
  module ActionController
2
- module Routing #:nodoc:
50
+ # == Routing
51
+ #
52
+ # The routing module provides URL rewriting in native Ruby. It's a way to
53
+ # redirect incoming requests to controllers and actions. This replaces
54
+ # mod_rewrite rules. Best of all Rails' Routing works with any web server.
55
+ # Routes are defined in routes.rb in your RAILS_ROOT/config directory.
56
+ #
57
+ # Consider the following route, installed by Rails when you generate your
58
+ # application:
59
+ #
60
+ # map.connect ':controller/:action/:id'
61
+ #
62
+ # This route states that it expects requests to consist of a
63
+ # :controller followed by an :action that in turns is fed by some :id
64
+ #
65
+ # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up
66
+ # with:
67
+ #
68
+ # params = { :controller => 'blog',
69
+ # :action => 'edit'
70
+ # :id => '22'
71
+ # }
72
+ #
73
+ # Think of creating routes as drawing a map for your requests. The map tells
74
+ # them where to go based on some predefined pattern:
75
+ #
76
+ # ActionController::Routing::Routes.draw do |map|
77
+ # Pattern 1 tells some request to go to one place
78
+ # Pattern 2 tell them to go to another
79
+ # ...
80
+ # end
81
+ #
82
+ # The following symbols are special:
83
+ #
84
+ # :controller maps to your controller name
85
+ # :action maps to an action with your controllers
86
+ #
87
+ # Other names simply map to a parameter as in the case of +:id+.
88
+ #
89
+ # == Route priority
90
+ #
91
+ # Not all routes are created equally. Routes have priority defined by the
92
+ # order of appearance of the routes in the routes.rb file. The priority goes
93
+ # from top to bottom. The last route in that file is at the lowest priority
94
+ # will be applied last. If no route matches, 404 is returned.
95
+ #
96
+ # Within blocks, the empty pattern goes first i.e. is at the highest priority.
97
+ # In practice this works out nicely:
98
+ #
99
+ # ActionController::Routing::Routes.draw do |map|
100
+ # map.with_options :controller => 'blog' do |blog|
101
+ # blog.show '', :action => 'list'
102
+ # end
103
+ # map.connect ':controller/:action/:view
104
+ # end
105
+ #
106
+ # In this case, invoking blog controller (with an URL like '/blog/')
107
+ # without parameters will activate the 'list' action by default.
108
+ #
109
+ # == Defaults routes and default parameters
110
+ #
111
+ # Setting a default route is straightforward in Rails because by appending a
112
+ # Hash to the end of your mapping you can set default parameters.
113
+ #
114
+ # Example:
115
+ # ActionController::Routing:Routes.draw do |map|
116
+ # map.connect ':controller/:action/:id', :controller => 'blog'
117
+ # end
118
+ #
119
+ # This sets up +blog+ as the default controller if no other is specified.
120
+ # This means visiting '/' would invoke the blog controller.
121
+ #
122
+ # More formally, you can define defaults in a route with the +:defaults+ key.
123
+ #
124
+ # map.connect ':controller/:id/:action', :action => 'show', :defaults => { :page => 'Dashboard' }
125
+ #
126
+ # == Named routes
127
+ #
128
+ # Routes can be named with the syntax <tt>map.name_of_route options</tt>,
129
+ # allowing for easy reference within your source as +name_of_route_url+.
130
+ #
131
+ # Example:
132
+ # # In routes.rb
133
+ # map.login 'login', :controller => 'accounts', :action => 'login'
134
+ #
135
+ # # With render, redirect_to, tests, etc.
136
+ # redirect_to login_url
137
+ #
138
+ # Arguments can be passed as well.
139
+ #
140
+ # redirect_to show_item_url(:id => 25)
141
+ #
142
+ # When using +with_options+, the name goes after the item passed to the block.
143
+ #
144
+ # ActionController::Routing::Routes.draw do |map|
145
+ # map.with_options :controller => 'blog' do |blog|
146
+ # blog.show '', :action => 'list'
147
+ # blog.delete 'delete/:id', :action => 'delete',
148
+ # blog.edit 'edit/:id', :action => 'edit'
149
+ # end
150
+ # map.connect ':controller/:action/:view
151
+ # end
152
+ #
153
+ # You would then use the named routes in your views:
154
+ #
155
+ # link_to @article.title, show_url(:id => @article.id)
156
+ #
157
+ # == Pretty URL's
158
+ #
159
+ # Routes can generate pretty URLs. For example:
160
+ #
161
+ # map.connect 'articles/:year/:month/:day',
162
+ # :controller => 'articles',
163
+ # :action => 'find_by_date',
164
+ # :year => /\d{4}/,
165
+ # :month => /\d{1,2}/,
166
+ # :day => /\d{1,2}/
167
+ #
168
+ # # Using the route above, the url below maps to:
169
+ # # params = {:year => '2005', :month => '11', :day => '06'}
170
+ # # http://localhost:3000/articles/2005/11/06
171
+ #
172
+ # == Regular Expressions and parameters
173
+ # You can specify a reqular expression to define a format for a parameter.
174
+ #
175
+ # map.geocode 'geocode/:postalcode', :controller => 'geocode',
176
+ # :action => 'show', :postalcode => /\d{5}(-\d{4})?/
177
+ #
178
+ # or more formally:
179
+ #
180
+ # map.geocode 'geocode/:postalcode', :controller => 'geocode',
181
+ # :action => 'show',
182
+ # :requirements { :postalcode => /\d{5}(-\d{4})?/ }
183
+ #
184
+ # == Route globbing
185
+ #
186
+ # Specifying <tt>*[string]</tt> as part of a rule like :
187
+ #
188
+ # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
189
+ #
190
+ # will glob all remaining parts of the route that were not recognized earlier. This idiom must appear at the end of the path. The globbed values are in <tt>params[:path]</tt> in this case.
191
+ #
192
+ # == Reloading routes
193
+ #
194
+ # You can reload routes if you feel you must:
195
+ #
196
+ # Action::Controller::Routes.reload
197
+ #
198
+ # This will clear all named routes and reload routes.rb
199
+ #
200
+ # == Testing Routes
201
+ #
202
+ # The two main methods for testing your routes:
203
+ #
204
+ # === +assert_routing+
205
+ #
206
+ # def test_movie_route_properly_splits
207
+ # opts = {:controller => "plugin", :action => "checkout", :id => "2"}
208
+ # assert_routing "plugin/checkout/2", opts
209
+ # end
210
+ #
211
+ # +assert_routing+ lets you test whether or not the route properly resolves into options.
212
+ #
213
+ # === +assert_recognizes+
214
+ #
215
+ # def test_route_has_options
216
+ # opts = {:controller => "plugin", :action => "show", :id => "12"}
217
+ # assert_recognizes opts, "/plugins/show/12"
218
+ # end
219
+ #
220
+ # Note the subtle difference between the two: +assert_routing+ tests that
221
+ # an URL fits options while +assert_recognizes+ tests that an URL
222
+ # breaks into parameters properly.
223
+ #
224
+ # In tests you can simply pass the URL or named route to +get+ or +post+.
225
+ #
226
+ # def send_to_jail
227
+ # get '/jail'
228
+ # assert_response :success
229
+ # assert_template "jail/front"
230
+ # end
231
+ #
232
+ # def goes_to_login
233
+ # get login_url
234
+ # #...
235
+ # end
236
+ #
237
+ module Routing
238
+ SEPARATORS = %w( / ; . , ? )
239
+
240
+ # The root paths which may contain controller files
241
+ mattr_accessor :controller_paths
242
+ self.controller_paths = []
243
+
3
244
  class << self
4
- def expiry_hash(options, recall)
5
- k = v = nil
6
- expire_on = {}
7
- options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))}
8
- expire_on
245
+ def with_controllers(names)
246
+ prior_controllers = @possible_controllers
247
+ use_controllers! names
248
+ yield
249
+ ensure
250
+ use_controllers! prior_controllers
9
251
  end
10
252
 
11
- def extract_parameter_value(parameter) #:nodoc:
12
- CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s)
13
- end
14
- def controller_relative_to(controller, previous)
15
- if controller.nil? then previous
16
- elsif controller[0] == ?/ then controller[1..-1]
17
- elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
18
- else controller
19
- end
20
- end
253
+ def normalize_paths(paths)
254
+ # do the hokey-pokey of path normalization...
255
+ paths = paths.collect do |path|
256
+ path = path.
257
+ gsub("//", "/"). # replace double / chars with a single
258
+ gsub("\\\\", "\\"). # replace double \ chars with a single
259
+ gsub(%r{(.)[\\/]$}, '\1') # drop final / or \ if path ends with it
21
260
 
22
- def treat_hash(hash, keys_to_delete = [])
23
- k = v = nil
24
- hash.each do |k, v|
25
- if v then hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s
26
- else
27
- hash.delete k
28
- keys_to_delete << k
29
- end
261
+ # eliminate .. paths where possible
262
+ re = %r{\w+[/\\]\.\.[/\\]}
263
+ path.gsub!(%r{\w+[/\\]\.\.[/\\]}, "") while path.match(re)
264
+ path
30
265
  end
31
- hash
266
+
267
+ # start with longest path, first
268
+ paths = paths.uniq.sort_by { |path| - path.length }
32
269
  end
33
-
34
- def test_condition(expression, condition)
35
- case condition
36
- when String then "(#{expression} == #{condition.inspect})"
37
- when Regexp then
38
- condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source
39
- "(#{condition.inspect} =~ #{expression})"
40
- when Array then
41
- conds = condition.collect do |condition|
42
- cond = test_condition(expression, condition)
43
- (cond[0, 1] == '(' && cond[-1, 1] == ')') ? cond : "(#{cond})"
270
+
271
+ def possible_controllers
272
+ unless @possible_controllers
273
+ @possible_controllers = []
274
+
275
+ paths = controller_paths.select { |path| File.directory?(path) && path != "." }
276
+
277
+ seen_paths = Hash.new {|h, k| h[k] = true; false}
278
+ normalize_paths(paths).each do |load_path|
279
+ Dir["#{load_path}/**/*_controller.rb"].collect do |path|
280
+ next if seen_paths[path.gsub(%r{^\.[/\\]}, "")]
281
+
282
+ controller_name = path[(load_path.length + 1)..-1]
283
+
284
+ controller_name.gsub!(/_controller\.rb\Z/, '')
285
+ @possible_controllers << controller_name
44
286
  end
45
- "(#{conds.join(' || ')})"
46
- when true then expression
47
- when nil then "! #{expression}"
48
- else
49
- raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil"
287
+ end
288
+
289
+ # remove duplicates
290
+ @possible_controllers.uniq!
50
291
  end
292
+ @possible_controllers
51
293
  end
52
- end
53
294
 
54
- class Component #:nodoc:
55
- def dynamic?() false end
56
- def optional?() false end
295
+ def use_controllers!(controller_names)
296
+ @possible_controllers = controller_names
297
+ end
57
298
 
58
- def key() nil end
59
-
60
- def self.new(string, *args)
61
- return super(string, *args) unless self == Component
62
- case string
63
- when ':controller' then ControllerComponent.new(:controller, *args)
64
- when /^:(\w+)$/ then DynamicComponent.new($1, *args)
65
- when /^\*(\w+)$/ then PathComponent.new($1, *args)
66
- else StaticComponent.new(string, *args)
67
- end
68
- end
299
+ def controller_relative_to(controller, previous)
300
+ if controller.nil? then previous
301
+ elsif controller[0] == ?/ then controller[1..-1]
302
+ elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
303
+ else controller
304
+ end
305
+ end
69
306
  end
70
-
71
- class StaticComponent < Component #:nodoc:
72
- attr_reader :value
73
307
 
74
- def initialize(value)
75
- @value = value
308
+ class Route
309
+ attr_accessor :segments, :requirements, :conditions
310
+
311
+ def initialize
312
+ @segments = []
313
+ @requirements = {}
314
+ @conditions = {}
76
315
  end
316
+
317
+ # Write and compile a +generate+ method for this Route.
318
+ def write_generation
319
+ # Build the main body of the generation
320
+ body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
321
+
322
+ # If we have conditions that must be tested first, nest the body inside an if
323
+ body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
324
+ args = "options, hash, expire_on = {}"
77
325
 
78
- def write_recognition(g)
79
- g.if_next_matches(value) do |gp|
80
- gp.move_forward {|gpp| gpp.continue}
81
- end
82
- end
326
+ # Nest the body inside of a def block, and then compile it.
327
+ raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
328
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
83
329
 
84
- def write_generation(g)
85
- g.add_segment(value) {|gp| gp.continue }
86
- end
87
- end
330
+ # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
331
+ # are the same as the keys that were recalled from the previous request. Thus,
332
+ # we can use the expire_on.keys to determine which keys ought to be used to build
333
+ # the query string. (Never use keys from the recalled request when building the
334
+ # query string.)
88
335
 
89
- class DynamicComponent < Component #:nodoc:
90
- attr_reader :key, :default
91
- attr_accessor :condition
92
-
93
- def dynamic?() true end
94
- def optional?() @optional end
336
+ method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(hash, expire_on))\nend"
337
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
95
338
 
96
- def default=(default)
97
- @optional = true
98
- @default = default
339
+ method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(hash, expire_on)]\nend"
340
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
341
+ raw_method
99
342
  end
100
-
101
- def initialize(key, options = {})
102
- @key = key.to_sym
103
- @optional = false
104
- default, @condition = options[:default], options[:condition]
105
- self.default = default if options.key?(:default)
343
+
344
+ # Build several lines of code that extract values from the options hash. If any
345
+ # of the values are missing or rejected then a return will be executed.
346
+ def generation_extraction
347
+ segments.collect do |segment|
348
+ segment.extraction_code
349
+ end.compact * "\n"
106
350
  end
107
-
108
- def default_check(g)
109
- presence = "#{g.hash_value(key, !! default)}"
110
- if default
111
- "!(#{presence} && #{g.hash_value(key, false)} != #{default.to_s.inspect})"
112
- else
113
- "! #{presence}"
351
+
352
+ # Produce a condition expression that will check the requirements of this route
353
+ # upon generation.
354
+ def generation_requirements
355
+ requirement_conditions = requirements.collect do |key, req|
356
+ if req.is_a? Regexp
357
+ value_regexp = Regexp.new "\\A#{req.source}\\Z"
358
+ "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
359
+ else
360
+ "hash[:#{key}] == #{req.inspect}"
361
+ end
114
362
  end
363
+ requirement_conditions * ' && ' unless requirement_conditions.empty?
364
+ end
365
+ def generation_structure
366
+ segments.last.string_structure segments[0..-2]
115
367
  end
116
368
 
117
- def write_generation(g)
118
- wrote_dropout = write_dropout_generation(g)
119
- write_continue_generation(g, wrote_dropout)
369
+ # Write and compile a +recognize+ method for this Route.
370
+ def write_recognition
371
+ # Create an if structure to extract the params from a match if it occurs.
372
+ body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
373
+ body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
374
+
375
+ # Build the method declaration and compile it
376
+ method_decl = "def recognize(path, env={})\n#{body}\nend"
377
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
378
+ method_decl
120
379
  end
121
380
 
122
- def write_dropout_generation(g)
123
- return false unless optional? && g.after.all? {|c| c.optional?}
124
-
125
- check = [default_check(g)]
126
- gp = g.dup # Use another generator to write the conditions after the first &&
127
- # We do this to ensure that the generator will not assume x_value is set. It will
128
- # not be set if it follows a false condition -- for example, false && (x = 2)
129
-
130
- check += gp.after.map {|c| c.default_check gp}
131
- gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here
132
- true
381
+ # Plugins may override this method to add other conditions, like checks on
382
+ # host, subdomain, and so forth. Note that changes here only affect route
383
+ # recognition, not generation.
384
+ def recognition_conditions
385
+ result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
386
+ result << "conditions[:method] === env[:method]" if conditions[:method]
387
+ result
133
388
  end
134
389
 
135
- def write_continue_generation(g, use_else)
136
- test = Routing.test_condition(g.hash_value(key, true, default), condition || true)
137
- check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test]
138
-
139
- g.send(*check) do |gp|
140
- gp.expire_for_keys(key) unless gp.after.empty?
141
- add_segments_to(gp) {|gpp| gpp.continue}
390
+ # Build the regular expression pattern that will match this route.
391
+ def recognition_pattern(wrap = true)
392
+ pattern = ''
393
+ segments.reverse_each do |segment|
394
+ pattern = segment.build_pattern pattern
142
395
  end
396
+ wrap ? ("\\A" + pattern + "\\Z") : pattern
143
397
  end
144
-
145
- def add_segments_to(g)
146
- g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp}
398
+
399
+ # Write the code to extract the parameters from a matched route.
400
+ def recognition_extraction
401
+ next_capture = 1
402
+ extraction = segments.collect do |segment|
403
+ x = segment.match_extraction next_capture
404
+ next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
405
+ x
406
+ end
407
+ extraction.compact
147
408
  end
148
409
 
149
- def recognition_check(g)
150
- test_type = [true, nil].include?(condition) ? :presence : :constraint
151
-
152
- prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : ''
153
- check = prefix + Routing.test_condition(g.next_segment(true), condition || true)
154
-
155
- g.if(check) {|gp| yield gp, test_type}
410
+ # Write the real generation implementation and then resend the message.
411
+ def generate(options, hash, expire_on = {})
412
+ write_generation
413
+ generate options, hash, expire_on
156
414
  end
157
-
158
- def write_recognition(g)
159
- test_type = nil
160
- recognition_check(g) do |gp, test_type|
161
- assign_result(gp) {|gpp| gpp.continue}
162
- end
163
-
164
- if optional? && g.after.all? {|c| c.optional?}
165
- call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"]
166
-
167
- g.send(*call) do |gp|
168
- assign_default(gp)
169
- gp.after.each {|c| c.assign_default(gp)}
170
- gp.finish(false)
171
- end
172
- end
415
+
416
+ def generate_extras(options, hash, expire_on = {})
417
+ write_generation
418
+ generate_extras options, hash, expire_on
173
419
  end
174
420
 
175
- def assign_result(g, with_default = false)
176
- g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})"
177
- g.move_forward {|gp| yield gp}
421
+ # Generate the query string with any extra keys in the hash and append
422
+ # it to the given path, returning the new path.
423
+ def append_query_string(path, hash, query_keys=nil)
424
+ return nil unless path
425
+ query_keys ||= extra_keys(hash)
426
+ "#{path}#{build_query_string(hash, query_keys)}"
178
427
  end
179
428
 
180
- def assign_default(g)
181
- g.constant_result key, default unless default.nil?
429
+ # Determine which keys in the given hash are "extra". Extra keys are
430
+ # those that were not used to generate a particular route. The extra
431
+ # keys also do not include those recalled from the prior request, nor
432
+ # do they include any keys that were implied in the route (like a
433
+ # :controller that is required, but not explicitly used in the text of
434
+ # the route.)
435
+ def extra_keys(hash, recall={})
436
+ (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
182
437
  end
183
- end
184
438
 
185
- class ControllerComponent < DynamicComponent #:nodoc:
186
- def key() :controller end
439
+ # Build a query string from the keys of the given hash. If +only_keys+
440
+ # is given (as an array), only the keys indicated will be used to build
441
+ # the query string. The query string will correctly build array parameter
442
+ # values.
443
+ def build_query_string(hash, only_keys=nil)
444
+ elements = []
187
445
 
188
- def add_segments_to(g)
189
- g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp}
446
+ only_keys ||= hash.keys
447
+
448
+ only_keys.each do |key|
449
+ value = hash[key] or next
450
+ key = CGI.escape key.to_s
451
+ if value.class == Array
452
+ key << '[]'
453
+ else
454
+ value = [ value ]
455
+ end
456
+ value.each { |val| elements << "#{key}=#{CGI.escape(val.to_param.to_s)}" }
457
+ end
458
+
459
+ query_string = "?#{elements.join("&")}" unless elements.empty?
460
+ query_string || ""
190
461
  end
191
-
192
- def recognition_check(g)
193
- g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})"
194
- g.if('controller_result') do |gp|
195
- gp << 'controller_value, segments_to_controller = controller_result'
196
- if condition
197
- gp << "controller_path = #{gp.path_name}[#{gp.index_name},segments_to_controller].join('/')"
198
- gp.if(Routing.test_condition("controller_path", condition)) do |gpp|
199
- gpp.move_forward('segments_to_controller') {|gppp| yield gppp, :constraint}
200
- end
201
- else
202
- gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint}
462
+
463
+ # Write the real recognition implementation and then resend the message.
464
+ def recognize(path, environment={})
465
+ write_recognition
466
+ recognize path, environment
467
+ end
468
+
469
+ # A route's parameter shell contains parameter values that are not in the
470
+ # route's path, but should be placed in the recognized hash.
471
+ #
472
+ # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
473
+ #
474
+ # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
475
+ #
476
+ def parameter_shell
477
+ @parameter_shell ||= returning({}) do |shell|
478
+ requirements.each do |key, requirement|
479
+ shell[key] = requirement unless requirement.is_a? Regexp
203
480
  end
204
481
  end
205
482
  end
206
-
207
- def assign_result(g)
208
- g.result key, 'controller_value'
209
- yield g
483
+
484
+ # Return an array containing all the keys that are used in this route. This
485
+ # includes keys that appear inside the path, and keys that have requirements
486
+ # placed upon them.
487
+ def significant_keys
488
+ @significant_keys ||= returning [] do |sk|
489
+ segments.each { |segment| sk << segment.key if segment.respond_to? :key }
490
+ sk.concat requirements.keys
491
+ sk.uniq!
492
+ end
210
493
  end
211
494
 
212
- def assign_default(g)
213
- ControllerComponent.assign_controller(g, default)
495
+ # Return a hash of key/value pairs representing the keys in the route that
496
+ # have defaults, or which are specified by non-regexp requirements.
497
+ def defaults
498
+ @defaults ||= returning({}) do |hash|
499
+ segments.each do |segment|
500
+ next unless segment.respond_to? :default
501
+ hash[segment.key] = segment.default unless segment.default.nil?
502
+ end
503
+ requirements.each do |key,req|
504
+ next if Regexp === req || req.nil?
505
+ hash[key] = req
506
+ end
507
+ end
214
508
  end
215
509
 
216
- class << self
217
- def assign_controller(g, controller)
218
- expr = "::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller"
219
- g.result :controller, expr, true
510
+ def matches_controller_and_action?(controller, action)
511
+ unless @matching_prepared
512
+ @controller_requirement = requirement_for(:controller)
513
+ @action_requirement = requirement_for(:action)
514
+ @matching_prepared = true
220
515
  end
221
516
 
222
- def traverse_to_controller(segments, start_at = 0)
223
- mod = ::Object
224
- length = segments.length
225
- index = start_at
226
- mod_name = controller_name = segment = nil
227
- while index < length
228
- return nil unless /\A[A-Za-z][A-Za-z\d_]*\Z/ =~ (segment = segments[index])
229
- index += 1
230
-
231
- mod_name = segment.camelize
232
- controller_name = "#{mod_name}Controller"
233
- path_suffix = File.join(segments[start_at..(index - 1)])
234
- next_mod = nil
235
-
236
- # If the controller is already present, or if we load it, return it.
237
- if mod.const_defined?(controller_name) || attempt_load(mod, controller_name, path_suffix + "_controller") == :defined
238
- controller = mod.const_get(controller_name)
239
- return nil unless controller.is_a?(Class) && controller.ancestors.include?(ActionController::Base) # it's not really a controller?
240
- return [controller, (index - start_at)]
241
- end
242
-
243
- # No controller? Look for the module
244
- if mod.const_defined? mod_name
245
- next_mod = mod.send(:const_get, mod_name)
246
- next_mod = nil unless next_mod.is_a?(Module)
247
- else
248
- # Try to load a file that defines the module we want.
249
- case attempt_load(mod, mod_name, path_suffix)
250
- when :defined then next_mod = mod.const_get mod_name
251
- when :dir then # We didn't find a file, but there's a dir.
252
- next_mod = Module.new # So create a module for the directory
253
- mod.send :const_set, mod_name, next_mod
254
- else
255
- return nil
256
- end
257
- end
258
- mod = next_mod
259
-
260
- return nil unless mod && mod.is_a?(Module)
261
- end
262
- nil
263
- end
264
-
265
- protected
266
- def safe_load_paths #:nodoc:
267
- if defined?(RAILS_ROOT)
268
- $LOAD_PATH.select do |base|
269
- base = File.expand_path(base)
270
- extended_root = File.expand_path(RAILS_ROOT)
271
- # Exclude all paths that are not nested within app, lib, or components.
272
- base.match(/\A#{Regexp.escape(extended_root)}\/*(app|lib|components)\/[a-z]/) || base =~ %r{rails-[\d.]+/builtin}
273
- end
274
- else
275
- $LOAD_PATH
276
- end
517
+ (@controller_requirement.nil? || @controller_requirement === controller) &&
518
+ (@action_requirement.nil? || @action_requirement === action)
519
+ end
520
+
521
+ def to_s
522
+ @to_s ||= begin
523
+ segs = segments.inject("") { |str,s| str << s.to_s }
524
+ "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
277
525
  end
278
-
279
- def attempt_load(mod, const_name, path)
280
- has_dir = false
281
- safe_load_paths.each do |load_path|
282
- full_path = File.join(load_path, path)
283
- file_path = full_path + '.rb'
284
- if File.file?(file_path) # Found a .rb file? Load it up
285
- require_dependency(file_path)
286
- return :defined if mod.const_defined? const_name
287
- else
288
- has_dir ||= File.directory?(full_path)
289
- end
290
- end
291
- return (has_dir ? :dir : nil)
526
+ end
527
+
528
+ protected
529
+ def requirement_for(key)
530
+ return requirements[key] if requirements.key? key
531
+ segments.each do |segment|
532
+ return segment.regexp if segment.respond_to?(:key) && segment.key == key
292
533
  end
534
+ nil
293
535
  end
536
+
294
537
  end
295
538
 
296
- class PathComponent < DynamicComponent #:nodoc:
297
- def optional?() true end
298
- def default() [] end
299
- def condition() nil end
539
+ class Segment
540
+ attr_accessor :is_optional
541
+ alias_method :optional?, :is_optional
542
+
543
+ def initialize
544
+ self.is_optional = false
545
+ end
300
546
 
301
- def default=(value)
302
- raise RoutingError, "All path components have an implicit default of []" unless value == []
547
+ def extraction_code
548
+ nil
303
549
  end
304
550
 
305
- def write_generation(g)
306
- raise RoutingError, 'Path components must occur last' unless g.after.empty?
307
- g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do
308
- g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)"
309
- g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish }
551
+ # Continue generating string for the prior segments.
552
+ def continue_string_structure(prior_segments)
553
+ if prior_segments.empty?
554
+ interpolation_statement(prior_segments)
555
+ else
556
+ new_priors = prior_segments[0..-2]
557
+ prior_segments.last.string_structure(new_priors)
310
558
  end
311
- g.else { g.finish }
312
559
  end
313
560
 
314
- def write_recognition(g)
315
- raise RoutingError, "Path components must occur last" unless g.after.empty?
316
-
317
- start = g.index_name
318
- start = "(#{start})" unless /^\w+$/ =~ start
319
-
320
- value_expr = "#{g.path_name}[#{start}..-1] || []"
321
- g.result key, "ActionController::Routing::PathComponent::Result.new_escaped(#{value_expr})"
322
- g.finish(false)
561
+ # Return a string interpolation statement for this segment and those before it.
562
+ def interpolation_statement(prior_segments)
563
+ chunks = prior_segments.collect { |s| s.interpolation_chunk }
564
+ chunks << interpolation_chunk
565
+ "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
323
566
  end
324
567
 
325
- class Result < ::Array #:nodoc:
326
- def to_s() join '/' end
327
- def self.new_escaped(strings)
328
- new strings.collect {|str| CGI.unescape str}
329
- end
568
+ def string_structure(prior_segments)
569
+ optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
570
+ end
571
+
572
+ # Return an if condition that is true if all the prior segments can be generated.
573
+ # If there are no optional segments before this one, then nil is returned.
574
+ def all_optionals_available_condition(prior_segments)
575
+ optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
576
+ optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
577
+ end
578
+
579
+ # Recognition
580
+
581
+ def match_extraction(next_capture)
582
+ nil
583
+ end
584
+
585
+ # Warning
586
+
587
+ # Returns true if this segment is optional? because of a default. If so, then
588
+ # no warning will be emitted regarding this segment.
589
+ def optionality_implied?
590
+ false
330
591
  end
331
592
  end
332
593
 
333
- class Route #:nodoc:
334
- attr_accessor :components, :known
335
- attr_reader :path, :options, :keys, :defaults
594
+ class StaticSegment < Segment
595
+ attr_accessor :value, :raw
596
+ alias_method :raw?, :raw
336
597
 
337
- def initialize(path, options = {})
338
- @path, @options = path, options
339
-
340
- initialize_components path
341
- defaults, conditions = initialize_hashes options.dup
342
- @defaults = defaults.dup
343
- configure_components(defaults, conditions)
344
- add_default_requirements
345
- initialize_keys
598
+ def initialize(value = nil)
599
+ super()
600
+ self.value = value
346
601
  end
347
602
 
348
- def inspect
349
- "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>"
603
+ def interpolation_chunk
604
+ raw? ? value : CGI.escape(value)
350
605
  end
351
606
 
352
- def write_generation(generator = CodeGeneration::GenerationGenerator.new)
353
- generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || [])
354
-
355
- if known.empty? then generator.go
607
+ def regexp_chunk
608
+ chunk = Regexp.escape value
609
+ optional? ? Regexp.optionalize(chunk) : chunk
610
+ end
611
+
612
+ def build_pattern(pattern)
613
+ escaped = Regexp.escape(value)
614
+ if optional? && ! pattern.empty?
615
+ "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
616
+ elsif optional?
617
+ Regexp.optionalize escaped
356
618
  else
357
- # Alter the conditions to allow :action => 'index' to also catch :action => nil
358
- altered_known = known.collect do |k, v|
359
- if k == :action && v== 'index' then [k, [nil, 'index']]
360
- else [k, v]
361
- end
362
- end
363
- generator.if(generator.check_conditions(altered_known)) {|gp| gp.go }
619
+ escaped + pattern
364
620
  end
365
-
366
- generator
367
621
  end
368
622
 
369
- def write_recognition(generator = CodeGeneration::RecognitionGenerator.new)
370
- g = generator.dup
371
- g.share_locals_with generator
372
- g.before, g.current, g.after = [], components.first, (components[1..-1] || [])
373
-
374
- known.each do |key, value|
375
- if key == :controller then ControllerComponent.assign_controller(g, value)
376
- else g.constant_result(key, value)
377
- end
378
- end
379
-
380
- g.go
381
-
382
- generator
623
+ def to_s
624
+ value
383
625
  end
626
+ end
384
627
 
385
- def initialize_keys
386
- @keys = (components.collect {|c| c.key} + known.keys).compact
387
- @keys.freeze
628
+ class DividerSegment < StaticSegment
629
+ def initialize(value = nil)
630
+ super(value)
631
+ self.raw = true
632
+ self.is_optional = true
388
633
  end
389
634
 
390
- def extra_keys(options)
391
- options.keys - @keys
635
+ def optionality_implied?
636
+ true
392
637
  end
393
-
394
- def matches_controller?(controller)
395
- if known[:controller] then known[:controller] == controller
396
- else
397
- c = components.find {|c| c.key == :controller}
398
- return false unless c
399
- return c.condition.nil? || eval(Routing.test_condition('controller', c.condition))
400
- end
638
+ end
639
+
640
+ class DynamicSegment < Segment
641
+ attr_accessor :key, :default, :regexp
642
+
643
+ def initialize(key = nil, options = {})
644
+ super()
645
+ self.key = key
646
+ self.default = options[:default] if options.key? :default
647
+ self.is_optional = true if options[:optional] || options.key?(:default)
401
648
  end
402
649
 
403
- protected
404
- def initialize_components(path)
405
- path = path.split('/') if path.is_a? String
406
- path.shift if path.first.blank?
407
- self.components = path.collect {|str| Component.new str}
408
- end
409
-
410
- def initialize_hashes(options)
411
- path_keys = components.collect {|c| c.key }.compact
412
- self.known = {}
413
- defaults = options.delete(:defaults) || {}
414
- conditions = options.delete(:require) || {}
415
- conditions.update(options.delete(:requirements) || {})
416
-
417
- options.each do |k, v|
418
- if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v
419
- else known[k] = v
420
- end
421
- end
422
- [defaults, conditions]
650
+ def to_s
651
+ ":#{key}"
652
+ end
653
+
654
+ # The local variable name that the value of this segment will be extracted to.
655
+ def local_name
656
+ "#{key}_value"
657
+ end
658
+
659
+ def extract_value
660
+ "#{local_name} = hash[:#{key}] #{"|| #{default.inspect}" if default}"
661
+ end
662
+ def value_check
663
+ if default # Then we know it won't be nil
664
+ "#{value_regexp.inspect} =~ #{local_name}" if regexp
665
+ elsif optional?
666
+ # If we have a regexp check that the value is not given, or that it matches.
667
+ # If we have no regexp, return nil since we do not require a condition.
668
+ "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
669
+ else # Then it must be present, and if we have a regexp, it must match too.
670
+ "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
423
671
  end
424
-
425
- def configure_components(defaults, conditions)
426
- components.each do |component|
427
- if defaults.key?(component.key) then component.default = defaults[component.key]
428
- elsif component.key == :action then component.default = 'index'
429
- elsif component.key == :id then component.default = nil
430
- end
431
-
432
- component.condition = conditions[component.key] if conditions.key?(component.key)
433
- end
672
+ end
673
+ def expiry_statement
674
+ "expired, hash = true, options if !expired && expire_on[:#{key}]"
675
+ end
676
+
677
+ def extraction_code
678
+ s = extract_value
679
+ vc = value_check
680
+ s << "\nreturn [nil,nil] unless #{vc}" if vc
681
+ s << "\n#{expiry_statement}"
682
+ end
683
+
684
+ def interpolation_chunk
685
+ "\#{CGI.escape(#{local_name}.to_s)}"
686
+ end
687
+
688
+ def string_structure(prior_segments)
689
+ if optional? # We have a conditional to do...
690
+ # If we should not appear in the url, just write the code for the prior
691
+ # segments. This occurs if our value is the default value, or, if we are
692
+ # optional, if we have nil as our value.
693
+ "if #{local_name} == #{default.inspect}\n" +
694
+ continue_string_structure(prior_segments) +
695
+ "\nelse\n" + # Otherwise, write the code up to here
696
+ "#{interpolation_statement(prior_segments)}\nend"
697
+ else
698
+ interpolation_statement(prior_segments)
434
699
  end
700
+ end
701
+
702
+ def value_regexp
703
+ Regexp.new "\\A#{regexp.source}\\Z" if regexp
704
+ end
705
+ def regexp_chunk
706
+ regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)"
707
+ end
708
+
709
+ def build_pattern(pattern)
710
+ chunk = regexp_chunk
711
+ chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0
712
+ pattern = "#{chunk}#{pattern}"
713
+ optional? ? Regexp.optionalize(pattern) : pattern
714
+ end
715
+ def match_extraction(next_capture)
716
+ hangon = (default ? "|| #{default.inspect}" : "if match[#{next_capture}]")
435
717
 
436
- def add_default_requirements
437
- component_keys = components.collect {|c| c.key}
438
- known[:action] ||= 'index' unless component_keys.include? :action
718
+ # All non code-related keys (such as :id, :slug) have to be unescaped as other CGI params
719
+ "params[:#{key}] = match[#{next_capture}] #{hangon}"
720
+ end
721
+
722
+ def optionality_implied?
723
+ [:action, :id].include? key
724
+ end
725
+
726
+ end
727
+
728
+ class ControllerSegment < DynamicSegment
729
+ def regexp_chunk
730
+ possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
731
+ "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
732
+ end
733
+
734
+ # Don't CGI.escape the controller name, since it may have slashes in it,
735
+ # like admin/foo.
736
+ def interpolation_chunk
737
+ "\#{#{local_name}.to_s}"
738
+ end
739
+
740
+ # Make sure controller names like Admin/Content are correctly normalized to
741
+ # admin/content
742
+ def extract_value
743
+ "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
744
+ end
745
+
746
+ def match_extraction(next_capture)
747
+ if default
748
+ "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
749
+ else
750
+ "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
439
751
  end
752
+ end
440
753
  end
441
754
 
442
- class RouteSet #:nodoc:
443
- attr_reader :routes, :categories, :controller_to_selector
444
- def initialize
445
- @routes = []
446
- @generation_methods = Hash.new(:generate_default_path)
755
+ class PathSegment < DynamicSegment
756
+ EscapedSlash = CGI.escape("/")
757
+ def interpolation_chunk
758
+ "\#{CGI.escape(#{local_name}.to_s).gsub(#{EscapedSlash.inspect}, '/')}"
447
759
  end
448
-
449
- def generate(options, request_or_recall_hash = {})
450
- recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters
451
- use_recall = true
452
-
453
- controller = options[:controller]
454
- options[:action] ||= 'index' if controller
455
- recall_controller = recall[:controller]
456
- if (recall_controller && recall_controller.include?(?/)) || (controller && controller.include?(?/))
457
- recall = {} if controller && controller[0] == ?/
458
- options[:controller] = Routing.controller_relative_to(controller, recall_controller)
459
- end
460
- options = recall.dup if options.empty? # XXX move to url_rewriter?
461
-
462
- keys_to_delete = []
463
- Routing.treat_hash(options, keys_to_delete)
464
-
465
- merged = recall.merge(options)
466
- keys_to_delete.each {|key| merged.delete key}
467
- expire_on = Routing.expiry_hash(options, recall)
468
-
469
- generate_path(merged, options, expire_on)
760
+
761
+ def default
762
+ ''
470
763
  end
471
-
472
- def generate_path(merged, options, expire_on)
473
- send @generation_methods[merged[:controller]], merged, options, expire_on
764
+
765
+ def default=(path)
766
+ raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
474
767
  end
475
- def generate_default_path(*args)
476
- write_generation
477
- generate_default_path(*args)
768
+
769
+ def match_extraction(next_capture)
770
+ "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
478
771
  end
479
-
480
- def write_generation
481
- method_sources = []
482
- @generation_methods = Hash.new(:generate_default_path)
483
- categorize_routes.each do |controller, routes|
484
- next unless routes.length < @routes.length
485
-
486
- ivar = controller.gsub('/', '__')
487
- method_name = "generate_path_for_#{ivar}".to_sym
488
- instance_variable_set "@#{ivar}", routes
489
- code = generation_code_for(ivar, method_name).to_s
490
- method_sources << code
491
-
492
- filename = "generated_code/routing/generation_for_controller_#{controller}.rb"
493
- eval(code, nil, filename)
494
-
495
- @generation_methods[controller.to_s] = method_name
496
- @generation_methods[controller.to_sym] = method_name
497
- end
498
-
499
- code = generation_code_for('routes', 'generate_default_path').to_s
500
- eval(code, nil, 'generated_code/routing/generation.rb')
501
-
502
- return (method_sources << code)
772
+
773
+ def regexp_chunk
774
+ regexp || "(.*)"
503
775
  end
504
776
 
505
- def recognize(request)
506
- string_path = request.path
507
- string_path.chomp! if string_path[0] == ?/
508
- path = string_path.split '/'
509
- path.shift
510
-
511
- hash = recognize_path(path)
512
- return recognition_failed(request) unless hash && hash['controller']
513
-
514
- controller = hash['controller']
515
- hash['controller'] = controller.controller_path
516
- request.path_parameters = hash
517
- controller.new
777
+ class Result < ::Array #:nodoc:
778
+ def to_s() join '/' end
779
+ def self.new_escaped(strings)
780
+ new strings.collect {|str| CGI.unescape str}
781
+ end
782
+ end
783
+ end
784
+
785
+ class RouteBuilder
786
+ attr_accessor :separators, :optional_separators
787
+
788
+ def initialize
789
+ self.separators = Routing::SEPARATORS
790
+ self.optional_separators = %w( / )
518
791
  end
519
- alias :recognize! :recognize
520
792
 
521
- def recognition_failed(request)
522
- raise ActionController::RoutingError, "Recognition failed for #{request.path.inspect}"
793
+ def separator_pattern(inverted = false)
794
+ "[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
523
795
  end
524
-
525
- def write_recognition
526
- g = generator = CodeGeneration::RecognitionGenerator.new
527
- g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"}
796
+
797
+ def interval_regexp
798
+ Regexp.new "(.*?)(#{separators.source}|$)"
799
+ end
800
+
801
+ # Accepts a "route path" (a string defining a route), and returns the array
802
+ # of segments that corresponds to it. Note that the segment array is only
803
+ # partially initialized--the defaults and requirements, for instance, need
804
+ # to be set separately, via the #assign_route_options method, and the
805
+ # #optional? method for each segment will not be reliable until after
806
+ # #assign_route_options is called, as well.
807
+ def segments_for_route_path(path)
808
+ rest, segments = path, []
528
809
 
529
- g.def "self.recognize_path(path)" do
530
- each do |route|
531
- g << 'index = 0'
532
- route.write_recognition(g)
533
- end
810
+ until rest.empty?
811
+ segment, rest = segment_for rest
812
+ segments << segment
534
813
  end
535
-
536
- eval g.to_s, nil, 'generated/routing/recognition.rb'
537
- return g.to_s
814
+ segments
538
815
  end
539
-
540
- def generation_code_for(ivar = 'routes', method_name = nil)
541
- routes = instance_variable_get('@' + ivar)
542
- key_ivar = "@keys_for_#{ivar}"
543
- instance_variable_set(key_ivar, routes.collect {|route| route.keys})
816
+
817
+ # A factory method that returns a new segment instance appropriate for the
818
+ # format of the given string.
819
+ def segment_for(string)
820
+ segment = case string
821
+ when /\A:(\w+)/
822
+ key = $1.to_sym
823
+ case key
824
+ when :controller then ControllerSegment.new(key)
825
+ else DynamicSegment.new key
826
+ end
827
+ when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
828
+ when /\A\?(.*?)\?/
829
+ returning segment = StaticSegment.new($1) do
830
+ segment.is_optional = true
831
+ end
832
+ when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
833
+ when Regexp.new(separator_pattern) then
834
+ returning segment = DividerSegment.new($&) do
835
+ segment.is_optional = (optional_separators.include? $&)
836
+ end
837
+ end
838
+ [segment, $~.post_match]
839
+ end
840
+
841
+ # Split the given hash of options into requirement and default hashes. The
842
+ # segments are passed alongside in order to distinguish between default values
843
+ # and requirements.
844
+ def divide_route_options(segments, options)
845
+ options = options.dup
846
+ requirements = (options.delete(:requirements) || {}).dup
847
+ defaults = (options.delete(:defaults) || {}).dup
848
+ conditions = (options.delete(:conditions) || {}).dup
849
+
850
+ path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
851
+ options.each do |key, value|
852
+ hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
853
+ hash[key] = value
854
+ end
544
855
 
545
- g = generator = CodeGeneration::GenerationGenerator.new
546
- g.def "self.#{method_name}(merged, options, expire_on)" do
547
- g << 'unused_count = options.length + 1'
548
- g << "unused_keys = keys = options.keys"
549
- g << 'path = nil'
856
+ [defaults, requirements, conditions]
857
+ end
550
858
 
551
- routes.each_with_index do |route, index|
552
- g << "new_unused_keys = keys - #{key_ivar}[#{index}]"
553
- g << 'new_path = ('
554
- g.source.indent do
555
- if index.zero?
556
- g << "new_unused_count = new_unused_keys.length"
557
- g << "hash = merged; not_expired = true"
558
- route.write_generation(g.dup)
559
- else
560
- g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp|
561
- gp << "hash = merged; not_expired = true"
562
- route.write_generation(gp)
563
- end
564
- end
565
- end
566
- g.source.lines.last << ' )' # Add the closing brace to the end line
567
- g.if 'new_path' do
568
- g << 'return new_path, [] if new_unused_count.zero?'
569
- g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count'
859
+ # Takes a hash of defaults and a hash of requirements, and assigns them to
860
+ # the segments. Any unused requirements (which do not correspond to a segment)
861
+ # are returned as a hash.
862
+ def assign_route_options(segments, defaults, requirements)
863
+ route_requirements = {} # Requirements that do not belong to a segment
864
+
865
+ segment_named = Proc.new do |key|
866
+ segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
867
+ end
868
+
869
+ requirements.each do |key, requirement|
870
+ segment = segment_named[key]
871
+ if segment
872
+ raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
873
+ if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
874
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
570
875
  end
876
+ segment.regexp = requirement
877
+ else
878
+ route_requirements[key] = requirement
571
879
  end
880
+ end
572
881
 
573
- g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path"
574
- g << "return path, unused_keys"
882
+ defaults.each do |key, default|
883
+ segment = segment_named[key]
884
+ raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
885
+ segment.is_optional = true
886
+ segment.default = default.to_param if default
575
887
  end
576
888
 
577
- return g
889
+ assign_default_route_options(segments)
890
+ ensure_required_segments(segments)
891
+ route_requirements
578
892
  end
579
893
 
580
- def categorize_routes
581
- @categorized_routes = by_controller = Hash.new(self)
582
-
583
- known_controllers.each do |name|
584
- set = by_controller[name] = []
585
- each do |route|
586
- set << route if route.matches_controller? name
894
+ # Assign default options, such as 'index' as a default for :action. This
895
+ # method must be run *after* user supplied requirements and defaults have
896
+ # been applied to the segments.
897
+ def assign_default_route_options(segments)
898
+ segments.each do |segment|
899
+ next unless segment.is_a? DynamicSegment
900
+ case segment.key
901
+ when :action
902
+ if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
903
+ segment.default ||= 'index'
904
+ segment.is_optional = true
905
+ end
906
+ when :id
907
+ if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
908
+ segment.is_optional = true
909
+ end
587
910
  end
588
911
  end
589
-
590
- @categorized_routes
591
912
  end
592
913
 
593
- def known_controllers
594
- @routes.inject([]) do |known, route|
595
- if (controller = route.known[:controller])
596
- if controller.is_a?(Regexp)
597
- known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word}
598
- else known << controller
914
+ # Makes sure that there are no optional segments that precede a required
915
+ # segment. If any are found that precede a required segment, they are
916
+ # made required.
917
+ def ensure_required_segments(segments)
918
+ allow_optional = true
919
+ segments.reverse_each do |segment|
920
+ allow_optional &&= segment.optional?
921
+ if !allow_optional && segment.optional?
922
+ unless segment.optionality_implied?
923
+ warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
599
924
  end
925
+ segment.is_optional = false
926
+ elsif allow_optional & segment.respond_to?(:default) && segment.default
927
+ # if a segment has a default, then it is optional
928
+ segment.is_optional = true
600
929
  end
601
- known
602
- end.uniq
930
+ end
603
931
  end
932
+
933
+ # Construct and return a route with the given path and options.
934
+ def build(path, options)
935
+ # Wrap the path with slashes
936
+ path = "/#{path}" unless path[0] == ?/
937
+ path = "#{path}/" unless path[-1] == ?/
938
+
939
+ segments = segments_for_route_path(path)
940
+ defaults, requirements, conditions = divide_route_options(segments, options)
941
+ requirements = assign_route_options(segments, defaults, requirements)
604
942
 
605
- def reload
606
- NamedRoutes.clear
607
-
608
- if defined?(RAILS_ROOT) then load(File.join(RAILS_ROOT, 'config', 'routes.rb'))
609
- else connect(':controller/:action/:id', :action => 'index', :id => nil)
943
+ route = Route.new
944
+ route.segments = segments
945
+ route.requirements = requirements
946
+ route.conditions = conditions
947
+
948
+ if !route.significant_keys.include?(:action) && !route.requirements[:action]
949
+ route.requirements[:action] = "index"
950
+ route.significant_keys << :action
951
+ end
952
+
953
+ if !route.significant_keys.include?(:controller)
954
+ raise ArgumentError, "Illegal route: the :controller must be specified!"
955
+ end
956
+
957
+ route
958
+ end
959
+ end
960
+
961
+ class RouteSet
962
+ # Mapper instances are used to build routes. The object passed to the draw
963
+ # block in config/routes.rb is a Mapper instance.
964
+ #
965
+ # Mapper instances have relatively few instance methods, in order to avoid
966
+ # clashes with named routes.
967
+ class Mapper
968
+ def initialize(set)
969
+ @set = set
970
+ end
971
+
972
+ # Create an unnamed route with the provided +path+ and +options+. See
973
+ # SomeHelpfulUrl for an introduction to routes.
974
+ def connect(path, options = {})
975
+ @set.add_route(path, options)
976
+ end
977
+
978
+ def named_route(name, path, options = {})
979
+ @set.add_named_route(name, path, options)
980
+ end
981
+
982
+ # Added deprecation notice for anyone who already added a named route called "root".
983
+ # It'll be used as a shortcut for map.connect '' in Rails 2.0.
984
+ def root(*args, &proc)
985
+ super unless args.length >= 1 && proc.nil?
986
+ @set.add_named_route("root", *args)
987
+ end
988
+ deprecate :root => "(as the the label for a named route) will become a shortcut for map.connect '', so find another name"
989
+
990
+ def method_missing(route_name, *args, &proc)
991
+ super unless args.length >= 1 && proc.nil?
992
+ @set.add_named_route(route_name, *args)
993
+ end
994
+ end
995
+
996
+ # A NamedRouteCollection instance is a collection of named routes, and also
997
+ # maintains an anonymous module that can be used to install helpers for the
998
+ # named routes.
999
+ class NamedRouteCollection
1000
+ include Enumerable
1001
+
1002
+ attr_reader :routes, :helpers
1003
+
1004
+ def initialize
1005
+ clear!
1006
+ end
1007
+
1008
+ def clear!
1009
+ @routes = {}
1010
+ @helpers = []
1011
+
1012
+ @module ||= Module.new
1013
+ @module.instance_methods.each do |selector|
1014
+ @module.send :remove_method, selector
1015
+ end
610
1016
  end
611
1017
 
612
- NamedRoutes.install
1018
+ def add(name, route)
1019
+ routes[name.to_sym] = route
1020
+ define_named_route_methods(name, route)
1021
+ end
1022
+
1023
+ def get(name)
1024
+ routes[name.to_sym]
1025
+ end
1026
+
1027
+ alias []= add
1028
+ alias [] get
1029
+ alias clear clear!
1030
+
1031
+ def each
1032
+ routes.each { |name, route| yield name, route }
1033
+ self
1034
+ end
1035
+
1036
+ def names
1037
+ routes.keys
1038
+ end
1039
+
1040
+ def length
1041
+ routes.length
1042
+ end
1043
+
1044
+ def install(destinations = [ActionController::Base, ActionView::Base])
1045
+ Array(destinations).each { |dest| dest.send :include, @module }
1046
+ end
1047
+
1048
+ private
1049
+ def url_helper_name(name, kind = :url)
1050
+ :"#{name}_#{kind}"
1051
+ end
1052
+
1053
+ def hash_access_name(name, kind = :url)
1054
+ :"hash_for_#{name}_#{kind}"
1055
+ end
1056
+
1057
+ def define_named_route_methods(name, route)
1058
+ {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
1059
+ hash = route.defaults.merge(:use_route => name).merge(opts)
1060
+ define_hash_access route, name, kind, hash
1061
+ define_url_helper route, name, kind, hash
1062
+ end
1063
+ end
1064
+
1065
+ def define_hash_access(route, name, kind, options)
1066
+ selector = hash_access_name(name, kind)
1067
+ @module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
1068
+ def #{selector}(options = nil)
1069
+ options ? #{options.inspect}.merge(options) : #{options.inspect}
1070
+ end
1071
+ end_eval
1072
+ @module.send(:protected, selector)
1073
+ helpers << selector
1074
+ end
1075
+
1076
+ def define_url_helper(route, name, kind, options)
1077
+ selector = url_helper_name(name, kind)
1078
+
1079
+ # The segment keys used for positional paramters
1080
+ segment_keys = route.segments.collect do |segment|
1081
+ segment.key if segment.respond_to? :key
1082
+ end.compact
1083
+ hash_access_method = hash_access_name(name, kind)
1084
+
1085
+ @module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
1086
+ def #{selector}(*args)
1087
+ opts = if args.empty? || Hash === args.first
1088
+ args.first || {}
1089
+ else
1090
+ # allow ordered parameters to be associated with corresponding
1091
+ # dynamic segments, so you can do
1092
+ #
1093
+ # foo_url(bar, baz, bang)
1094
+ #
1095
+ # instead of
1096
+ #
1097
+ # foo_url(:bar => bar, :baz => baz, :bang => bang)
1098
+ args.zip(#{segment_keys.inspect}).inject({}) do |h, (v, k)|
1099
+ h[k] = v
1100
+ h
1101
+ end
1102
+ end
1103
+
1104
+ url_for(#{hash_access_method}(opts))
1105
+ end
1106
+ end_eval
1107
+ @module.send(:protected, selector)
1108
+ helpers << selector
1109
+ end
1110
+
1111
+ end
1112
+
1113
+ attr_accessor :routes, :named_routes
1114
+
1115
+ def initialize
1116
+ self.routes = []
1117
+ self.named_routes = NamedRouteCollection.new
613
1118
  end
614
1119
 
615
- def connect(*args)
616
- new_route = Route.new(*args)
617
- @routes << new_route
618
- return new_route
1120
+ # Subclasses and plugins may override this method to specify a different
1121
+ # RouteBuilder instance, so that other route DSL's can be created.
1122
+ def builder
1123
+ @builder ||= RouteBuilder.new
619
1124
  end
620
1125
 
621
1126
  def draw
622
- old_routes = @routes
623
- @routes = []
624
-
625
- begin yield self
626
- rescue
627
- @routes = old_routes
628
- raise
1127
+ clear!
1128
+ yield Mapper.new(self)
1129
+ named_routes.install
1130
+ end
1131
+
1132
+ def clear!
1133
+ routes.clear
1134
+ named_routes.clear
1135
+ @combined_regexp = nil
1136
+ @routes_by_controller = nil
1137
+ end
1138
+
1139
+ def empty?
1140
+ routes.empty?
1141
+ end
1142
+
1143
+ def load!
1144
+ Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
1145
+ clear!
1146
+ load_routes!
1147
+ named_routes.install
1148
+ end
1149
+
1150
+ alias reload load!
1151
+
1152
+ def load_routes!
1153
+ if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
1154
+ load File.join("#{RAILS_ROOT}/config/routes.rb")
1155
+ else
1156
+ add_route ":controller/:action/:id"
629
1157
  end
630
- write_generation
631
- write_recognition
632
1158
  end
633
-
634
- def empty?() @routes.empty? end
635
1159
 
636
- def each(&block) @routes.each(&block) end
637
-
638
- # Defines a new named route with the provided name and arguments.
639
- # This method need only be used when you wish to use a name that a RouteSet instance
640
- # method exists for, such as categories.
641
- #
642
- # For example, map.categories '/categories', :controller => 'categories' will not work
643
- # due to RouteSet#categories.
644
- def named_route(name, path, hash = {})
645
- route = connect(path, hash)
646
- NamedRoutes.name_route(route, name)
1160
+ def add_route(path, options = {})
1161
+ route = builder.build(path, options)
1162
+ routes << route
647
1163
  route
648
1164
  end
649
-
650
- def method_missing(name, *args)
651
- (1..2).include?(args.length) ? named_route(name, *args) : super(name, *args)
1165
+
1166
+ def add_named_route(name, path, options = {})
1167
+ named_routes[name] = add_route(path, options)
652
1168
  end
1169
+
1170
+ def options_as_params(options)
1171
+ # If an explicit :controller was given, always make :action explicit
1172
+ # too, so that action expiry works as expected for things like
1173
+ #
1174
+ # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
1175
+ #
1176
+ # (the above is from the unit tests). In the above case, because the
1177
+ # controller was explicitly given, but no action, the action is implied to
1178
+ # be "index", not the recalled action of "show".
1179
+ #
1180
+ # great fun, eh?
653
1181
 
654
- def extra_keys(options, recall = {})
655
- generate(options.dup, recall).last
1182
+ options_as_params = options[:controller] ? { :action => "index" } : {}
1183
+ options.each do |k, value|
1184
+ options_as_params[k] = value.to_param
1185
+ end
1186
+ options_as_params
656
1187
  end
657
- end
658
-
659
- module NamedRoutes #:nodoc:
660
- Helpers = []
661
- class << self
662
- def clear() Helpers.clear end
663
1188
 
664
- def hash_access_name(name)
665
- "hash_for_#{name}_url"
1189
+ def build_expiry(options, recall)
1190
+ recall.inject({}) do |expiry, (key, recalled_value)|
1191
+ expiry[key] = (options.key?(key) && options[key] != recalled_value)
1192
+ expiry
666
1193
  end
1194
+ end
667
1195
 
668
- def url_helper_name(name)
669
- "#{name}_url"
1196
+ # Generate the path indicated by the arguments, and return an array of
1197
+ # the keys that were not used to generate it.
1198
+ def extra_keys(options, recall={})
1199
+ generate_extras(options, recall).last
1200
+ end
1201
+
1202
+ def generate_extras(options, recall={})
1203
+ generate(options, recall, :generate_extras)
1204
+ end
1205
+
1206
+ def generate(options, recall = {}, method=:generate)
1207
+ named_route_name = options.delete(:use_route)
1208
+ if named_route_name
1209
+ named_route = named_routes[named_route_name]
1210
+ options = named_route.parameter_shell.merge(options)
670
1211
  end
671
-
672
- def known_hash_for_route(route)
673
- hash = route.known.symbolize_keys
674
- route.defaults.each do |key, value|
675
- hash[key.to_sym] ||= value if value
676
- end
677
- hash[:controller] = "/#{hash[:controller]}"
678
-
679
- hash
1212
+
1213
+ options = options_as_params(options)
1214
+ expire_on = build_expiry(options, recall)
1215
+
1216
+ # if the controller has changed, make sure it changes relative to the
1217
+ # current controller module, if any. In other words, if we're currently
1218
+ # on admin/get, and the new controller is 'set', the new controller
1219
+ # should really be admin/set.
1220
+ if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
1221
+ old_parts = recall[:controller].split('/')
1222
+ new_parts = options[:controller].split('/')
1223
+ parts = old_parts[0..-(new_parts.length + 1)] + new_parts
1224
+ options[:controller] = parts.join('/')
680
1225
  end
681
-
682
- def define_hash_access_method(route, name)
683
- hash = known_hash_for_route(route)
684
- define_method(hash_access_name(name)) do |*args|
685
- args.first ? hash.merge(args.first) : hash
1226
+
1227
+ # drop the leading '/' on the controller name
1228
+ options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
1229
+ merged = recall.merge(options)
1230
+
1231
+ if named_route
1232
+ path = named_route.generate(options, merged, expire_on)
1233
+ raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}" if path.nil?
1234
+ return path
1235
+ else
1236
+ merged[:action] ||= 'index'
1237
+ options[:action] ||= 'index'
1238
+
1239
+ controller = merged[:controller]
1240
+ action = merged[:action]
1241
+
1242
+ raise RoutingError, "Need controller and action!" unless controller && action
1243
+ # don't use the recalled keys when determining which routes to check
1244
+ routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
1245
+
1246
+ routes.each do |route|
1247
+ results = route.send(method, options, merged, expire_on)
1248
+ return results if results && (!results.is_a?(Array) || results.first)
686
1249
  end
687
1250
  end
688
-
689
- def name_route(route, name)
690
- define_hash_access_method(route, name)
691
-
692
- module_eval(%{def #{url_helper_name name}(options = {})
693
- url_for(#{hash_access_name(name)}.merge(options))
694
- end}, "generated/routing/named_routes/#{name}.rb")
695
-
696
- protected url_helper_name(name), hash_access_name(name)
697
-
698
- Helpers << url_helper_name(name).to_sym
699
- Helpers << hash_access_name(name).to_sym
700
- Helpers.uniq!
701
- end
702
1251
 
703
- def install(cls = ActionController::Base)
704
- cls.send :include, self
705
- if cls.respond_to? :helper_method
706
- Helpers.each do |helper_name|
707
- cls.send :helper_method, helper_name
1252
+ raise RoutingError, "No route matches #{options.inspect}"
1253
+ end
1254
+
1255
+ def recognize(request)
1256
+ params = recognize_path(request.path, extract_request_environment(request))
1257
+ request.path_parameters = params.with_indifferent_access
1258
+ "#{params[:controller].camelize}Controller".constantize
1259
+ end
1260
+
1261
+ def recognize_path(path, environment={})
1262
+ path = CGI.unescape(path)
1263
+ routes.each do |route|
1264
+ result = route.recognize(path, environment) and return result
1265
+ end
1266
+ raise RoutingError, "no route found to match #{path.inspect} with #{environment.inspect}"
1267
+ end
1268
+
1269
+ def routes_by_controller
1270
+ @routes_by_controller ||= Hash.new do |controller_hash, controller|
1271
+ controller_hash[controller] = Hash.new do |action_hash, action|
1272
+ action_hash[action] = Hash.new do |key_hash, keys|
1273
+ key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
708
1274
  end
709
1275
  end
710
1276
  end
711
1277
  end
1278
+
1279
+ def routes_for(options, merged, expire_on)
1280
+ raise "Need controller and action!" unless controller && action
1281
+ controller = merged[:controller]
1282
+ merged = options if expire_on[:controller]
1283
+ action = merged[:action] || 'index'
1284
+
1285
+ routes_by_controller[controller][action][merged.keys]
1286
+ end
1287
+
1288
+ def routes_for_controller_and_action(controller, action)
1289
+ selected = routes.select do |route|
1290
+ route.matches_controller_and_action? controller, action
1291
+ end
1292
+ (selected.length == routes.length) ? routes : selected
1293
+ end
1294
+
1295
+ def routes_for_controller_and_action_and_keys(controller, action, keys)
1296
+ selected = routes.select do |route|
1297
+ route.matches_controller_and_action? controller, action
1298
+ end
1299
+ selected.sort_by do |route|
1300
+ (keys - route.significant_keys).length
1301
+ end
1302
+ end
1303
+
1304
+ # Subclasses and plugins may override this method to extract further attributes
1305
+ # from the request, for use by route conditions and such.
1306
+ def extract_request_environment(request)
1307
+ { :method => request.method }
1308
+ end
712
1309
  end
713
1310
 
714
1311
  Routes = RouteSet.new
715
1312
  end
716
1313
  end
1314
+