actionpack 1.8.1 → 1.9.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 (101) hide show
  1. data/CHANGELOG +309 -16
  2. data/README +1 -1
  3. data/lib/action_controller.rb +5 -0
  4. data/lib/action_controller/assertions.rb +57 -12
  5. data/lib/action_controller/auto_complete.rb +47 -0
  6. data/lib/action_controller/base.rb +288 -258
  7. data/lib/action_controller/benchmarking.rb +8 -3
  8. data/lib/action_controller/caching.rb +88 -42
  9. data/lib/action_controller/cgi_ext/cgi_ext.rb +1 -1
  10. data/lib/action_controller/cgi_ext/cgi_methods.rb +41 -11
  11. data/lib/action_controller/cgi_ext/multipart_progress.rb +169 -0
  12. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +30 -12
  13. data/lib/action_controller/cgi_process.rb +39 -11
  14. data/lib/action_controller/code_generation.rb +235 -0
  15. data/lib/action_controller/cookies.rb +14 -8
  16. data/lib/action_controller/deprecated_renders_and_redirects.rb +76 -0
  17. data/lib/action_controller/filters.rb +8 -7
  18. data/lib/action_controller/helpers.rb +41 -6
  19. data/lib/action_controller/layout.rb +45 -16
  20. data/lib/action_controller/request.rb +86 -23
  21. data/lib/action_controller/rescue.rb +1 -0
  22. data/lib/action_controller/response.rb +1 -1
  23. data/lib/action_controller/routing.rb +536 -272
  24. data/lib/action_controller/scaffolding.rb +30 -25
  25. data/lib/action_controller/session/active_record_store.rb +251 -50
  26. data/lib/action_controller/streaming.rb +133 -0
  27. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +0 -7
  28. data/lib/action_controller/templates/scaffolds/edit.rhtml +2 -2
  29. data/lib/action_controller/templates/scaffolds/layout.rhtml +22 -18
  30. data/lib/action_controller/templates/scaffolds/list.rhtml +3 -3
  31. data/lib/action_controller/templates/scaffolds/new.rhtml +2 -2
  32. data/lib/action_controller/templates/scaffolds/show.rhtml +1 -1
  33. data/lib/action_controller/test_process.rb +68 -47
  34. data/lib/action_controller/upload_progress.rb +421 -0
  35. data/lib/action_controller/url_rewriter.rb +8 -11
  36. data/lib/action_controller/vendor/html-scanner/html/document.rb +6 -5
  37. data/lib/action_controller/vendor/html-scanner/html/node.rb +70 -14
  38. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +17 -10
  39. data/lib/action_controller/vendor/html-scanner/html/version.rb +3 -3
  40. data/lib/action_controller/vendor/xml_simple.rb +1019 -0
  41. data/lib/action_controller/verification.rb +36 -30
  42. data/lib/action_view/base.rb +21 -14
  43. data/lib/action_view/helpers/active_record_helper.rb +15 -13
  44. data/lib/action_view/helpers/asset_tag_helper.rb +26 -9
  45. data/lib/action_view/helpers/benchmark_helper.rb +24 -0
  46. data/lib/action_view/helpers/capture_helper.rb +7 -5
  47. data/lib/action_view/helpers/date_helper.rb +63 -46
  48. data/lib/action_view/helpers/form_helper.rb +7 -1
  49. data/lib/action_view/helpers/form_options_helper.rb +19 -11
  50. data/lib/action_view/helpers/form_tag_helper.rb +5 -1
  51. data/lib/action_view/helpers/javascript_helper.rb +403 -35
  52. data/lib/action_view/helpers/javascripts/controls.js +261 -0
  53. data/lib/action_view/helpers/javascripts/dragdrop.js +476 -0
  54. data/lib/action_view/helpers/javascripts/effects.js +570 -0
  55. data/lib/action_view/helpers/javascripts/prototype.js +633 -371
  56. data/lib/action_view/helpers/number_helper.rb +11 -13
  57. data/lib/action_view/helpers/tag_helper.rb +1 -2
  58. data/lib/action_view/helpers/text_helper.rb +69 -6
  59. data/lib/action_view/helpers/upload_progress_helper.rb +433 -0
  60. data/lib/action_view/helpers/url_helper.rb +98 -3
  61. data/lib/action_view/partials.rb +14 -8
  62. data/lib/action_view/vendor/builder/xmlmarkup.rb +11 -0
  63. data/rakefile +13 -5
  64. data/test/abstract_unit.rb +1 -1
  65. data/test/controller/action_pack_assertions_test.rb +52 -9
  66. data/test/controller/active_record_assertions_test.rb +119 -120
  67. data/test/controller/active_record_store_test.rb +111 -0
  68. data/test/controller/addresses_render_test.rb +45 -0
  69. data/test/controller/caching_filestore.rb +92 -0
  70. data/test/controller/capture_test.rb +39 -0
  71. data/test/controller/cgi_test.rb +40 -3
  72. data/test/controller/helper_test.rb +65 -13
  73. data/test/controller/multipart_progress_testx.rb +365 -0
  74. data/test/controller/new_render_test.rb +263 -0
  75. data/test/controller/redirect_test.rb +64 -0
  76. data/test/controller/render_test.rb +20 -21
  77. data/test/controller/request_test.rb +83 -3
  78. data/test/controller/routing_test.rb +702 -0
  79. data/test/controller/send_file_test.rb +2 -0
  80. data/test/controller/test_test.rb +44 -8
  81. data/test/controller/upload_progress_testx.rb +89 -0
  82. data/test/controller/verification_test.rb +94 -29
  83. data/test/fixtures/addresses/list.rhtml +1 -0
  84. data/test/fixtures/test/capturing.rhtml +4 -0
  85. data/test/fixtures/test/list.rhtml +1 -1
  86. data/test/fixtures/test/update_element_with_capture.rhtml +9 -0
  87. data/test/template/active_record_helper_test.rb +30 -15
  88. data/test/template/asset_tag_helper_test.rb +12 -5
  89. data/test/template/benchmark_helper_test.rb +72 -0
  90. data/test/template/date_helper_test.rb +69 -0
  91. data/test/template/form_helper_test.rb +18 -10
  92. data/test/template/form_options_helper_test.rb +40 -5
  93. data/test/template/javascript_helper.rb +149 -2
  94. data/test/template/number_helper_test.rb +2 -0
  95. data/test/template/tag_helper_test.rb +4 -0
  96. data/test/template/text_helper_test.rb +36 -0
  97. data/test/template/upload_progress_helper_testx.rb +272 -0
  98. data/test/template/url_helper_test.rb +30 -0
  99. metadata +30 -6
  100. data/test/controller/layout_test.rb +0 -49
  101. data/test/controller/routing_tests.rb +0 -543
@@ -69,7 +69,7 @@ module ActionController #:nodoc:
69
69
  # Or just as a quick test. It works like this:
70
70
  #
71
71
  # class WeblogController < ActionController::Base
72
- # before_filter { |controller| return false if controller.params["stop_action"] }
72
+ # before_filter { |controller| false if controller.params["stop_action"] }
73
73
  # end
74
74
  #
75
75
  # As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables.
@@ -202,10 +202,11 @@ module ActionController #:nodoc:
202
202
  # A#after
203
203
  # B#after
204
204
  def append_around_filter(*filters)
205
+ conditions = extract_conditions!(filters)
205
206
  for filter in filters.flatten
206
207
  ensure_filter_responds_to_before_and_after(filter)
207
- append_before_filter { |c| filter.before(c) }
208
- prepend_after_filter { |c| filter.after(c) }
208
+ append_before_filter(conditions || {}) { |c| filter.before(c) }
209
+ prepend_after_filter(conditions || {}) { |c| filter.after(c) }
209
210
  end
210
211
  end
211
212
 
@@ -338,10 +339,10 @@ module ActionController #:nodoc:
338
339
 
339
340
  def action_exempted?(filter)
340
341
  case
341
- when self.class.included_actions[filter]
342
- !self.class.included_actions[filter].include?(action_name)
343
- when self.class.excluded_actions[filter]
344
- self.class.excluded_actions[filter].include?(action_name)
342
+ when ia = self.class.included_actions[filter]
343
+ !ia.include?(action_name)
344
+ when ea = self.class.excluded_actions[filter]
345
+ ea.include?(action_name)
345
346
  end
346
347
  end
347
348
  end
@@ -2,8 +2,26 @@ module ActionController #:nodoc:
2
2
  module Helpers #:nodoc:
3
3
  def self.append_features(base)
4
4
  super
5
- base.class_eval { class << self; alias_method :inherited_without_helper, :inherited; end }
5
+
6
+ # Initialize the base module to aggregate its helpers.
7
+ base.class_inheritable_accessor :master_helper_module
8
+ base.master_helper_module = Module.new
9
+
10
+ # Extend base with class methods to declare helpers.
6
11
  base.extend(ClassMethods)
12
+
13
+ base.class_eval do
14
+ # Wrap inherited to create a new master helper module for subclasses.
15
+ class << self
16
+ alias_method :inherited_without_helper, :inherited
17
+ alias_method :inherited, :inherited_with_helper
18
+ end
19
+
20
+ # Wrap initialize_template_class to extend new template class
21
+ # instances with the master helper module.
22
+ alias_method :initialize_template_class_without_helper, :initialize_template_class
23
+ alias_method :initialize_template_class, :initialize_template_class_with_helper
24
+ end
7
25
  end
8
26
 
9
27
  # The template helpers serves to relieve the templates from including the same inline code again and again. It's a
@@ -32,7 +50,7 @@ module ActionController #:nodoc:
32
50
  # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
33
51
  # available to the templates.
34
52
  def add_template_helper(helper_module) #:nodoc:
35
- template_class.class_eval "include #{helper_module}"
53
+ master_helper_module.module_eval "include #{helper_module}"
36
54
  end
37
55
 
38
56
  # Declare a helper:
@@ -68,7 +86,7 @@ module ActionController #:nodoc:
68
86
  end
69
87
 
70
88
  # Evaluate block in template class if given.
71
- template_class.module_eval(&block) if block_given?
89
+ master_helper_module.module_eval(&block) if block_given?
72
90
  end
73
91
 
74
92
  # Declare a controller method as a helper. For example,
@@ -76,7 +94,13 @@ module ActionController #:nodoc:
76
94
  # def link_to(name, options) ... end
77
95
  # makes the link_to controller method available in the view.
78
96
  def helper_method(*methods)
79
- template_class.controller_delegate(*methods)
97
+ methods.flatten.each do |method|
98
+ master_helper_module.module_eval <<-end_eval
99
+ def #{method}(*args, &block)
100
+ controller.send(%(#{method}), *args, &block)
101
+ end
102
+ end_eval
103
+ end
80
104
  end
81
105
 
82
106
  # Declare a controller attribute as a helper. For example,
@@ -89,13 +113,24 @@ module ActionController #:nodoc:
89
113
  end
90
114
 
91
115
  private
92
- def inherited(child)
116
+ def inherited_with_helper(child)
93
117
  inherited_without_helper(child)
94
- begin child.helper(child.controller_path)
118
+ begin
119
+ child.master_helper_module = Module.new
120
+ child.master_helper_module.send :include, master_helper_module
121
+ child.helper child.controller_path
95
122
  rescue MissingSourceFile => e
96
123
  raise unless e.is_missing?("helpers/#{child.controller_path}_helper")
97
124
  end
98
125
  end
99
126
  end
127
+
128
+ private
129
+ # Extend the template class instance with our controller's helper module.
130
+ def initialize_template_class_with_helper(response)
131
+ returning(initialize_template_class_without_helper(response)) do
132
+ response.template.extend self.class.master_helper_module
133
+ end
134
+ end
100
135
  end
101
136
  end
@@ -3,8 +3,9 @@ module ActionController #:nodoc:
3
3
  def self.append_features(base)
4
4
  super
5
5
  base.class_eval do
6
- alias_method :render_without_layout, :render
7
- alias_method :render, :render_with_layout
6
+ alias_method :render_with_no_layout, :render
7
+ alias_method :render, :render_with_a_layout
8
+
8
9
  class << self
9
10
  alias_method :inherited_without_layout, :inherited
10
11
  end
@@ -124,7 +125,7 @@ module ActionController #:nodoc:
124
125
  #
125
126
  # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
126
127
  # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
127
- # +:only+ and +:except+ options can be passed to the layout call. For example:
128
+ # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
128
129
  #
129
130
  # class WeblogController < ActionController::Base
130
131
  # layout "weblog_standard", :except => :rss
@@ -136,19 +137,19 @@ module ActionController #:nodoc:
136
137
  # This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
137
138
  # around the rendered view.
138
139
  #
139
- # Both the +:only+ and +:except+ condition can accept an arbitrary number of method references, so +:except => [ :rss, :text_only ]+
140
- # is valid, as is # +:except => :rss+.
140
+ # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
141
+ # #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
141
142
  #
142
143
  # == Using a different layout in the action render call
143
144
  #
144
145
  # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
145
146
  # Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
146
- # This is possible using <tt>render_with_layout</tt> method. It's just a bit more manual work as you'll have to supply fully
147
+ # This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
147
148
  # qualified template and layout names as this example shows:
148
149
  #
149
150
  # class WeblogController < ActionController::Base
150
151
  # def help
151
- # render_with_layout "help/index", "200", "layouts/help"
152
+ # render :action => "help/index", :layout => "help"
152
153
  # end
153
154
  # end
154
155
  #
@@ -201,21 +202,50 @@ module ActionController #:nodoc:
201
202
  active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout
202
203
  end
203
204
 
204
- def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc:
205
- if layout ||= active_layout and action_has_layout?
205
+ def render_with_a_layout(options = {}, deprecated_status = nil, deprecated_layout = nil) #:nodoc:
206
+ options = render_with_a_layout_options(options)
207
+ if (layout = pick_layout(options, deprecated_layout))
208
+ logger.info("Rendering #{options[:template]} within #{layout}") unless logger.nil?
209
+
210
+ @content_for_layout = render_with_no_layout(options.merge(:layout => false))
211
+ erase_render_results
212
+
206
213
  add_variables_to_assigns
207
- logger.info("Rendering #{template_name} within #{layout}") unless logger.nil?
208
- @content_for_layout = @template.render_file(template_name, true)
209
- render_without_layout(layout, status)
214
+ render_with_no_layout(options.merge({ :text => @template.render_file(layout, true), :status => options[:status] || deprecated_status }))
210
215
  else
211
- render_without_layout(template_name, status)
216
+ render_with_no_layout(options, deprecated_status)
212
217
  end
213
218
  end
214
219
 
215
220
  private
216
-
221
+ def render_with_a_layout_options(options)
222
+ return { :template => options } unless options.is_a?(Hash)
223
+ if options.values_at(:text, :file, :inline, :partial, :nothing).compact.empty?
224
+ options
225
+ else
226
+ { :layout => false }.merge(options)
227
+ end
228
+ end
229
+
230
+ def pick_layout(options = {}, deprecated_layout = nil)
231
+ return deprecated_layout if !deprecated_layout.nil?
232
+
233
+ if options.is_a?(Hash)
234
+ case options[:layout]
235
+ when FalseClass
236
+ nil
237
+ when NilClass, TrueClass
238
+ active_layout if action_has_layout?
239
+ else
240
+ active_layout(options[:layout])
241
+ end
242
+ else
243
+ (deprecated_layout || active_layout) if action_has_layout?
244
+ end
245
+ end
246
+
217
247
  def action_has_layout?
218
- conditions = self.class.layout_conditions
248
+ conditions = self.class.layout_conditions || {}
219
249
  case
220
250
  when conditions[:only]
221
251
  conditions[:only].include?(action_name)
@@ -225,6 +255,5 @@ module ActionController #:nodoc:
225
255
  true
226
256
  end
227
257
  end
228
-
229
258
  end
230
259
  end
@@ -1,37 +1,90 @@
1
1
  module ActionController
2
2
  # These methods are available in both the production and test Request objects.
3
3
  class AbstractRequest
4
+ cattr_accessor :relative_url_root
5
+
4
6
  # Returns both GET and POST parameters in a single hash.
5
7
  def parameters
6
- # puts "#{request_parameters.inspect} | #{query_parameters.inspect} | #{path_parameters.inspect}"
7
8
  @parameters ||= request_parameters.merge(query_parameters).merge(path_parameters).with_indifferent_access
8
9
  end
9
10
 
11
+ # Returns the HTTP request method as a lowercase symbol (:get, for example)
10
12
  def method
11
- env['REQUEST_METHOD'].downcase.intern
13
+ env['REQUEST_METHOD'].downcase.to_sym
12
14
  end
13
15
 
16
+ # Is this a GET request? Equivalent to request.method == :get
14
17
  def get?
15
18
  method == :get
16
19
  end
17
20
 
21
+ # Is this a POST request? Equivalent to request.method == :post
18
22
  def post?
19
23
  method == :post
20
24
  end
21
25
 
26
+ # Is this a PUT request? Equivalent to request.method == :put
22
27
  def put?
23
28
  method == :put
24
29
  end
25
30
 
31
+ # Is this a DELETE request? Equivalent to request.method == :delete
26
32
  def delete?
27
33
  method == :delete
28
34
  end
29
35
 
36
+ # Is this a HEAD request? Equivalent to request.method == :head
30
37
  def head?
31
38
  method == :head
32
39
  end
33
-
34
-
40
+
41
+ # Determine whether the body of a POST request is URL-encoded (default),
42
+ # XML, or YAML by checking the Content-Type HTTP header:
43
+ #
44
+ # Content-Type Post Format
45
+ # application/xml :xml
46
+ # text/xml :xml
47
+ # application/x-yaml :yaml
48
+ # text/x-yaml :yaml
49
+ # * :url_encoded
50
+ #
51
+ # For backward compatibility, the post format is extracted from the
52
+ # X-Post-Data-Format HTTP header if present.
53
+ def post_format
54
+ if env['HTTP_X_POST_DATA_FORMAT']
55
+ env['HTTP_X_POST_DATA_FORMAT'].downcase.to_sym
56
+ else
57
+ case env['CONTENT_TYPE'].to_s.downcase
58
+ when 'application/xml', 'text/xml' then :xml
59
+ when 'application/x-yaml', 'text/x-yaml' then :yaml
60
+ else :url_encoded
61
+ end
62
+ end
63
+ end
64
+
65
+ # Is this a POST request formatted as XML or YAML?
66
+ def formatted_post?
67
+ [ :xml, :yaml ].include?(post_format) && post?
68
+ end
69
+
70
+ # Is this a POST request formatted as XML?
71
+ def xml_post?
72
+ post_format == :xml && post?
73
+ end
74
+
75
+ # Is this a POST request formatted as YAML?
76
+ def yaml_post?
77
+ post_format == :yaml && post?
78
+ end
79
+
80
+ # Returns true if the request's "X-Requested-With" header contains
81
+ # "XMLHttpRequest". (The Prototype Javascript library sends this header with
82
+ # every Ajax request.)
83
+ def xml_http_request?
84
+ not /XMLHttpRequest/i.match(env['HTTP_X_REQUESTED_WITH']).nil?
85
+ end
86
+ alias xhr? :xml_http_request?
87
+
35
88
  # Determine originating IP address. REMOTE_ADDR is the standard
36
89
  # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
37
90
  # HTTP_X_FORWARDED_FOR are set by proxies so check for these before
@@ -83,61 +136,71 @@ module ActionController
83
136
  request_uri += '?' + env["QUERY_STRING"] unless env["QUERY_STRING"].nil? || env["QUERY_STRING"].empty?
84
137
  return request_uri
85
138
  end
86
- end
139
+ end
87
140
 
141
+ # Return 'https://' if this is an SSL request and 'http://' otherwise.
88
142
  def protocol
89
143
  env["HTTPS"] == "on" ? 'https://' : 'http://'
90
144
  end
91
145
 
146
+ # Is this an SSL request?
92
147
  def ssl?
93
148
  protocol == 'https://'
94
149
  end
95
150
 
96
- # returns the interpreted path to requested resource after
97
- # all the installation directory of this application was taken into account
151
+ # Returns the interpreted path to requested resource after all the installation directory of this application was taken into account
98
152
  def path
99
- path = request_uri ? request_uri.split('?').first : ''
153
+ path = (uri = request_uri) ? uri.split('?').first : ''
100
154
 
101
- # cut off the part of the url which leads to the installation directory of this app
102
- path[relative_url_root.length..-1]
155
+ # Cut off the path to the installation directory if given
156
+ if root = relative_url_root
157
+ path[root.length..-1]
158
+ else
159
+ path
160
+ end
103
161
  end
104
162
 
105
- # returns the path minus the web server relative
106
- # installation directory
163
+ # Returns the path minus the web server relative installation directory.
164
+ # This method returns nil unless the web server is apache.
107
165
  def relative_url_root
108
- File.dirname(env["SCRIPT_NAME"].to_s).gsub /(^\.$|^\/$)/, ''
166
+ @@relative_url_root ||= File.dirname(env["SCRIPT_NAME"].to_s).gsub(/(^\.$|^\/$)/, '') if server_software == 'apache'
109
167
  end
110
168
 
169
+ # Returns the port number of this request as an integer.
111
170
  def port
112
171
  env['SERVER_PORT'].to_i
113
172
  end
114
173
 
115
- # Returns a string like ":8080" if the port is not 80 or 443 while on https.
174
+ # Returns a port suffix like ":8080" if the port number of this request
175
+ # is not the default HTTP port 80 or HTTPS port 443.
116
176
  def port_string
117
177
  (protocol == 'http://' && port == 80) || (protocol == 'https://' && port == 443) ? '' : ":#{port}"
118
178
  end
119
179
 
180
+ # Returns a host:port string for this request, such as example.com or
181
+ # example.com:8080.
120
182
  def host_with_port
121
183
  env['HTTP_HOST'] || host + port_string
122
184
  end
123
185
 
124
186
  def path_parameters=(parameters)
125
187
  @path_parameters = parameters
126
- @parameters = nil
188
+ @symbolized_path_parameters = @parameters = nil
189
+ end
190
+
191
+ def symbolized_path_parameters
192
+ @symbolized_path_parameters ||= path_parameters.symbolize_keys
127
193
  end
128
194
 
129
195
  def path_parameters
130
196
  @path_parameters ||= {}
131
197
  end
132
-
133
- # Returns true if the request's "X-Requested-With" header contains
134
- # "XMLHttpRequest". (The Prototype Javascript library sends this header with
135
- # every Ajax request.)
136
- def xml_http_request?
137
- env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
198
+
199
+ # Returns the lowercase name of the HTTP server software.
200
+ def server_software
201
+ (env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ env['SERVER_SOFTWARE']) ? $1.downcase : nil
138
202
  end
139
- alias xhr? :xml_http_request?
140
-
203
+
141
204
  #--
142
205
  # Must be implemented in the concrete request
143
206
  #++
@@ -25,6 +25,7 @@ module ActionController #:nodoc:
25
25
  # Exception handler called when the performance of an action raises an exception.
26
26
  def rescue_action(exception)
27
27
  log_error(exception) unless logger.nil?
28
+ erase_render_results if performed?
28
29
 
29
30
  if consider_all_requests_local || local_request?
30
31
  rescue_action_locally(exception)
@@ -8,7 +8,7 @@ module ActionController
8
8
  end
9
9
 
10
10
  def redirect(to_url, permanently = false)
11
- @headers["Status"] = permanently ? "301 Moved Permanently" : "302 Found"
11
+ @headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
12
12
  @headers["location"] = to_url
13
13
 
14
14
  @body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
@@ -1,342 +1,606 @@
1
1
  module ActionController
2
- # See http://manuals.rubyonrails.com/read/chapter/65
3
- module Routing
4
- class Route #:nodoc:
5
- attr_reader :defaults # The defaults hash
6
-
7
- def initialize(path, hash={})
8
- raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
9
- @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
10
- @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
11
- self.items = path
2
+ module Routing #:nodoc:
3
+ 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
9
+ end
10
+
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
21
+
22
+ def treat_hash(hash)
23
+ k = v = nil
12
24
  hash.each do |k, v|
13
- raise TypeError, "Hash keys must be symbols!" unless k.kind_of? Symbol
14
- if v.kind_of? Regexp
15
- raise ArgumentError, "Regexp requirement on #{k}, but #{k} is not in this route's path!" unless @items.include? k
16
- @requirements[k] = v
17
- else
18
- (@items.include?(k) ? @defaults : @requirements)[k] = (v.nil? ? nil : v.to_s)
19
- end
25
+ hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s
20
26
  end
21
-
22
- @defaults.each do |k, v|
23
- raise ArgumentError, "A default has been specified for #{k}, but #{k} is not in the path!" unless @items.include? k
24
- @defaults[k] = v.to_s unless v.kind_of?(String) || v.nil?
27
+ hash
28
+ end
29
+ end
30
+
31
+ class << self
32
+ def test_condition(expression, condition)
33
+ case condition
34
+ when String then "(#{expression} == #{condition.inspect})"
35
+ when Regexp then
36
+ condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source
37
+ "(#{condition.inspect} =~ #{expression})"
38
+ when true then expression
39
+ when nil then "! #{expression}"
40
+ else
41
+ raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil"
25
42
  end
26
- @requirements.each {|k, v| raise ArgumentError, "A Regexp requirement has been specified for #{k}, but #{k} is not in the path!" if v.kind_of?(Regexp) && ! @items.include?(k)}
27
-
28
- # Add in defaults for :action and :id.
29
- [[:action, 'index'], [:id, nil]].each do |name, default|
30
- @defaults[name] = default if @items.include?(name) && ! (@requirements.key?(name) || @defaults.key?(name))
43
+ end
44
+ end
45
+
46
+ class Component #:nodoc:
47
+ def dynamic?() false end
48
+ def optional?() false end
49
+
50
+ def key() nil end
51
+
52
+ def self.new(string, *args)
53
+ return super(string, *args) unless self == Component
54
+ case string
55
+ when ':controller' then ControllerComponent.new(:controller, *args)
56
+ when /^:(\w+)$/ then DynamicComponent.new($1, *args)
57
+ when /^\*(\w+)$/ then PathComponent.new($1, *args)
58
+ else StaticComponent.new(string, *args)
31
59
  end
60
+ end
61
+ end
62
+
63
+ class StaticComponent < Component #:nodoc:
64
+ attr_reader :value
65
+
66
+ def initialize(value)
67
+ @value = value
32
68
  end
33
-
34
- # Generate a URL given the provided options.
35
- # All values in options should be symbols.
36
- # Returns the path and the unused names in a 2 element array.
37
- # If generation fails, [nil, nil] is returned
38
- # Generation can fail because of a missing value, or because an equality check fails.
39
- #
40
- # Generate urls will be as short as possible. If the last component of a url is equal to the default value,
41
- # then that component is removed. This is applied as many times as possible. So, your index controller's
42
- # index action will generate []
43
- def generate(options, defaults={})
44
- non_matching = @requirements.keys.select {|name| ! passes_requirements?(name, options[name] || defaults[name])}
45
- non_matching.collect! {|name| requirements_for(name)}
46
- return nil, "Mismatching option#{'s' if non_matching.length > 1}:\n #{non_matching.join '\n '}" unless non_matching.empty?
47
-
48
- used_names = @requirements.inject({}) {|hash, (k, v)| hash[k] = true; hash} # Mark requirements as used so they don't get put in the query params
49
- components = @items.collect do |item|
50
69
 
51
- if item.kind_of? Symbol
52
- collection = false
70
+ def write_recognition(g)
71
+ g.if_next_matches(value) do |gp|
72
+ gp.move_forward {|gpp| gpp.continue}
73
+ end
74
+ end
53
75
 
54
- if /^\*/ =~ item.to_s
55
- collection = true
56
- item = item.to_s.sub(/^\*/,"").intern
57
- end
76
+ def write_generation(g)
77
+ g.add_segment(value) {|gp| gp.continue }
78
+ end
79
+ end
58
80
 
59
- used_names[item] = true
60
- value = options[item] || defaults[item] || @defaults[item]
61
- return nil, requirements_for(item) unless passes_requirements?(item, value)
81
+ class DynamicComponent < Component #:nodoc:
82
+ attr_reader :key, :default
83
+ attr_accessor :condition
84
+
85
+ def dynamic?() true end
86
+ def optional?() @optional end
62
87
 
63
- defaults = {} unless defaults == {} || value == defaults[item] # Stop using defaults if this component isn't the same as the default.
88
+ def default=(default)
89
+ @optional = true
90
+ @default = default
91
+ end
92
+
93
+ def initialize(key, options = {})
94
+ @key = key.to_sym
95
+ @default, @condition = options[:default], options[:condition]
96
+ @optional = options.key?(:default)
97
+ end
64
98
 
65
- if value.nil? || item == :controller
66
- value
67
- elsif collection
68
- if value.kind_of?(Array)
69
- value = value.collect {|v| Routing.extract_parameter_value(v)}.join('/')
70
- else
71
- value = Routing.extract_parameter_value(value).gsub(/%2F/, "/")
72
- end
73
- value
74
- else
75
- Routing.extract_parameter_value(value)
76
- end
77
- else
78
- item
79
- end
99
+ def default_check(g)
100
+ presence = "#{g.hash_value(key, !! default)}"
101
+ if default
102
+ "!(#{presence} && #{g.hash_value(key, false)} != #{default.inspect})"
103
+ else
104
+ "! #{presence}"
80
105
  end
106
+ end
107
+
108
+ def write_generation(g)
109
+ wrote_dropout = write_dropout_generation(g)
110
+ write_continue_generation(g, wrote_dropout)
111
+ end
112
+
113
+ def write_dropout_generation(g)
114
+ return false unless optional? && g.after.all? {|c| c.optional?}
115
+
116
+ check = [default_check(g)]
117
+ gp = g.dup # Use another generator to write the conditions after the first &&
118
+ # We do this to ensure that the generator will not assume x_value is set. It will
119
+ # not be set if it follows a false condition -- for example, false && (x = 2)
81
120
 
82
- @items.reverse_each do |item| # Remove default components from the end of the generated url.
83
- break unless item.kind_of?(Symbol) && @defaults[item] == components.last
84
- components.pop
121
+ check += gp.after.map {|c| c.default_check gp}
122
+ gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here
123
+ true
124
+ end
125
+
126
+ def write_continue_generation(g, use_else)
127
+ test = Routing.test_condition(g.hash_value(key, true, default), condition || true)
128
+ check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test]
129
+
130
+ g.send(*check) do |gp|
131
+ gp.expire_for_keys(key) unless gp.after.empty?
132
+ add_segments_to(gp) {|gpp| gpp.continue}
85
133
  end
86
-
87
- # If we have any nil components then we can't proceed.
88
- # This might need to be changed. In some cases we may be able to return all componets after nil as extras.
89
- missing = []; components.each_with_index {|c, i| missing << @items[i] if c.nil?}
90
- return nil, "No values provided for component#{'s' if missing.length > 1} #{missing.join ', '} but values are required due to use of later components" unless missing.empty? # how wide is your screen?
91
-
92
- unused = (options.keys - used_names.keys).inject({}) do |unused, key|
93
- unused[key] = options[key] if options[key] != @defaults[key]
94
- unused
134
+ end
135
+
136
+ def add_segments_to(g)
137
+ g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp}
138
+ end
139
+
140
+ def recognition_check(g)
141
+ test_type = [true, nil].include?(condition) ? :presence : :constraint
142
+
143
+ prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : ''
144
+ check = prefix + Routing.test_condition(g.next_segment(true), condition || true)
145
+
146
+ g.if(check) {|gp| yield gp, test_type}
147
+ end
148
+
149
+ def write_recognition(g)
150
+ test_type = nil
151
+ recognition_check(g) do |gp, test_type|
152
+ assign_result(gp) {|gpp| gpp.continue}
95
153
  end
96
-
97
- components.collect! {|c| c.to_s}
98
- return components, unused
154
+
155
+ if optional? && g.after.all? {|c| c.optional?}
156
+ call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"]
157
+
158
+ g.send(*call) do |gp|
159
+ assign_default(gp)
160
+ gp.after.each {|c| c.assign_default(gp)}
161
+ gp.finish(false)
162
+ end
163
+ end
164
+ end
165
+
166
+ def assign_result(g, with_default = false)
167
+ g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})"
168
+ g.move_forward {|gp| yield gp}
169
+ end
170
+
171
+ def assign_default(g)
172
+ g.constant_result key, default unless default.nil?
173
+ end
174
+ end
175
+
176
+ class ControllerComponent < DynamicComponent #:nodoc:
177
+ def key() :controller end
178
+
179
+ def add_segments_to(g)
180
+ g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp}
181
+ end
182
+
183
+ def recognition_check(g)
184
+ g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})"
185
+ g.if('controller_result') do |gp|
186
+ gp << 'controller_value, segments_to_controller = controller_result'
187
+ gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint}
188
+ end
189
+ end
190
+
191
+ def assign_result(g)
192
+ g.result key, 'controller_value'
193
+ yield g
99
194
  end
195
+
196
+ def assign_default(g)
197
+ ControllerComponent.assign_controller(g, default)
198
+ end
199
+
200
+ class << self
201
+ def assign_controller(g, controller)
202
+ expr = "::Controllers::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller"
203
+ g.result :controller, expr, true
204
+ end
205
+
206
+ def traverse_to_controller(segments, start_at = 0)
207
+ mod = ::Controllers
208
+ length = segments.length
209
+ index = start_at
210
+ mod_name = controller_name = segment = nil
100
211
 
101
- # Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized.
102
- # The path should be a list of component strings.
103
- # Options is a hash of the ?k=v pairs
104
- def recognize(components, options={})
105
- options = options.clone
106
- components = components.clone
107
- controller_class = nil
212
+ while index < length
213
+ return nil unless /^[a-z][a-z\d_]*$/ =~ (segment = segments[index])
214
+ index += 1
108
215
 
109
- @items.each do |item|
110
- if item == :controller # Special case for controller
111
- if components.empty? && @defaults[:controller]
112
- controller_class, leftover = eat_path_to_controller(@defaults[:controller].split('/'))
113
- raise RoutingError, "Default controller does not exist: #{@defaults[:controller]}" if controller_class.nil? || leftover.empty? == false
114
- else
115
- controller_class, remaining_components = eat_path_to_controller(components)
116
- return nil, "No controller found at subpath #{components.join('/')}" if controller_class.nil?
117
- components = remaining_components
118
- end
119
- options[:controller] = controller_class.controller_path
120
- return nil, requirements_for(:controller) unless passes_requirements?(:controller, options[:controller])
121
- elsif /^\*/ =~ item.to_s
122
- if components.empty?
123
- value = @defaults.has_key?(item) ? @defaults[item].clone : []
124
- else
125
- value = components.clone
126
- end
127
- value.collect! {|c| CGI.unescape c}
128
- components = []
129
- def value.to_s() self.join('/') end
130
- options[item.to_s.sub(/^\*/,"").intern] = value
131
- elsif item.kind_of? Symbol
132
- value = components.shift || @defaults[item]
133
- return nil, requirements_for(item) unless passes_requirements?(item, value)
134
- options[item] = value.nil? ? value : CGI.unescape(value)
135
- else
136
- return nil, "No value available for component #{item.inspect}" if components.empty?
137
- component = components.shift
138
- return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
216
+ mod_name = segment.camelize
217
+ controller_name = "#{mod_name}Controller"
218
+
219
+ return eval("mod::#{controller_name}", nil, 'routing.rb', __LINE__), (index - start_at) if mod.const_available?(controller_name)
220
+ return nil unless mod.const_available?(mod_name)
221
+ mod = eval("mod::#{mod_name}", nil, 'routing.rb', __LINE__)
139
222
  end
140
223
  end
141
-
142
- if controller_class.nil? && @requirements[:controller] # Load a default controller
143
- controller_class, extras = eat_path_to_controller(@requirements[:controller].split('/'))
144
- raise RoutingError, "Illegal controller path for route default: #{@requirements[:controller]}" unless controller_class && extras.empty?
145
- options[:controller] = controller_class.controller_path
224
+ end
225
+ end
226
+
227
+ class PathComponent < DynamicComponent #:nodoc:
228
+ def optional?() true end
229
+ def default() '' end
230
+ def condition() nil end
231
+
232
+ def write_generation(g)
233
+ raise RoutingError, 'Path components must occur last' unless g.after.empty?
234
+ g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do
235
+ g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)"
236
+ g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish }
146
237
  end
147
- @requirements.each {|k,v| options[k] ||= v unless v.kind_of?(Regexp)}
238
+ g.else { g.finish }
239
+ end
240
+
241
+ def write_recognition(g)
242
+ raise RoutingError, "Path components must occur last" unless g.after.empty?
243
+
244
+ start = g.index_name
245
+ start = "(#{start})" unless /^\w+$/ =~ start
246
+
247
+ value_expr = "#{g.path_name}[#{start}..-1] || []"
248
+ g.result key, "ActionController::Routing::PathComponent::Result.new(#{value_expr})"
249
+ g.finish(false)
250
+ end
251
+
252
+ class Result < ::Array
253
+ def to_s() join '/' end
254
+ end
255
+ end
148
256
 
149
- return nil, "Route recognition didn't find a controller class!" unless controller_class
150
- return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
151
- options.delete_if {|k, v| v.nil?} # Remove nil values.
152
- return controller_class, options
257
+ class Route #:nodoc:
258
+ attr_accessor :components, :known
259
+ attr_reader :path, :options, :keys
260
+
261
+ def initialize(path, options = {})
262
+ @path, @options = path, options
263
+
264
+ initialize_components path
265
+ defaults, conditions = initialize_hashes options.dup
266
+ configure_components(defaults, conditions)
267
+ initialize_keys
153
268
  end
154
-
269
+
155
270
  def inspect
156
- when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
157
- default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
158
- "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>"
271
+ "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>"
159
272
  end
160
-
161
- protected
162
- # Find the controller given a list of path components.
163
- # Return the controller class and the unused path components.
164
- def eat_path_to_controller(path)
165
- path.inject([Controllers, 1]) do |(mod, length), name|
166
- name = name.camelize
167
- return nil, nil unless /^[A-Z][_a-zA-Z\d]*$/ =~ name
168
- controller_name = name + "Controller"
169
- return eval("mod::#{controller_name}"), path[length..-1] if mod.const_available? controller_name
170
- return nil, nil unless mod.const_available? name
171
- [mod.const_get(name), length + 1]
273
+
274
+ def write_generation(generator = CodeGeneration::GenerationGenerator.new)
275
+ generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || [])
276
+
277
+ if known.empty? then generator.go
278
+ else generator.if(generator.check_conditions(known)) {|gp| gp.go }
279
+ end
280
+
281
+ generator
282
+ end
283
+
284
+ def write_recognition(generator = CodeGeneration::RecognitionGenerator.new)
285
+ g = generator.dup
286
+ g.share_locals_with generator
287
+ g.before, g.current, g.after = [], components.first, (components[1..-1] || [])
288
+
289
+ known.each do |key, value|
290
+ if key == :controller then ControllerComponent.assign_controller(g, value)
291
+ else g.constant_result(key, value)
172
292
  end
173
- return nil, nil # Path ended, but no controller found.
174
293
  end
294
+
295
+ g.go
296
+
297
+ generator
298
+ end
299
+
300
+ def initialize_keys
301
+ @keys = (components.collect {|c| c.key} + known.keys).compact
302
+ @keys.freeze
303
+ end
304
+
305
+ def extra_keys(options)
306
+ options.keys - @keys
307
+ end
308
+
309
+ def matches_controller?(controller)
310
+ if known[:controller] then known[:controller] == controller
311
+ else
312
+ c = components.find {|c| c.key == :controller}
313
+ return false unless c
314
+ return c.condition.nil? || eval(Routing.test_condition('controller', c.condition))
315
+ end
316
+ end
317
+
318
+ protected
319
+
320
+ def initialize_components(path)
321
+ path = path.split('/') if path.is_a? String
322
+ path.shift if path.first.blank?
323
+ self.components = path.collect {|str| Component.new str}
324
+ end
325
+
326
+ def initialize_hashes(options)
327
+ path_keys = components.collect {|c| c.key }.compact
328
+ self.known = {}
329
+ defaults = options.delete(:defaults) || {}
330
+ conditions = options.delete(:require) || {}
331
+ conditions.update(options.delete(:requirements) || {})
175
332
 
176
- def items=(path)
177
- items = path.split('/').collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if path.kind_of?(String) # split and convert ':xyz' to symbols
178
- items.shift if items.first == ""
179
- items.pop if items.last == ""
180
- @items = items
181
-
182
- # Verify uniqueness of each component.
183
- @items.inject({}) do |seen, item|
184
- if item.kind_of? Symbol
185
- raise ArgumentError, "Illegal route path -- duplicate item #{item}\n #{path.inspect}" if seen.key? item
186
- seen[item] = true
333
+ options.each do |k, v|
334
+ if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v
335
+ else known[k] = v
187
336
  end
188
- seen
189
337
  end
338
+ [defaults, conditions]
190
339
  end
340
+
341
+ def configure_components(defaults, conditions)
342
+ components.each do |component|
343
+ if defaults.key?(component.key) then component.default = defaults[component.key]
344
+ elsif component.key == :action then component.default = 'index'
345
+ elsif component.key == :id then component.default = nil
346
+ end
191
347
 
192
- # Verify that the given value passes this route's requirements
193
- def passes_requirements?(name, value)
194
- return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
195
-
196
- case @requirements[name]
197
- when nil then true
198
- when Regexp then
199
- value = value.to_s
200
- match = @requirements[name].match(value)
201
- match && match[0].length == value.length
202
- else
203
- @requirements[name] == value.to_s
204
- end
205
- end
206
- def requirements_for(name)
207
- name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
208
- presence = (@defaults.key?(name) && @defaults[name].nil?)
209
- requirement = case @requirements[name]
210
- when nil then nil
211
- when Regexp then "match #{@requirements[name].inspect}"
212
- else "be equal to #{@requirements[name].inspect}"
213
- end
214
- if presence && requirement then "#{name} must be present and #{requirement}"
215
- elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
216
- else "#{name} has no requirements"
348
+ component.condition = conditions[component.key] if conditions.key?(component.key)
217
349
  end
218
350
  end
219
351
  end
220
-
221
- class RouteSet#:nodoc:
352
+
353
+ class RouteSet #:nodoc:
354
+ attr_reader :routes, :categories, :controller_to_selector
222
355
  def initialize
223
356
  @routes = []
357
+ @generation_methods = Hash.new(:generate_default_path)
224
358
  end
225
359
 
226
- def add_route(route)
227
- raise TypeError, "#{route.inspect} is not a Route instance!" unless route.kind_of?(Route)
228
- @routes << route
360
+ def generate(options, request_or_recall_hash = {})
361
+ recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters
362
+ use_recall = true
363
+
364
+ controller = options[:controller]
365
+ options[:action] ||= 'index' if controller
366
+ recall_controller = recall[:controller]
367
+ if (recall_controller && recall_controller.include?(?/)) || (controller && controller.include?(?/))
368
+ recall = {} if controller && controller[0] == ?/
369
+ options[:controller] = Routing.controller_relative_to(controller, recall_controller)
370
+ end
371
+ options = recall.dup if options.empty? # XXX move to url_rewriter?
372
+ Routing.treat_hash(options) # XXX Move inwards (to generated code) or inline?
373
+ merged = recall.merge(options)
374
+ expire_on = Routing.expiry_hash(options, recall)
375
+
376
+ path, keys = generate_path(merged, options, expire_on)
377
+
378
+ # Factor out?
379
+ extras = {}
380
+ k = nil
381
+ keys.each {|k| extras[k] = options[k]}
382
+ [path, extras]
229
383
  end
230
- def empty?
231
- @routes.empty?
384
+
385
+ def generate_path(merged, options, expire_on)
386
+ send @generation_methods[merged[:controller]], merged, options, expire_on
232
387
  end
233
- def each
234
- @routes.each {|route| yield route}
388
+ def generate_default_path(*args)
389
+ write_generation
390
+ generate_default_path(*args)
235
391
  end
392
+
393
+ def write_generation
394
+ method_sources = []
395
+ @generation_methods = Hash.new(:generate_default_path)
396
+ categorize_routes.each do |controller, routes|
397
+ next unless routes.length < @routes.length
398
+
399
+ ivar = controller.gsub('/', '__')
400
+ method_name = "generate_path_for_#{ivar}".to_sym
401
+ instance_variable_set "@#{ivar}", routes
402
+ code = generation_code_for(ivar, method_name).to_s
403
+ method_sources << code
404
+
405
+ filename = "generated_code/routing/generation_for_controller_#{controller}.rb"
406
+ eval(code, nil, filename)
236
407
 
237
- # Generate a path for the provided options
238
- # Returns the path as an array of components and a hash of unused names
239
- # Raises RoutingError if not route can handle the provided components.
240
- #
241
- # Note that we don't return the first generated path. We do this so that when a route
242
- # generates a path from a subset of the available options we can keep looking for a
243
- # route which can generate a path that uses more options.
244
- # Note that we *do* return immediately if
245
- def generate(options, request)
246
- raise RoutingError, "There are no routes defined!" if @routes.empty?
247
-
248
- options = options.symbolize_keys
249
- defaults = request.path_parameters.symbolize_keys
250
- if options.empty? then options = defaults.clone # Get back the current url if no options was passed
251
- else expand_controller_path!(options, defaults) # Expand the supplied controller path.
408
+ @generation_methods[controller.to_s] = method_name
409
+ @generation_methods[controller.to_sym] = method_name
252
410
  end
253
- defaults.delete_if {|k, v| options.key?(k) && options[k].nil?} # Remove defaults that have been manually cleared using :name => nil
254
-
255
- failures = []
256
- selected = nil
257
- self.each do |route|
258
- path, unused = route.generate(options, defaults)
259
- if path.nil?
260
- failures << [route, unused] if ActionController::Base.debug_routes
261
- else
262
- return path, unused if unused.empty? # Found a perfect route -- we're finished.
263
- if selected.nil? || unused.length < selected.last.length
264
- failures << [selected.first, "A better url than #{selected[1]} was found."] if selected
265
- selected = [route, path, unused]
266
- end
411
+
412
+
413
+ code = generation_code_for('routes', 'generate_default_path').to_s
414
+ eval(code, nil, 'generated_code/routing/generation.rb')
415
+
416
+ return (method_sources << code)
417
+ end
418
+
419
+ def recognize(request)
420
+ string_path = request.path
421
+ string_path.chomp! if string_path[0] == ?/
422
+ path = string_path.split '/'
423
+ path.shift
424
+
425
+ hash = recognize_path(path)
426
+ recognition_failed(request) unless hash && hash['controller']
427
+
428
+ controller = hash['controller']
429
+ hash['controller'] = controller.controller_path
430
+ request.path_parameters = hash
431
+ controller.new
432
+ end
433
+ alias :recognize! :recognize
434
+
435
+ def recognition_failed(request)
436
+ raise ActionController::RoutingError, "Recognition failed for #{request.path.inspect}"
437
+ end
438
+
439
+ def write_recognition
440
+ g = generator = CodeGeneration::RecognitionGenerator.new
441
+ g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"}
442
+
443
+ g.def "self.recognize_path(path)" do
444
+ each do |route|
445
+ g << 'index = 0'
446
+ route.write_recognition(g)
267
447
  end
268
448
  end
269
-
270
- return selected[1..-1] unless selected.nil?
271
- raise RoutingError.new("Generation failure: No route for url_options #{options.inspect}, defaults: #{defaults.inspect}", failures)
449
+
450
+ eval g.to_s, nil, 'generated/routing/recognition.rb'
451
+ return g.to_s
272
452
  end
273
-
274
- # Recognize the provided path.
275
- # Raise RoutingError if the path can't be recognized.
276
- def recognize!(request)
277
- path = ((%r{^/?(.*)/?$} =~ request.path) ? $1 : request.path).split('/')
278
- raise RoutingError, "There are no routes defined!" if @routes.empty?
279
453
 
280
- failures = []
281
- self.each do |route|
282
- controller, options = route.recognize(path)
283
- if controller.nil?
284
- failures << [route, options] if ActionController::Base.debug_routes
285
- else
286
- request.path_parameters = options
287
- return controller
454
+ def generation_code_for(ivar = 'routes', method_name = nil)
455
+ routes = instance_variable_get('@' + ivar)
456
+ key_ivar = "@keys_for_#{ivar}"
457
+ instance_variable_set(key_ivar, routes.collect {|route| route.keys})
458
+
459
+ g = generator = CodeGeneration::GenerationGenerator.new
460
+ g.def "self.#{method_name}(merged, options, expire_on)" do
461
+ g << 'unused_count = options.length + 1'
462
+ g << "unused_keys = keys = options.keys"
463
+ g << 'path = nil'
464
+
465
+ routes.each_with_index do |route, index|
466
+ g << "new_unused_keys = keys - #{key_ivar}[#{index}]"
467
+ g << 'new_path = ('
468
+ g.source.indent do
469
+ if index.zero?
470
+ g << "new_unused_count = new_unused_keys.length"
471
+ g << "hash = merged; not_expired = true"
472
+ route.write_generation(g.dup)
473
+ else
474
+ g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp|
475
+ gp << "hash = merged; not_expired = true"
476
+ route.write_generation(gp)
477
+ end
478
+ end
479
+ end
480
+ g.source.lines.last << ' )' # Add the closing brace to the end line
481
+ g.if 'new_path' do
482
+ g << 'return new_path, [] if new_unused_count.zero?'
483
+ g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count'
484
+ end
288
485
  end
486
+
487
+ g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path"
488
+ g << "return path, unused_keys"
289
489
  end
290
490
 
291
- raise RoutingError.new("No route for path: #{path.join('/').inspect}", failures)
491
+ return g
292
492
  end
293
493
 
294
- def expand_controller_path!(options, defaults)
295
- if options[:controller]
296
- if /^\// =~ options[:controller]
297
- options[:controller] = options[:controller][1..-1]
298
- defaults.clear # Sending to absolute controller implies fresh defaults
299
- else
300
- relative_to = defaults[:controller] ? defaults[:controller].split('/')[0..-2].join('/') : ''
301
- options[:controller] = relative_to.empty? ? options[:controller] : "#{relative_to}/#{options[:controller]}"
302
- defaults.delete(:action) if options.key?(:controller)
494
+ def categorize_routes
495
+ @categorized_routes = by_controller = Hash.new(self)
496
+
497
+ known_controllers.each do |name|
498
+ set = by_controller[name] = []
499
+ each do |route|
500
+ set << route if route.matches_controller? name
303
501
  end
304
- else
305
- options[:controller] = defaults[:controller]
306
502
  end
503
+
504
+ @categorized_routes
307
505
  end
308
506
 
309
- def route(*args)
310
- add_route(Route.new(*args))
507
+ def known_controllers
508
+ @routes.inject([]) do |known, route|
509
+ if (controller = route.known[:controller])
510
+ if controller.is_a?(Regexp)
511
+ known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word}
512
+ else known << controller
513
+ end
514
+ end
515
+ known
516
+ end.uniq
311
517
  end
312
- alias :connect :route
313
-
518
+
314
519
  def reload
315
- begin
316
- route_file = defined?(RAILS_ROOT) ? File.join(RAILS_ROOT, 'config', 'routes') : nil
317
- require_dependency(route_file) if route_file
318
- rescue LoadError, ScriptError => e
319
- raise RoutingError.new("Cannot load config/routes.rb:\n #{e.message}").copy_blame!(e)
320
- ensure # Ensure that there is at least one route:
321
- connect(':controller/:action/:id', :action => 'index', :id => nil) if @routes.empty?
520
+ NamedRoutes.clear
521
+
522
+ if defined?(RAILS_ROOT) then load(File.join(RAILS_ROOT, 'config', 'routes.rb'))
523
+ else connect(':controller/:action/:id', :action => 'index', :id => nil)
322
524
  end
525
+
526
+ NamedRoutes.install
323
527
  end
324
-
528
+
529
+ def connect(*args)
530
+ new_route = Route.new(*args)
531
+ @routes << new_route
532
+ return new_route
533
+ end
534
+
325
535
  def draw
326
- @routes.clear
327
- yield self
536
+ old_routes = @routes
537
+ @routes = []
538
+
539
+ begin yield self
540
+ rescue
541
+ @routes = old_routes
542
+ raise
543
+ end
544
+ write_generation
545
+ write_recognition
546
+ end
547
+
548
+ def empty?() @routes.empty? end
549
+
550
+ def each(&block) @routes.each(&block) end
551
+
552
+ def method_missing(name, *args)
553
+ return super(name, *args) unless (1..2).include?(args.length)
554
+
555
+ route = connect(*args)
556
+ NamedRoutes.name_route(route, name)
557
+ route
558
+ end
559
+
560
+ def extra_keys(options, recall = {})
561
+ generate(options.dup, recall).last.keys
328
562
  end
329
563
  end
564
+
565
+ module NamedRoutes #:nodoc:
566
+ Helpers = []
567
+ class << self
568
+ def clear() Helpers.clear end
569
+
570
+ def hash_access_name(name)
571
+ "hash_for_#{name}_url"
572
+ end
573
+
574
+ def url_helper_name(name)
575
+ "#{name}_url"
576
+ end
577
+
578
+ def name_route(route, name)
579
+ hash = route.known.symbolize_keys
580
+ hash[:controller] = "/#{hash[:controller]}"
581
+
582
+ define_method(hash_access_name(name)) { hash }
583
+ module_eval(%{def #{url_helper_name name}(options = {})
584
+ url_for(#{hash_access_name(name)}.merge(options))
585
+ end}, "generated/routing/named_routes/#{name}.rb")
586
+
587
+ protected url_helper_name(name), hash_access_name(name)
588
+
589
+ Helpers << url_helper_name(name).to_sym
590
+ Helpers.uniq!
591
+ end
330
592
 
331
- def self.extract_parameter_value(parameter) #:nodoc:
332
- value = (parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s
333
- CGI.escape(value)
593
+ def install(cls = ActionController::Base)
594
+ cls.send :include, self
595
+ if cls.respond_to? :helper_method
596
+ Helpers.each do |helper_name|
597
+ cls.send :helper_method, helper_name
598
+ end
599
+ end
600
+ end
601
+ end
334
602
  end
335
603
 
336
- def self.draw(*args, &block) #:nodoc:
337
- Routes.draw(*args) {|*args| block.call(*args)}
338
- end
339
-
340
604
  Routes = RouteSet.new
341
605
  end
342
606
  end