actionpack 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. data/CHANGELOG +149 -7
  2. data/MIT-LICENSE +1 -1
  3. data/README +1 -1
  4. data/Rakefile +5 -6
  5. data/lib/action_controller.rb +2 -2
  6. data/lib/action_controller/assertions/model_assertions.rb +2 -1
  7. data/lib/action_controller/assertions/response_assertions.rb +4 -2
  8. data/lib/action_controller/assertions/routing_assertions.rb +3 -3
  9. data/lib/action_controller/assertions/selector_assertions.rb +30 -27
  10. data/lib/action_controller/assertions/tag_assertions.rb +3 -3
  11. data/lib/action_controller/base.rb +103 -129
  12. data/lib/action_controller/benchmarking.rb +3 -3
  13. data/lib/action_controller/caching.rb +41 -652
  14. data/lib/action_controller/caching/actions.rb +144 -0
  15. data/lib/action_controller/caching/fragments.rb +138 -0
  16. data/lib/action_controller/caching/pages.rb +154 -0
  17. data/lib/action_controller/caching/sql_cache.rb +18 -0
  18. data/lib/action_controller/caching/sweeping.rb +97 -0
  19. data/lib/action_controller/cgi_ext/cookie.rb +27 -23
  20. data/lib/action_controller/cgi_ext/stdinput.rb +1 -0
  21. data/lib/action_controller/cgi_process.rb +6 -4
  22. data/lib/action_controller/components.rb +7 -6
  23. data/lib/action_controller/cookies.rb +31 -19
  24. data/lib/action_controller/dispatcher.rb +51 -84
  25. data/lib/action_controller/filters.rb +295 -421
  26. data/lib/action_controller/flash.rb +1 -6
  27. data/lib/action_controller/headers.rb +31 -0
  28. data/lib/action_controller/helpers.rb +26 -9
  29. data/lib/action_controller/http_authentication.rb +1 -1
  30. data/lib/action_controller/integration.rb +65 -13
  31. data/lib/action_controller/layout.rb +24 -39
  32. data/lib/action_controller/mime_responds.rb +7 -3
  33. data/lib/action_controller/mime_type.rb +25 -9
  34. data/lib/action_controller/mime_types.rb +1 -1
  35. data/lib/action_controller/polymorphic_routes.rb +32 -17
  36. data/lib/action_controller/record_identifier.rb +10 -4
  37. data/lib/action_controller/request.rb +46 -30
  38. data/lib/action_controller/request_forgery_protection.rb +10 -9
  39. data/lib/action_controller/request_profiler.rb +29 -8
  40. data/lib/action_controller/rescue.rb +24 -24
  41. data/lib/action_controller/resources.rb +66 -23
  42. data/lib/action_controller/response.rb +2 -2
  43. data/lib/action_controller/routing.rb +113 -1229
  44. data/lib/action_controller/routing/builder.rb +204 -0
  45. data/lib/action_controller/{routing_optimisation.rb → routing/optimisations.rb} +13 -12
  46. data/lib/action_controller/routing/recognition_optimisation.rb +158 -0
  47. data/lib/action_controller/routing/route.rb +240 -0
  48. data/lib/action_controller/routing/route_set.rb +435 -0
  49. data/lib/action_controller/routing/routing_ext.rb +46 -0
  50. data/lib/action_controller/routing/segments.rb +283 -0
  51. data/lib/action_controller/session/active_record_store.rb +13 -8
  52. data/lib/action_controller/session/cookie_store.rb +20 -17
  53. data/lib/action_controller/session_management.rb +10 -3
  54. data/lib/action_controller/streaming.rb +45 -31
  55. data/lib/action_controller/test_case.rb +33 -23
  56. data/lib/action_controller/test_process.rb +39 -35
  57. data/lib/action_controller/url_rewriter.rb +18 -12
  58. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +1 -1
  59. data/lib/action_pack.rb +1 -1
  60. data/lib/action_pack/version.rb +2 -2
  61. data/lib/action_view.rb +11 -3
  62. data/lib/action_view/base.rb +73 -390
  63. data/lib/action_view/helpers/active_record_helper.rb +83 -62
  64. data/lib/action_view/helpers/asset_tag_helper.rb +101 -44
  65. data/lib/action_view/helpers/atom_feed_helper.rb +35 -7
  66. data/lib/action_view/helpers/benchmark_helper.rb +5 -3
  67. data/lib/action_view/helpers/cache_helper.rb +3 -2
  68. data/lib/action_view/helpers/capture_helper.rb +1 -2
  69. data/lib/action_view/helpers/date_helper.rb +104 -82
  70. data/lib/action_view/helpers/form_helper.rb +148 -75
  71. data/lib/action_view/helpers/form_options_helper.rb +44 -23
  72. data/lib/action_view/helpers/form_tag_helper.rb +22 -13
  73. data/lib/action_view/helpers/javascripts/controls.js +1 -1
  74. data/lib/action_view/helpers/javascripts/dragdrop.js +1 -1
  75. data/lib/action_view/helpers/javascripts/effects.js +1 -1
  76. data/lib/action_view/helpers/number_helper.rb +10 -3
  77. data/lib/action_view/helpers/prototype_helper.rb +61 -29
  78. data/lib/action_view/helpers/record_tag_helper.rb +3 -3
  79. data/lib/action_view/helpers/sanitize_helper.rb +23 -17
  80. data/lib/action_view/helpers/scriptaculous_helper.rb +86 -60
  81. data/lib/action_view/helpers/text_helper.rb +153 -125
  82. data/lib/action_view/helpers/url_helper.rb +83 -28
  83. data/lib/action_view/inline_template.rb +20 -0
  84. data/lib/action_view/partial_template.rb +70 -0
  85. data/lib/action_view/partials.rb +31 -73
  86. data/lib/action_view/template.rb +127 -0
  87. data/lib/action_view/template_error.rb +8 -7
  88. data/lib/action_view/template_finder.rb +177 -0
  89. data/lib/action_view/template_handler.rb +18 -1
  90. data/lib/action_view/template_handlers/builder.rb +10 -2
  91. data/lib/action_view/template_handlers/compilable.rb +128 -0
  92. data/lib/action_view/template_handlers/erb.rb +37 -2
  93. data/lib/action_view/template_handlers/rjs.rb +14 -1
  94. data/lib/action_view/test_case.rb +58 -0
  95. data/test/abstract_unit.rb +1 -1
  96. data/test/active_record_unit.rb +3 -6
  97. data/test/activerecord/active_record_store_test.rb +1 -2
  98. data/test/activerecord/render_partial_with_record_identification_test.rb +158 -41
  99. data/test/adv_attr_test.rb +20 -0
  100. data/test/controller/action_pack_assertions_test.rb +16 -19
  101. data/test/controller/addresses_render_test.rb +1 -1
  102. data/test/controller/assert_select_test.rb +13 -6
  103. data/test/controller/base_test.rb +48 -2
  104. data/test/controller/benchmark_test.rb +1 -2
  105. data/test/controller/caching_test.rb +282 -21
  106. data/test/controller/capture_test.rb +1 -1
  107. data/test/controller/cgi_test.rb +1 -1
  108. data/test/controller/components_test.rb +1 -1
  109. data/test/controller/content_type_test.rb +2 -2
  110. data/test/controller/cookie_test.rb +13 -2
  111. data/test/controller/custom_handler_test.rb +14 -10
  112. data/test/controller/deprecation/deprecated_base_methods_test.rb +1 -1
  113. data/test/controller/dispatcher_test.rb +31 -49
  114. data/test/controller/fake_controllers.rb +17 -0
  115. data/test/controller/fake_models.rb +6 -0
  116. data/test/controller/filter_params_test.rb +14 -8
  117. data/test/controller/filters_test.rb +44 -16
  118. data/test/controller/flash_test.rb +2 -2
  119. data/test/controller/header_test.rb +14 -0
  120. data/test/controller/helper_test.rb +19 -15
  121. data/test/controller/html-scanner/document_test.rb +1 -2
  122. data/test/controller/html-scanner/node_test.rb +1 -2
  123. data/test/controller/html-scanner/sanitizer_test.rb +8 -5
  124. data/test/controller/html-scanner/tag_node_test.rb +1 -2
  125. data/test/controller/html-scanner/text_node_test.rb +2 -3
  126. data/test/controller/html-scanner/tokenizer_test.rb +8 -2
  127. data/test/controller/http_authentication_test.rb +1 -1
  128. data/test/controller/integration_test.rb +14 -16
  129. data/test/controller/integration_upload_test.rb +43 -0
  130. data/test/controller/layout_test.rb +26 -6
  131. data/test/controller/mime_responds_test.rb +39 -7
  132. data/test/controller/mime_type_test.rb +29 -5
  133. data/test/controller/new_render_test.rb +105 -34
  134. data/test/controller/polymorphic_routes_test.rb +32 -20
  135. data/test/controller/record_identifier_test.rb +38 -2
  136. data/test/controller/redirect_test.rb +21 -1
  137. data/test/controller/render_test.rb +59 -15
  138. data/test/controller/request_forgery_protection_test.rb +92 -5
  139. data/test/controller/request_test.rb +64 -6
  140. data/test/controller/rescue_test.rb +22 -6
  141. data/test/controller/resources_test.rb +102 -14
  142. data/test/controller/routing_test.rb +231 -19
  143. data/test/controller/selector_test.rb +2 -2
  144. data/test/controller/send_file_test.rb +14 -3
  145. data/test/controller/session/cookie_store_test.rb +16 -4
  146. data/test/controller/session/mem_cache_store_test.rb +3 -4
  147. data/test/controller/session_fixation_test.rb +1 -1
  148. data/test/controller/session_management_test.rb +23 -1
  149. data/test/controller/test_test.rb +39 -18
  150. data/test/controller/url_rewriter_test.rb +35 -1
  151. data/test/controller/verification_test.rb +1 -1
  152. data/test/controller/view_paths_test.rb +15 -12
  153. data/test/controller/webservice_test.rb +48 -3
  154. data/test/fixtures/bad_customers/_bad_customer.html.erb +1 -0
  155. data/test/fixtures/company.rb +1 -0
  156. data/test/fixtures/customers/_customer.html.erb +1 -0
  157. data/test/fixtures/db_definitions/sqlite.sql +6 -0
  158. data/test/fixtures/functional_caching/_partial.erb +3 -0
  159. data/test/fixtures/functional_caching/fragment_cached.html.erb +2 -0
  160. data/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb +1 -0
  161. data/test/fixtures/functional_caching/js_fragment_cached_with_partial.js.rjs +1 -0
  162. data/test/fixtures/good_customers/_good_customer.html.erb +1 -0
  163. data/test/fixtures/mascot.rb +3 -0
  164. data/test/fixtures/mascots.yml +4 -0
  165. data/test/fixtures/mascots/_mascot.html.erb +1 -0
  166. data/test/fixtures/multipart/boundary_problem_file +10 -0
  167. data/test/fixtures/public/javascripts/application.js +1 -0
  168. data/test/fixtures/public/javascripts/controls.js +1 -0
  169. data/test/fixtures/public/javascripts/dragdrop.js +1 -0
  170. data/test/fixtures/public/javascripts/effects.js +1 -0
  171. data/test/fixtures/public/javascripts/prototype.js +1 -0
  172. data/test/fixtures/public/javascripts/version.1.0.js +1 -0
  173. data/test/fixtures/public/stylesheets/version.1.0.css +1 -0
  174. data/test/fixtures/reply.rb +1 -0
  175. data/test/fixtures/shared.html.erb +1 -0
  176. data/test/fixtures/symlink_parent/symlinked_layout.erb +5 -0
  177. data/test/fixtures/test/_customer_counter.erb +1 -0
  178. data/test/fixtures/test/_form.erb +1 -0
  179. data/test/fixtures/test/_labelling_form.erb +1 -0
  180. data/test/fixtures/test/_raise.html.erb +1 -0
  181. data/test/fixtures/test/greeting.js.rjs +1 -0
  182. data/test/fixtures/topics/_topic.html.erb +1 -0
  183. data/test/template/active_record_helper_test.rb +25 -8
  184. data/test/template/asset_tag_helper_test.rb +100 -17
  185. data/test/template/atom_feed_helper_test.rb +29 -1
  186. data/test/template/benchmark_helper_test.rb +10 -22
  187. data/test/template/date_helper_test.rb +455 -153
  188. data/test/template/erb_util_test.rb +10 -42
  189. data/test/template/form_helper_test.rb +192 -66
  190. data/test/template/form_options_helper_test.rb +19 -8
  191. data/test/template/form_tag_helper_test.rb +11 -8
  192. data/test/template/javascript_helper_test.rb +3 -9
  193. data/test/template/number_helper_test.rb +6 -3
  194. data/test/template/prototype_helper_test.rb +27 -40
  195. data/test/template/record_tag_helper_test.rb +54 -0
  196. data/test/template/sanitize_helper_test.rb +5 -6
  197. data/test/template/scriptaculous_helper_test.rb +7 -13
  198. data/test/template/tag_helper_test.rb +3 -6
  199. data/test/template/template_finder_test.rb +73 -0
  200. data/test/template/template_object_test.rb +95 -0
  201. data/test/template/test_test.rb +56 -0
  202. data/test/template/text_helper_test.rb +46 -33
  203. data/test/template/url_helper_test.rb +8 -10
  204. metadata +65 -12
  205. data/lib/action_view/compiled_templates.rb +0 -69
  206. data/test/action_view_test.rb +0 -44
  207. data/test/activerecord/fixtures_test.rb +0 -24
  208. data/test/controller/fragment_store_setting_test.rb +0 -47
  209. data/test/template/compiled_templates_test.rb +0 -197
  210. data/test/template/deprecate_ivars_test.rb +0 -51
@@ -17,13 +17,13 @@ module ActionController
17
17
  reset!
18
18
  end
19
19
 
20
- def benchmark(n)
20
+ def benchmark(n, profiling = false)
21
21
  @quiet = true
22
22
  print ' '
23
23
 
24
24
  result = Benchmark.realtime do
25
25
  n.times do |i|
26
- run
26
+ run(profiling)
27
27
  print_progress(i)
28
28
  end
29
29
  end
@@ -41,7 +41,23 @@ module ActionController
41
41
  private
42
42
  def define_run_method(script_path)
43
43
  script = File.read(script_path)
44
- source = "def run\n#{script}\nreset!\nend"
44
+
45
+ source = <<-end_source
46
+ def run(profiling = false)
47
+ if profiling
48
+ RubyProf.resume do
49
+ #{script}
50
+ end
51
+ else
52
+ #{script}
53
+ end
54
+
55
+ old_request_count = request_count
56
+ reset!
57
+ self.request_count = old_request_count
58
+ end
59
+ end_source
60
+
45
61
  instance_eval source, script_path, 1
46
62
  end
47
63
 
@@ -82,21 +98,22 @@ module ActionController
82
98
  def profile(sandbox)
83
99
  load_ruby_prof
84
100
 
85
- results = RubyProf.profile { benchmark(sandbox) }
101
+ benchmark(sandbox, true)
102
+ results = RubyProf.stop
86
103
 
87
104
  show_profile_results results
88
105
  results
89
106
  end
90
107
 
91
- def benchmark(sandbox)
108
+ def benchmark(sandbox, profiling = false)
92
109
  sandbox.request_count = 0
93
- elapsed = sandbox.benchmark(options[:n]).to_f
110
+ elapsed = sandbox.benchmark(options[:n], profiling).to_f
94
111
  count = sandbox.request_count.to_i
95
112
  puts '%.2f sec, %d requests, %d req/sec' % [elapsed, count, count / elapsed]
96
113
  end
97
114
 
98
115
  def warmup(sandbox)
99
- Benchmark.realtime { sandbox.run }
116
+ Benchmark.realtime { sandbox.run(false) }
100
117
  end
101
118
 
102
119
  def default_options
@@ -110,6 +127,7 @@ module ActionController
110
127
 
111
128
  opt.on('-n', '--times [100]', 'How many requests to process. Defaults to 100.') { |v| options[:n] = v.to_i if v }
112
129
  opt.on('-b', '--benchmark', 'Benchmark instead of profiling') { |v| options[:benchmark] = v }
130
+ opt.on('-m', '--measure [mode]', 'Which ruby-prof measure mode to use: process_time, wall_time, cpu_time, allocations, or memory. Defaults to process_time.') { |v| options[:measure] = v }
113
131
  opt.on('--open [CMD]', 'Command to open profile results. Defaults to "open %s &"') { |v| options[:open] = v }
114
132
  opt.on('-h', '--help', 'Show this help') { puts opt; exit }
115
133
 
@@ -126,8 +144,11 @@ module ActionController
126
144
  protected
127
145
  def load_ruby_prof
128
146
  begin
147
+ gem 'ruby-prof', '>= 0.6.1'
129
148
  require 'ruby-prof'
130
- #RubyProf.measure_mode = RubyProf::ALLOCATED_OBJECTS
149
+ if mode = options[:measure]
150
+ RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
151
+ end
131
152
  rescue LoadError
132
153
  abort '`gem install ruby-prof` to use the profiler'
133
154
  end
@@ -26,7 +26,7 @@ module ActionController #:nodoc:
26
26
 
27
27
  DEFAULT_RESCUE_TEMPLATE = 'diagnostics'
28
28
  DEFAULT_RESCUE_TEMPLATES = {
29
- 'ActionController::MissingTemplate' => 'missing_template',
29
+ 'ActionView::MissingTemplate' => 'missing_template',
30
30
  'ActionController::RoutingError' => 'routing_error',
31
31
  'ActionController::UnknownAction' => 'unknown_action',
32
32
  'ActionView::TemplateError' => 'template_error'
@@ -58,33 +58,35 @@ module ActionController #:nodoc:
58
58
  # Rescue exceptions raised in controller actions.
59
59
  #
60
60
  # <tt>rescue_from</tt> receives a series of exception classes or class
61
- # names, and a trailing :with option with the name of a method or a Proc
62
- # object to be called to handle them. Alternatively a block can be given.
61
+ # names, and a trailing <tt>:with</tt> option with the name of a method
62
+ # or a Proc object to be called to handle them. Alternatively a block can
63
+ # be given.
63
64
  #
64
65
  # Handlers that take one argument will be called with the exception, so
65
66
  # that the exception can be inspected when dealing with it.
66
67
  #
67
68
  # Handlers are inherited. They are searched from right to left, from
68
69
  # bottom to top, and up the hierarchy. The handler of the first class for
69
- # which exception.is_a?(klass) holds true is the one invoked, if any.
70
+ # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
71
+ # any.
70
72
  #
71
- # class ApplicationController < ActionController::Base
72
- # rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
73
- # rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
73
+ # class ApplicationController < ActionController::Base
74
+ # rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
75
+ # rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
74
76
  #
75
- # rescue_from 'MyAppError::Base' do |exception|
76
- # render :xml => exception, :status => 500
77
- # end
78
- #
79
- # protected
80
- # def deny_access
81
- # ...
77
+ # rescue_from 'MyAppError::Base' do |exception|
78
+ # render :xml => exception, :status => 500
82
79
  # end
83
80
  #
84
- # def show_errors(exception)
85
- # exception.record.new_record? ? ...
86
- # end
87
- # end
81
+ # protected
82
+ # def deny_access
83
+ # ...
84
+ # end
85
+ #
86
+ # def show_errors(exception)
87
+ # exception.record.new_record? ? ...
88
+ # end
89
+ # end
88
90
  def rescue_from(*klasses, &block)
89
91
  options = klasses.extract_options!
90
92
  unless options.has_key?(:with)
@@ -153,7 +155,7 @@ module ActionController #:nodoc:
153
155
  # If the file doesn't exist, the body of the response will be left empty.
154
156
  def render_optional_error_file(status_code)
155
157
  status = interpret_status(status_code)
156
- path = "#{RAILS_ROOT}/public/#{status[0,3]}.html"
158
+ path = "#{Rails.public_path}/#{status[0,3]}.html"
157
159
  if File.exist?(path)
158
160
  render :file => path, :status => status
159
161
  else
@@ -165,7 +167,7 @@ module ActionController #:nodoc:
165
167
  # method if you wish to redefine the meaning of a local request to
166
168
  # include remote IP addresses or other criteria.
167
169
  def local_request? #:doc:
168
- request.remote_addr == LOCALHOST and request.remote_ip == LOCALHOST
170
+ request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST
169
171
  end
170
172
 
171
173
  # Render detailed diagnostics for unhandled exceptions rescued from
@@ -197,10 +199,8 @@ module ActionController #:nodoc:
197
199
  private
198
200
  def perform_action_with_rescue #:nodoc:
199
201
  perform_action_without_rescue
200
- rescue Exception => exception # errors from action performed
201
- return if rescue_action_with_handler(exception)
202
-
203
- rescue_action(exception)
202
+ rescue Exception => exception
203
+ rescue_action_with_handler(exception) || rescue_action(exception)
204
204
  end
205
205
 
206
206
  def rescues_path(template_name)
@@ -44,13 +44,14 @@ module ActionController
44
44
  module Resources
45
45
  class Resource #:nodoc:
46
46
  attr_reader :collection_methods, :member_methods, :new_methods
47
- attr_reader :path_prefix, :name_prefix
47
+ attr_reader :path_prefix, :name_prefix, :path_segment
48
48
  attr_reader :plural, :singular
49
49
  attr_reader :options
50
50
 
51
51
  def initialize(entities, options)
52
52
  @plural ||= entities
53
53
  @singular ||= options[:singular] || plural.to_s.singularize
54
+ @path_segment = options.delete(:as) || @plural
54
55
 
55
56
  @options = options
56
57
 
@@ -75,11 +76,13 @@ module ActionController
75
76
  end
76
77
 
77
78
  def path
78
- @path ||= "#{path_prefix}/#{plural}"
79
+ @path ||= "#{path_prefix}/#{path_segment}"
79
80
  end
80
81
 
81
82
  def new_path
82
- @new_path ||= "#{path}/new"
83
+ new_action = self.options[:path_names][:new] if self.options[:path_names]
84
+ new_action ||= Base.resources_path_names[:new]
85
+ @new_path ||= "#{path}/#{new_action}"
83
86
  end
84
87
 
85
88
  def member_path
@@ -188,7 +191,7 @@ module ActionController
188
191
  # end
189
192
  # end
190
193
  #
191
- # Along with the routes themselves, #resources generates named routes for use in
194
+ # Along with the routes themselves, +resources+ generates named routes for use in
192
195
  # controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers:
193
196
  #
194
197
  # Named Route Helpers
@@ -205,7 +208,7 @@ module ActionController
205
208
  # edit_message edit_message_url(id), hash_for_edit_message_url(id),
206
209
  # edit_message_path(id), hash_for_edit_message_path(id)
207
210
  #
208
- # You can use these helpers instead of #url_for or methods that take #url_for parameters. For example:
211
+ # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example:
209
212
  #
210
213
  # redirect_to :controller => 'messages', :action => 'index'
211
214
  # # and
@@ -226,17 +229,53 @@ module ActionController
226
229
  #
227
230
  # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %>
228
231
  #
229
- # The #resources method accepts the following options to customize the resulting routes:
230
- # * <tt>:collection</tt> - add named routes for other actions that operate on the collection.
232
+ # or
233
+ #
234
+ # <% form_for @message do |f| %>
235
+ #
236
+ # which takes into account whether <tt>@message</tt> is a new record or not and generates the
237
+ # path and method accordingly.
238
+ #
239
+ # The +resources+ method accepts the following options to customize the resulting routes:
240
+ # * <tt>:collection</tt> - Add named routes for other actions that operate on the collection.
231
241
  # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>
232
- # or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages/rss, with a route of rss_messages_url.
233
- # * <tt>:member</tt> - same as :collection, but for actions that operate on a specific member.
234
- # * <tt>:new</tt> - same as :collection, but for actions that operate on the new resource action.
235
- # * <tt>:controller</tt> - specify the controller name for the routes.
236
- # * <tt>:singular</tt> - specify the singular name used in the member routes.
237
- # * <tt>:requirements</tt> - set custom routing parameter requirements.
238
- # * <tt>:conditions</tt> - specify custom routing recognition conditions. Resources sets the :method value for the method-specific routes.
239
- # * <tt>:path_prefix</tt> - set a prefix to the routes with required route variables.
242
+ # or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages/rss, with a route of +rss_messages_url+.
243
+ # * <tt>:member</tt> - Same as <tt>:collection</tt>, but for actions that operate on a specific member.
244
+ # * <tt>:new</tt> - Same as <tt>:collection</tt>, but for actions that operate on the new resource action.
245
+ # * <tt>:controller</tt> - Specify the controller name for the routes.
246
+ # * <tt>:singular</tt> - Specify the singular name used in the member routes.
247
+ # * <tt>:requirements</tt> - Set custom routing parameter requirements.
248
+ # * <tt>:conditions</tt> - Specify custom routing recognition conditions. Resources sets the <tt>:method</tt> value for the method-specific routes.
249
+ # * <tt>:as</tt> - Specify a different resource name to use in the URL path. For example:
250
+ # # products_path == '/productos'
251
+ # map.resources :products, :as => 'productos' do |product|
252
+ # # product_reviews_path(product) == '/productos/1234/comentarios'
253
+ # product.resources :product_reviews, :as => 'comentarios'
254
+ # end
255
+ #
256
+ # * <tt>:has_one</tt> - Specify nested resources, this is a shorthand for mapping singleton resources beneath the current.
257
+ # * <tt>:has_many</tt> - Same has <tt>:has_one</tt>, but for plural resources.
258
+ #
259
+ # You may directly specify the routing association with +has_one+ and +has_many+ like:
260
+ #
261
+ # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments]
262
+ #
263
+ # This is the same as:
264
+ #
265
+ # map.resources :notes do |notes|
266
+ # notes.resource :author
267
+ # notes.resources :comments
268
+ # notes.resources :attachments
269
+ # end
270
+ #
271
+ # * <tt>:path_names</tt> - Specify different names for the 'new' and 'edit' actions. For example:
272
+ # # new_products_path == '/productos/nuevo'
273
+ # map.resources :products, :as => 'productos', :path_names => { :new => 'nuevo', :edit => 'editar' }
274
+ #
275
+ # You can also set default action names from an environment, like this:
276
+ # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' }
277
+ #
278
+ # * <tt>:path_prefix</tt> - Set a prefix to the routes with required route variables.
240
279
  #
241
280
  # Weblog comments usually belong to a post, so you might use resources like:
242
281
  #
@@ -249,7 +288,7 @@ module ActionController
249
288
  # article.resources :comments
250
289
  # end
251
290
  #
252
- # The comment resources work the same, but must now include a value for :article_id.
291
+ # The comment resources work the same, but must now include a value for <tt>:article_id</tt>.
253
292
  #
254
293
  # article_comments_url(@article)
255
294
  # article_comment_url(@article, @comment)
@@ -257,13 +296,13 @@ module ActionController
257
296
  # article_comments_url(:article_id => @article)
258
297
  # article_comment_url(:article_id => @article, :id => @comment)
259
298
  #
260
- # * <tt>:name_prefix</tt> - define a prefix for all generated routes, usually ending in an underscore.
299
+ # * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore.
261
300
  # Use this if you have named routes that may clash.
262
301
  #
263
302
  # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_'
264
303
  # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_'
265
304
  #
266
- # You may also use :name_prefix to override the generic named routes in a nested resource:
305
+ # You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested resource:
267
306
  #
268
307
  # map.resources :articles do |article|
269
308
  # article.resources :comments, :name_prefix => nil
@@ -304,7 +343,7 @@ module ActionController
304
343
  # # --> GET /categories/7/messages/1
305
344
  # # has named route "category_message"
306
345
  #
307
- # The #resources method sets HTTP method restrictions on the routes it generates. For example, making an
346
+ # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an
308
347
  # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in
309
348
  # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for resource routes.
310
349
  def resources(*entities, &block)
@@ -325,7 +364,7 @@ module ActionController
325
364
  #
326
365
  # See map.resources for general conventions. These are the main differences:
327
366
  # * A singular name is given to map.resource. The default controller name is still taken from the plural name.
328
- # * To specify a custom plural name, use the :plural option. There is no :singular option.
367
+ # * To specify a custom plural name, use the <tt>:plural</tt> option. There is no <tt>:singular</tt> option.
329
368
  # * No default index route is created for the singleton resource controller.
330
369
  # * When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1')
331
370
  #
@@ -367,7 +406,7 @@ module ActionController
367
406
  # end
368
407
  # end
369
408
  #
370
- # Along with the routes themselves, #resource generates named routes for
409
+ # Along with the routes themselves, +resource+ generates named routes for
371
410
  # use in controllers and views. <tt>map.resource :account</tt> produces
372
411
  # these named routes and helpers:
373
412
  #
@@ -485,8 +524,12 @@ module ActionController
485
524
  resource.member_methods.each do |method, actions|
486
525
  actions.each do |action|
487
526
  action_options = action_options_for(action, resource, method)
488
- map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}", action_options)
489
- map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}.:format",action_options)
527
+
528
+ action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
529
+ action_path ||= Base.resources_path_names[action] || action
530
+
531
+ map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options)
532
+ map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}.:format",action_options)
490
533
  end
491
534
  end
492
535
 
@@ -30,9 +30,9 @@ module ActionController
30
30
 
31
31
  def redirect(to_url, response_status)
32
32
  self.headers["Status"] = response_status
33
- self.headers["Location"] = to_url.gsub(/[\r\n]/, '')
33
+ self.headers["Location"] = to_url
34
34
 
35
- self.body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(to_url)}\">redirected</a>.</body></html>"
35
+ self.body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
36
36
  end
37
37
 
38
38
  def prepare!
@@ -1,53 +1,13 @@
1
1
  require 'cgi'
2
2
  require 'uri'
3
3
  require 'action_controller/polymorphic_routes'
4
- require 'action_controller/routing_optimisation'
5
-
6
- class Object
7
- def to_param
8
- to_s
9
- end
10
- end
11
-
12
- class TrueClass
13
- def to_param
14
- self
15
- end
16
- end
17
-
18
- class FalseClass
19
- def to_param
20
- self
21
- end
22
- end
23
-
24
- class NilClass
25
- def to_param
26
- self
27
- end
28
- end
29
-
30
- class Regexp #:nodoc:
31
- def number_of_captures
32
- Regexp.new("|#{source}").match('').captures.length
33
- end
34
-
35
- class << self
36
- def optionalize(pattern)
37
- case unoptionalize(pattern)
38
- when /\A(.|\(.*\))\Z/ then "#{pattern}?"
39
- else "(?:#{pattern})?"
40
- end
41
- end
42
-
43
- def unoptionalize(pattern)
44
- [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp|
45
- return $1 if regexp =~ pattern
46
- end
47
- return pattern
48
- end
49
- end
50
- end
4
+ require 'action_controller/routing/optimisations'
5
+ require 'action_controller/routing/routing_ext'
6
+ require 'action_controller/routing/route'
7
+ require 'action_controller/routing/segments'
8
+ require 'action_controller/routing/builder'
9
+ require 'action_controller/routing/route_set'
10
+ require 'action_controller/routing/recognition_optimisation'
51
11
 
52
12
  module ActionController
53
13
  # == Routing
@@ -55,7 +15,7 @@ module ActionController
55
15
  # The routing module provides URL rewriting in native Ruby. It's a way to
56
16
  # redirect incoming requests to controllers and actions. This replaces
57
17
  # mod_rewrite rules. Best of all, Rails' Routing works with any web server.
58
- # Routes are defined in routes.rb in your RAILS_ROOT/config directory.
18
+ # Routes are defined in <tt>config/routes.rb</tt>.
59
19
  #
60
20
  # Consider the following route, installed by Rails when you generate your
61
21
  # application:
@@ -63,7 +23,8 @@ module ActionController
63
23
  # map.connect ':controller/:action/:id'
64
24
  #
65
25
  # This route states that it expects requests to consist of a
66
- # :controller followed by an :action that in turn is fed some :id.
26
+ # <tt>:controller</tt> followed by an <tt>:action</tt> that in turn is fed
27
+ # some <tt>:id</tt>.
67
28
  #
68
29
  # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up
69
30
  # with:
@@ -76,35 +37,35 @@ module ActionController
76
37
  # Think of creating routes as drawing a map for your requests. The map tells
77
38
  # them where to go based on some predefined pattern:
78
39
  #
79
- # ActionController::Routing::Routes.draw do |map|
80
- # Pattern 1 tells some request to go to one place
81
- # Pattern 2 tell them to go to another
82
- # ...
83
- # end
40
+ # ActionController::Routing::Routes.draw do |map|
41
+ # Pattern 1 tells some request to go to one place
42
+ # Pattern 2 tell them to go to another
43
+ # ...
44
+ # end
84
45
  #
85
46
  # The following symbols are special:
86
47
  #
87
48
  # :controller maps to your controller name
88
49
  # :action maps to an action with your controllers
89
50
  #
90
- # Other names simply map to a parameter as in the case of +:id+.
51
+ # Other names simply map to a parameter as in the case of <tt>:id</tt>.
91
52
  #
92
53
  # == Route priority
93
54
  #
94
55
  # Not all routes are created equally. Routes have priority defined by the
95
- # order of appearance of the routes in the routes.rb file. The priority goes
56
+ # order of appearance of the routes in the <tt>config/routes.rb</tt> file. The priority goes
96
57
  # from top to bottom. The last route in that file is at the lowest priority
97
58
  # and will be applied last. If no route matches, 404 is returned.
98
59
  #
99
60
  # Within blocks, the empty pattern is at the highest priority.
100
61
  # In practice this works out nicely:
101
62
  #
102
- # ActionController::Routing::Routes.draw do |map|
103
- # map.with_options :controller => 'blog' do |blog|
104
- # blog.show '', :action => 'list'
105
- # end
106
- # map.connect ':controller/:action/:view'
107
- # end
63
+ # ActionController::Routing::Routes.draw do |map|
64
+ # map.with_options :controller => 'blog' do |blog|
65
+ # blog.show '', :action => 'list'
66
+ # end
67
+ # map.connect ':controller/:action/:view'
68
+ # end
108
69
  #
109
70
  # In this case, invoking blog controller (with an URL like '/blog/')
110
71
  # without parameters will activate the 'list' action by default.
@@ -115,14 +76,15 @@ module ActionController
115
76
  # Hash at the end of your mapping to set any default parameters.
116
77
  #
117
78
  # Example:
118
- # ActionController::Routing:Routes.draw do |map|
119
- # map.connect ':controller/:action/:id', :controller => 'blog'
120
- # end
79
+ #
80
+ # ActionController::Routing:Routes.draw do |map|
81
+ # map.connect ':controller/:action/:id', :controller => 'blog'
82
+ # end
121
83
  #
122
84
  # This sets up +blog+ as the default controller if no other is specified.
123
85
  # This means visiting '/' would invoke the blog controller.
124
86
  #
125
- # More formally, you can define defaults in a route with the +:defaults+ key.
87
+ # More formally, you can define defaults in a route with the <tt>:defaults</tt> key.
126
88
  #
127
89
  # map.connect ':controller/:action/:id', :action => 'show', :defaults => { :page => 'Dashboard' }
128
90
  #
@@ -133,6 +95,7 @@ module ActionController
133
95
  # for the full URL and +name_of_route_path+ for the URI path.
134
96
  #
135
97
  # Example:
98
+ #
136
99
  # # In routes.rb
137
100
  # map.login 'login', :controller => 'accounts', :action => 'login'
138
101
  #
@@ -155,6 +118,12 @@ module ActionController
155
118
  # root_url # => 'http://www.example.com/'
156
119
  # root_path # => ''
157
120
  #
121
+ # You can also specify an already-defined named route in your <tt>map.root</tt> call:
122
+ #
123
+ # # In routes.rb
124
+ # map.new_session :controller => 'sessions', :action => 'new'
125
+ # map.root :new_session
126
+ #
158
127
  # Note: when using +with_options+, the route is simply named after the
159
128
  # method you call on the block parameter rather than map.
160
129
  #
@@ -172,33 +141,52 @@ module ActionController
172
141
  #
173
142
  # Routes can generate pretty URLs. For example:
174
143
  #
175
- # map.connect 'articles/:year/:month/:day',
176
- # :controller => 'articles',
177
- # :action => 'find_by_date',
178
- # :year => /\d{4}/,
179
- # :month => /\d{1,2}/,
180
- # :day => /\d{1,2}/
144
+ # map.connect 'articles/:year/:month/:day',
145
+ # :controller => 'articles',
146
+ # :action => 'find_by_date',
147
+ # :year => /\d{4}/,
148
+ # :month => /\d{1,2}/,
149
+ # :day => /\d{1,2}/
181
150
  #
182
- # # Using the route above, the url below maps to:
183
- # # params = {:year => '2005', :month => '11', :day => '06'}
184
- # # http://localhost:3000/articles/2005/11/06
151
+ # Using the route above, the URL "http://localhost:3000/articles/2005/11/06"
152
+ # maps to
153
+ #
154
+ # params = {:year => '2005', :month => '11', :day => '06'}
185
155
  #
186
156
  # == Regular Expressions and parameters
187
157
  # You can specify a regular expression to define a format for a parameter.
188
158
  #
189
- # map.geocode 'geocode/:postalcode', :controller => 'geocode',
190
- # :action => 'show', :postalcode => /\d{5}(-\d{4})?/
159
+ # map.geocode 'geocode/:postalcode', :controller => 'geocode',
160
+ # :action => 'show', :postalcode => /\d{5}(-\d{4})?/
191
161
  #
192
162
  # or, more formally:
193
163
  #
194
164
  # map.geocode 'geocode/:postalcode', :controller => 'geocode',
195
165
  # :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ }
196
166
  #
167
+ # Formats can include the 'ignorecase' and 'extended syntax' regular
168
+ # expression modifiers:
169
+ #
170
+ # map.geocode 'geocode/:postalcode', :controller => 'geocode',
171
+ # :action => 'show', :postalcode => /hx\d\d\s\d[a-z]{2}/i
172
+ #
173
+ # map.geocode 'geocode/:postalcode', :controller => 'geocode',
174
+ # :action => 'show',:requirements => {
175
+ # :postalcode => /# Postcode format
176
+ # \d{5} #Prefix
177
+ # (-\d{4})? #Suffix
178
+ # /x
179
+ # }
180
+ #
181
+ # Using the multiline match modifier will raise an ArgumentError.
182
+ # Encoding regular expression modifiers are silently ignored. The
183
+ # match will always use the default encoding or ASCII.
184
+ #
197
185
  # == Route globbing
198
186
  #
199
187
  # Specifying <tt>*[string]</tt> as part of a rule like:
200
188
  #
201
- # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
189
+ # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
202
190
  #
203
191
  # will glob all remaining parts of the route that were not recognized earlier. This idiom
204
192
  # must appear at the end of the path. The globbed values are in <tt>params[:path]</tt> in
@@ -226,10 +214,10 @@ module ActionController
226
214
  #
227
215
  # You can reload routes if you feel you must:
228
216
  #
229
- # ActionController::Routing::Routes.reload
217
+ # ActionController::Routing::Routes.reload
230
218
  #
231
219
  # This will clear all named routes and reload routes.rb if the file has been modified from
232
- # last load. To absolutely force reloading, use +reload!+.
220
+ # last load. To absolutely force reloading, use <tt>reload!</tt>.
233
221
  #
234
222
  # == Testing Routes
235
223
  #
@@ -237,19 +225,19 @@ module ActionController
237
225
  #
238
226
  # === +assert_routing+
239
227
  #
240
- # def test_movie_route_properly_splits
241
- # opts = {:controller => "plugin", :action => "checkout", :id => "2"}
242
- # assert_routing "plugin/checkout/2", opts
243
- # end
228
+ # def test_movie_route_properly_splits
229
+ # opts = {:controller => "plugin", :action => "checkout", :id => "2"}
230
+ # assert_routing "plugin/checkout/2", opts
231
+ # end
244
232
  #
245
233
  # +assert_routing+ lets you test whether or not the route properly resolves into options.
246
234
  #
247
235
  # === +assert_recognizes+
248
236
  #
249
- # def test_route_has_options
250
- # opts = {:controller => "plugin", :action => "show", :id => "12"}
251
- # assert_recognizes opts, "/plugins/show/12"
252
- # end
237
+ # def test_route_has_options
238
+ # opts = {:controller => "plugin", :action => "show", :id => "12"}
239
+ # assert_recognizes opts, "/plugins/show/12"
240
+ # end
253
241
  #
254
242
  # Note the subtle difference between the two: +assert_routing+ tests that
255
243
  # a URL fits options while +assert_recognizes+ tests that a URL
@@ -257,16 +245,16 @@ module ActionController
257
245
  #
258
246
  # In tests you can simply pass the URL or named route to +get+ or +post+.
259
247
  #
260
- # def send_to_jail
261
- # get '/jail'
262
- # assert_response :success
263
- # assert_template "jail/front"
264
- # end
248
+ # def send_to_jail
249
+ # get '/jail'
250
+ # assert_response :success
251
+ # assert_template "jail/front"
252
+ # end
265
253
  #
266
- # def goes_to_login
267
- # get login_url
268
- # #...
269
- # end
254
+ # def goes_to_login
255
+ # get login_url
256
+ # #...
257
+ # end
270
258
  #
271
259
  # == View a list of all your routes
272
260
  #
@@ -289,6 +277,9 @@ module ActionController
289
277
  end
290
278
 
291
279
  class << self
280
+ # Expects an array of controller names as the first argument.
281
+ # Executes the passed block with only the named controllers named available.
282
+ # This method is used in internal Rails testing.
292
283
  def with_controllers(names)
293
284
  prior_controllers = @possible_controllers
294
285
  use_controllers! names
@@ -297,6 +288,10 @@ module ActionController
297
288
  use_controllers! prior_controllers
298
289
  end
299
290
 
291
+ # Returns an array of paths, cleaned of double-slashes and relative path references.
292
+ # * "\\\" and "//" become "\\" or "/".
293
+ # * "/foo/bar/../config" becomes "/foo/config".
294
+ # The returned array is sorted by length, descending.
300
295
  def normalize_paths(paths)
301
296
  # do the hokey-pokey of path normalization...
302
297
  paths = paths.collect do |path|
@@ -315,6 +310,7 @@ module ActionController
315
310
  paths = paths.uniq.sort_by { |path| - path.length }
316
311
  end
317
312
 
313
+ # Returns the array of controller names currently available to ActionController::Routing.
318
314
  def possible_controllers
319
315
  unless @possible_controllers
320
316
  @possible_controllers = []
@@ -339,10 +335,28 @@ module ActionController
339
335
  @possible_controllers
340
336
  end
341
337
 
338
+ # Replaces the internal list of controllers available to ActionController::Routing with the passed argument.
339
+ # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ])
342
340
  def use_controllers!(controller_names)
343
341
  @possible_controllers = controller_names
344
342
  end
345
343
 
344
+ # Returns a controller path for a new +controller+ based on a +previous+ controller path.
345
+ # Handles 4 scenarios:
346
+ #
347
+ # * stay in the previous controller:
348
+ # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion"
349
+ #
350
+ # * stay in the previous namespace:
351
+ # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts"
352
+ #
353
+ # * forced move to the root namespace:
354
+ # controller_relative_to( "/posts", "groups/discussion" ) # => "posts"
355
+ #
356
+ # * previous namespace is root:
357
+ # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts"
358
+ #
359
+
346
360
  def controller_relative_to(controller, previous)
347
361
  if controller.nil? then previous
348
362
  elsif controller[0] == ?/ then controller[1..-1]
@@ -351,1142 +365,12 @@ module ActionController
351
365
  end
352
366
  end
353
367
  end
354
-
355
- class Route #:nodoc:
356
- attr_accessor :segments, :requirements, :conditions, :optimise
357
-
358
- def initialize
359
- @segments = []
360
- @requirements = {}
361
- @conditions = {}
362
- @optimise = true
363
- end
364
-
365
- # Indicates whether the routes should be optimised with the string interpolation
366
- # version of the named routes methods.
367
- def optimise?
368
- @optimise && ActionController::Base::optimise_named_routes
369
- end
370
-
371
- def segment_keys
372
- segments.collect do |segment|
373
- segment.key if segment.respond_to? :key
374
- end.compact
375
- end
376
-
377
- # Write and compile a +generate+ method for this Route.
378
- def write_generation
379
- # Build the main body of the generation
380
- body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
381
-
382
- # If we have conditions that must be tested first, nest the body inside an if
383
- body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
384
- args = "options, hash, expire_on = {}"
385
-
386
- # Nest the body inside of a def block, and then compile it.
387
- raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
388
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
389
-
390
- # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
391
- # are the same as the keys that were recalled from the previous request. Thus,
392
- # we can use the expire_on.keys to determine which keys ought to be used to build
393
- # the query string. (Never use keys from the recalled request when building the
394
- # query string.)
395
-
396
- method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
397
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
398
-
399
- method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
400
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
401
- raw_method
402
- end
403
-
404
- # Build several lines of code that extract values from the options hash. If any
405
- # of the values are missing or rejected then a return will be executed.
406
- def generation_extraction
407
- segments.collect do |segment|
408
- segment.extraction_code
409
- end.compact * "\n"
410
- end
411
-
412
- # Produce a condition expression that will check the requirements of this route
413
- # upon generation.
414
- def generation_requirements
415
- requirement_conditions = requirements.collect do |key, req|
416
- if req.is_a? Regexp
417
- value_regexp = Regexp.new "\\A#{req.source}\\Z"
418
- "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
419
- else
420
- "hash[:#{key}] == #{req.inspect}"
421
- end
422
- end
423
- requirement_conditions * ' && ' unless requirement_conditions.empty?
424
- end
425
-
426
- def generation_structure
427
- segments.last.string_structure segments[0..-2]
428
- end
429
-
430
- # Write and compile a +recognize+ method for this Route.
431
- def write_recognition
432
- # Create an if structure to extract the params from a match if it occurs.
433
- body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
434
- body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
435
-
436
- # Build the method declaration and compile it
437
- method_decl = "def recognize(path, env={})\n#{body}\nend"
438
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
439
- method_decl
440
- end
441
-
442
- # Plugins may override this method to add other conditions, like checks on
443
- # host, subdomain, and so forth. Note that changes here only affect route
444
- # recognition, not generation.
445
- def recognition_conditions
446
- result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
447
- result << "conditions[:method] === env[:method]" if conditions[:method]
448
- result
449
- end
450
-
451
- # Build the regular expression pattern that will match this route.
452
- def recognition_pattern(wrap = true)
453
- pattern = ''
454
- segments.reverse_each do |segment|
455
- pattern = segment.build_pattern pattern
456
- end
457
- wrap ? ("\\A" + pattern + "\\Z") : pattern
458
- end
459
-
460
- # Write the code to extract the parameters from a matched route.
461
- def recognition_extraction
462
- next_capture = 1
463
- extraction = segments.collect do |segment|
464
- x = segment.match_extraction(next_capture)
465
- next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
466
- x
467
- end
468
- extraction.compact
469
- end
470
-
471
- # Write the real generation implementation and then resend the message.
472
- def generate(options, hash, expire_on = {})
473
- write_generation
474
- generate options, hash, expire_on
475
- end
476
-
477
- def generate_extras(options, hash, expire_on = {})
478
- write_generation
479
- generate_extras options, hash, expire_on
480
- end
481
-
482
- # Generate the query string with any extra keys in the hash and append
483
- # it to the given path, returning the new path.
484
- def append_query_string(path, hash, query_keys=nil)
485
- return nil unless path
486
- query_keys ||= extra_keys(hash)
487
- "#{path}#{build_query_string(hash, query_keys)}"
488
- end
489
-
490
- # Determine which keys in the given hash are "extra". Extra keys are
491
- # those that were not used to generate a particular route. The extra
492
- # keys also do not include those recalled from the prior request, nor
493
- # do they include any keys that were implied in the route (like a
494
- # :controller that is required, but not explicitly used in the text of
495
- # the route.)
496
- def extra_keys(hash, recall={})
497
- (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
498
- end
499
-
500
- # Build a query string from the keys of the given hash. If +only_keys+
501
- # is given (as an array), only the keys indicated will be used to build
502
- # the query string. The query string will correctly build array parameter
503
- # values.
504
- def build_query_string(hash, only_keys = nil)
505
- elements = []
506
-
507
- (only_keys || hash.keys).each do |key|
508
- if value = hash[key]
509
- elements << value.to_query(key)
510
- end
511
- end
512
-
513
- elements.empty? ? '' : "?#{elements.sort * '&'}"
514
- end
515
-
516
- # Write the real recognition implementation and then resend the message.
517
- def recognize(path, environment={})
518
- write_recognition
519
- recognize path, environment
520
- end
521
-
522
- # A route's parameter shell contains parameter values that are not in the
523
- # route's path, but should be placed in the recognized hash.
524
- #
525
- # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
526
- #
527
- # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
528
- #
529
- def parameter_shell
530
- @parameter_shell ||= returning({}) do |shell|
531
- requirements.each do |key, requirement|
532
- shell[key] = requirement unless requirement.is_a? Regexp
533
- end
534
- end
535
- end
536
-
537
- # Return an array containing all the keys that are used in this route. This
538
- # includes keys that appear inside the path, and keys that have requirements
539
- # placed upon them.
540
- def significant_keys
541
- @significant_keys ||= returning [] do |sk|
542
- segments.each { |segment| sk << segment.key if segment.respond_to? :key }
543
- sk.concat requirements.keys
544
- sk.uniq!
545
- end
546
- end
547
-
548
- # Return a hash of key/value pairs representing the keys in the route that
549
- # have defaults, or which are specified by non-regexp requirements.
550
- def defaults
551
- @defaults ||= returning({}) do |hash|
552
- segments.each do |segment|
553
- next unless segment.respond_to? :default
554
- hash[segment.key] = segment.default unless segment.default.nil?
555
- end
556
- requirements.each do |key,req|
557
- next if Regexp === req || req.nil?
558
- hash[key] = req
559
- end
560
- end
561
- end
562
-
563
- def matches_controller_and_action?(controller, action)
564
- unless defined? @matching_prepared
565
- @controller_requirement = requirement_for(:controller)
566
- @action_requirement = requirement_for(:action)
567
- @matching_prepared = true
568
- end
569
-
570
- (@controller_requirement.nil? || @controller_requirement === controller) &&
571
- (@action_requirement.nil? || @action_requirement === action)
572
- end
573
-
574
- def to_s
575
- @to_s ||= begin
576
- segs = segments.inject("") { |str,s| str << s.to_s }
577
- "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
578
- end
579
- end
580
-
581
- protected
582
- def requirement_for(key)
583
- return requirements[key] if requirements.key? key
584
- segments.each do |segment|
585
- return segment.regexp if segment.respond_to?(:key) && segment.key == key
586
- end
587
- nil
588
- end
589
-
590
- end
591
-
592
- class Segment #:nodoc:
593
- RESERVED_PCHAR = ':@&=+$,;'
594
- UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
595
-
596
- attr_accessor :is_optional
597
- alias_method :optional?, :is_optional
598
-
599
- def initialize
600
- self.is_optional = false
601
- end
602
-
603
- def extraction_code
604
- nil
605
- end
606
-
607
- # Continue generating string for the prior segments.
608
- def continue_string_structure(prior_segments)
609
- if prior_segments.empty?
610
- interpolation_statement(prior_segments)
611
- else
612
- new_priors = prior_segments[0..-2]
613
- prior_segments.last.string_structure(new_priors)
614
- end
615
- end
616
-
617
- def interpolation_chunk
618
- URI.escape(value, UNSAFE_PCHAR)
619
- end
620
-
621
- # Return a string interpolation statement for this segment and those before it.
622
- def interpolation_statement(prior_segments)
623
- chunks = prior_segments.collect { |s| s.interpolation_chunk }
624
- chunks << interpolation_chunk
625
- "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
626
- end
627
-
628
- def string_structure(prior_segments)
629
- optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
630
- end
631
-
632
- # Return an if condition that is true if all the prior segments can be generated.
633
- # If there are no optional segments before this one, then nil is returned.
634
- def all_optionals_available_condition(prior_segments)
635
- optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
636
- optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
637
- end
638
-
639
- # Recognition
640
-
641
- def match_extraction(next_capture)
642
- nil
643
- end
644
-
645
- # Warning
646
-
647
- # Returns true if this segment is optional? because of a default. If so, then
648
- # no warning will be emitted regarding this segment.
649
- def optionality_implied?
650
- false
651
- end
652
- end
653
-
654
- class StaticSegment < Segment #:nodoc:
655
- attr_accessor :value, :raw
656
- alias_method :raw?, :raw
657
-
658
- def initialize(value = nil)
659
- super()
660
- self.value = value
661
- end
662
-
663
- def interpolation_chunk
664
- raw? ? value : super
665
- end
666
-
667
- def regexp_chunk
668
- chunk = Regexp.escape(value)
669
- optional? ? Regexp.optionalize(chunk) : chunk
670
- end
671
-
672
- def build_pattern(pattern)
673
- escaped = Regexp.escape(value)
674
- if optional? && ! pattern.empty?
675
- "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
676
- elsif optional?
677
- Regexp.optionalize escaped
678
- else
679
- escaped + pattern
680
- end
681
- end
682
-
683
- def to_s
684
- value
685
- end
686
- end
687
-
688
- class DividerSegment < StaticSegment #:nodoc:
689
- def initialize(value = nil)
690
- super(value)
691
- self.raw = true
692
- self.is_optional = true
693
- end
694
-
695
- def optionality_implied?
696
- true
697
- end
698
- end
699
-
700
- class DynamicSegment < Segment #:nodoc:
701
- attr_accessor :key, :default, :regexp
702
-
703
- def initialize(key = nil, options = {})
704
- super()
705
- self.key = key
706
- self.default = options[:default] if options.key? :default
707
- self.is_optional = true if options[:optional] || options.key?(:default)
708
- end
709
-
710
- def to_s
711
- ":#{key}"
712
- end
713
-
714
- # The local variable name that the value of this segment will be extracted to.
715
- def local_name
716
- "#{key}_value"
717
- end
718
-
719
- def extract_value
720
- "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
721
- end
722
- def value_check
723
- if default # Then we know it won't be nil
724
- "#{value_regexp.inspect} =~ #{local_name}" if regexp
725
- elsif optional?
726
- # If we have a regexp check that the value is not given, or that it matches.
727
- # If we have no regexp, return nil since we do not require a condition.
728
- "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
729
- else # Then it must be present, and if we have a regexp, it must match too.
730
- "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
731
- end
732
- end
733
- def expiry_statement
734
- "expired, hash = true, options if !expired && expire_on[:#{key}]"
735
- end
736
-
737
- def extraction_code
738
- s = extract_value
739
- vc = value_check
740
- s << "\nreturn [nil,nil] unless #{vc}" if vc
741
- s << "\n#{expiry_statement}"
742
- end
743
-
744
- def interpolation_chunk(value_code = "#{local_name}")
745
- "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
746
- end
747
-
748
- def string_structure(prior_segments)
749
- if optional? # We have a conditional to do...
750
- # If we should not appear in the url, just write the code for the prior
751
- # segments. This occurs if our value is the default value, or, if we are
752
- # optional, if we have nil as our value.
753
- "if #{local_name} == #{default.inspect}\n" +
754
- continue_string_structure(prior_segments) +
755
- "\nelse\n" + # Otherwise, write the code up to here
756
- "#{interpolation_statement(prior_segments)}\nend"
757
- else
758
- interpolation_statement(prior_segments)
759
- end
760
- end
761
-
762
- def value_regexp
763
- Regexp.new "\\A#{regexp.source}\\Z" if regexp
764
- end
765
- def regexp_chunk
766
- regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)"
767
- end
768
-
769
- def build_pattern(pattern)
770
- chunk = regexp_chunk
771
- chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0
772
- pattern = "#{chunk}#{pattern}"
773
- optional? ? Regexp.optionalize(pattern) : pattern
774
- end
775
- def match_extraction(next_capture)
776
- # All non code-related keys (such as :id, :slug) are URI-unescaped as
777
- # path parameters.
778
- default_value = default ? default.inspect : nil
779
- %[
780
- value = if (m = match[#{next_capture}])
781
- URI.unescape(m)
782
- else
783
- #{default_value}
784
- end
785
- params[:#{key}] = value if value
786
- ]
787
- end
788
-
789
- def optionality_implied?
790
- [:action, :id].include? key
791
- end
792
-
793
- end
794
-
795
- class ControllerSegment < DynamicSegment #:nodoc:
796
- def regexp_chunk
797
- possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
798
- "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
799
- end
800
-
801
- # Don't URI.escape the controller name since it may contain slashes.
802
- def interpolation_chunk(value_code = "#{local_name}")
803
- "\#{#{value_code}.to_s}"
804
- end
805
-
806
- # Make sure controller names like Admin/Content are correctly normalized to
807
- # admin/content
808
- def extract_value
809
- "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
810
- end
811
-
812
- def match_extraction(next_capture)
813
- if default
814
- "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
815
- else
816
- "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
817
- end
818
- end
819
- end
820
-
821
- class PathSegment < DynamicSegment #:nodoc:
822
- RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/"
823
- UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
824
-
825
- def interpolation_chunk(value_code = "#{local_name}")
826
- "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}"
827
- end
828
-
829
- def default
830
- ''
831
- end
832
-
833
- def default=(path)
834
- raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
835
- end
836
-
837
- def match_extraction(next_capture)
838
- "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
839
- end
840
-
841
- def regexp_chunk
842
- regexp || "(.*)"
843
- end
844
-
845
- def optionality_implied?
846
- true
847
- end
848
-
849
- class Result < ::Array #:nodoc:
850
- def to_s() join '/' end
851
- def self.new_escaped(strings)
852
- new strings.collect {|str| URI.unescape str}
853
- end
854
- end
855
- end
856
-
857
- class RouteBuilder #:nodoc:
858
- attr_accessor :separators, :optional_separators
859
-
860
- def initialize
861
- self.separators = Routing::SEPARATORS
862
- self.optional_separators = %w( / )
863
- end
864
-
865
- def separator_pattern(inverted = false)
866
- "[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
867
- end
868
-
869
- def interval_regexp
870
- Regexp.new "(.*?)(#{separators.source}|$)"
871
- end
872
-
873
- # Accepts a "route path" (a string defining a route), and returns the array
874
- # of segments that corresponds to it. Note that the segment array is only
875
- # partially initialized--the defaults and requirements, for instance, need
876
- # to be set separately, via the #assign_route_options method, and the
877
- # #optional? method for each segment will not be reliable until after
878
- # #assign_route_options is called, as well.
879
- def segments_for_route_path(path)
880
- rest, segments = path, []
881
-
882
- until rest.empty?
883
- segment, rest = segment_for rest
884
- segments << segment
885
- end
886
- segments
887
- end
888
-
889
- # A factory method that returns a new segment instance appropriate for the
890
- # format of the given string.
891
- def segment_for(string)
892
- segment = case string
893
- when /\A:(\w+)/
894
- key = $1.to_sym
895
- case key
896
- when :controller then ControllerSegment.new(key)
897
- else DynamicSegment.new key
898
- end
899
- when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
900
- when /\A\?(.*?)\?/
901
- returning segment = StaticSegment.new($1) do
902
- segment.is_optional = true
903
- end
904
- when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
905
- when Regexp.new(separator_pattern) then
906
- returning segment = DividerSegment.new($&) do
907
- segment.is_optional = (optional_separators.include? $&)
908
- end
909
- end
910
- [segment, $~.post_match]
911
- end
912
-
913
- # Split the given hash of options into requirement and default hashes. The
914
- # segments are passed alongside in order to distinguish between default values
915
- # and requirements.
916
- def divide_route_options(segments, options)
917
- options = options.dup
918
-
919
- if options[:namespace]
920
- options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}"
921
- options.delete(:path_prefix)
922
- options.delete(:name_prefix)
923
- options.delete(:namespace)
924
- end
925
-
926
- requirements = (options.delete(:requirements) || {}).dup
927
- defaults = (options.delete(:defaults) || {}).dup
928
- conditions = (options.delete(:conditions) || {}).dup
929
-
930
- path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
931
- options.each do |key, value|
932
- hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
933
- hash[key] = value
934
- end
935
-
936
- [defaults, requirements, conditions]
937
- end
938
-
939
- # Takes a hash of defaults and a hash of requirements, and assigns them to
940
- # the segments. Any unused requirements (which do not correspond to a segment)
941
- # are returned as a hash.
942
- def assign_route_options(segments, defaults, requirements)
943
- route_requirements = {} # Requirements that do not belong to a segment
944
-
945
- segment_named = Proc.new do |key|
946
- segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
947
- end
948
-
949
- requirements.each do |key, requirement|
950
- segment = segment_named[key]
951
- if segment
952
- raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
953
- if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
954
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
955
- end
956
- segment.regexp = requirement
957
- else
958
- route_requirements[key] = requirement
959
- end
960
- end
961
-
962
- defaults.each do |key, default|
963
- segment = segment_named[key]
964
- raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
965
- segment.is_optional = true
966
- segment.default = default.to_param if default
967
- end
968
-
969
- assign_default_route_options(segments)
970
- ensure_required_segments(segments)
971
- route_requirements
972
- end
973
-
974
- # Assign default options, such as 'index' as a default for :action. This
975
- # method must be run *after* user supplied requirements and defaults have
976
- # been applied to the segments.
977
- def assign_default_route_options(segments)
978
- segments.each do |segment|
979
- next unless segment.is_a? DynamicSegment
980
- case segment.key
981
- when :action
982
- if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
983
- segment.default ||= 'index'
984
- segment.is_optional = true
985
- end
986
- when :id
987
- if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
988
- segment.is_optional = true
989
- end
990
- end
991
- end
992
- end
993
-
994
- # Makes sure that there are no optional segments that precede a required
995
- # segment. If any are found that precede a required segment, they are
996
- # made required.
997
- def ensure_required_segments(segments)
998
- allow_optional = true
999
- segments.reverse_each do |segment|
1000
- allow_optional &&= segment.optional?
1001
- if !allow_optional && segment.optional?
1002
- unless segment.optionality_implied?
1003
- warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
1004
- end
1005
- segment.is_optional = false
1006
- elsif allow_optional && segment.respond_to?(:default) && segment.default
1007
- # if a segment has a default, then it is optional
1008
- segment.is_optional = true
1009
- end
1010
- end
1011
- end
1012
-
1013
- # Construct and return a route with the given path and options.
1014
- def build(path, options)
1015
- # Wrap the path with slashes
1016
- path = "/#{path}" unless path[0] == ?/
1017
- path = "#{path}/" unless path[-1] == ?/
1018
-
1019
- path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]
1020
-
1021
- segments = segments_for_route_path(path)
1022
- defaults, requirements, conditions = divide_route_options(segments, options)
1023
- requirements = assign_route_options(segments, defaults, requirements)
1024
-
1025
- route = Route.new
1026
-
1027
- route.segments = segments
1028
- route.requirements = requirements
1029
- route.conditions = conditions
1030
-
1031
- if !route.significant_keys.include?(:action) && !route.requirements[:action]
1032
- route.requirements[:action] = "index"
1033
- route.significant_keys << :action
1034
- end
1035
-
1036
- # Routes cannot use the current string interpolation method
1037
- # if there are user-supplied :requirements as the interpolation
1038
- # code won't raise RoutingErrors when generating
1039
- if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
1040
- route.optimise = false
1041
- end
1042
-
1043
- if !route.significant_keys.include?(:controller)
1044
- raise ArgumentError, "Illegal route: the :controller must be specified!"
1045
- end
1046
-
1047
- route
1048
- end
1049
- end
1050
-
1051
- class RouteSet #:nodoc:
1052
- # Mapper instances are used to build routes. The object passed to the draw
1053
- # block in config/routes.rb is a Mapper instance.
1054
- #
1055
- # Mapper instances have relatively few instance methods, in order to avoid
1056
- # clashes with named routes.
1057
- class Mapper #:doc:
1058
- def initialize(set) #:nodoc:
1059
- @set = set
1060
- end
1061
-
1062
- # Create an unnamed route with the provided +path+ and +options+. See
1063
- # ActionController::Routing for an introduction to routes.
1064
- def connect(path, options = {})
1065
- @set.add_route(path, options)
1066
- end
1067
-
1068
- # Creates a named route called "root" for matching the root level request.
1069
- def root(options = {})
1070
- named_route("root", '', options)
1071
- end
1072
-
1073
- def named_route(name, path, options = {}) #:nodoc:
1074
- @set.add_named_route(name, path, options)
1075
- end
1076
-
1077
- # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
1078
- # Example:
1079
- #
1080
- # map.namespace(:admin) do |admin|
1081
- # admin.resources :products,
1082
- # :has_many => [ :tags, :images, :variants ]
1083
- # end
1084
- #
1085
- # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
1086
- # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
1087
- # Admin::TagsController.
1088
- def namespace(name, options = {}, &block)
1089
- if options[:namespace]
1090
- with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
1091
- else
1092
- with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
1093
- end
1094
- end
1095
-
1096
- def method_missing(route_name, *args, &proc) #:nodoc:
1097
- super unless args.length >= 1 && proc.nil?
1098
- @set.add_named_route(route_name, *args)
1099
- end
1100
- end
1101
-
1102
- # A NamedRouteCollection instance is a collection of named routes, and also
1103
- # maintains an anonymous module that can be used to install helpers for the
1104
- # named routes.
1105
- class NamedRouteCollection #:nodoc:
1106
- include Enumerable
1107
- include ActionController::Routing::Optimisation
1108
- attr_reader :routes, :helpers
1109
-
1110
- def initialize
1111
- clear!
1112
- end
1113
-
1114
- def clear!
1115
- @routes = {}
1116
- @helpers = []
1117
-
1118
- @module ||= Module.new
1119
- @module.instance_methods.each do |selector|
1120
- @module.class_eval { remove_method selector }
1121
- end
1122
- end
1123
-
1124
- def add(name, route)
1125
- routes[name.to_sym] = route
1126
- define_named_route_methods(name, route)
1127
- end
1128
-
1129
- def get(name)
1130
- routes[name.to_sym]
1131
- end
1132
-
1133
- alias []= add
1134
- alias [] get
1135
- alias clear clear!
1136
-
1137
- def each
1138
- routes.each { |name, route| yield name, route }
1139
- self
1140
- end
1141
-
1142
- def names
1143
- routes.keys
1144
- end
1145
-
1146
- def length
1147
- routes.length
1148
- end
1149
-
1150
- def reset!
1151
- old_routes = routes.dup
1152
- clear!
1153
- old_routes.each do |name, route|
1154
- add(name, route)
1155
- end
1156
- end
1157
-
1158
- def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
1159
- reset! if regenerate
1160
- Array(destinations).each do |dest|
1161
- dest.send! :include, @module
1162
- end
1163
- end
1164
-
1165
- private
1166
- def url_helper_name(name, kind = :url)
1167
- :"#{name}_#{kind}"
1168
- end
1169
-
1170
- def hash_access_name(name, kind = :url)
1171
- :"hash_for_#{name}_#{kind}"
1172
- end
1173
-
1174
- def define_named_route_methods(name, route)
1175
- {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
1176
- hash = route.defaults.merge(:use_route => name).merge(opts)
1177
- define_hash_access route, name, kind, hash
1178
- define_url_helper route, name, kind, hash
1179
- end
1180
- end
1181
-
1182
- def define_hash_access(route, name, kind, options)
1183
- selector = hash_access_name(name, kind)
1184
- @module.module_eval <<-end_eval # We use module_eval to avoid leaks
1185
- def #{selector}(options = nil)
1186
- options ? #{options.inspect}.merge(options) : #{options.inspect}
1187
- end
1188
- protected :#{selector}
1189
- end_eval
1190
- helpers << selector
1191
- end
1192
-
1193
- def define_url_helper(route, name, kind, options)
1194
- selector = url_helper_name(name, kind)
1195
- # The segment keys used for positional paramters
1196
-
1197
- hash_access_method = hash_access_name(name, kind)
1198
-
1199
- # allow ordered parameters to be associated with corresponding
1200
- # dynamic segments, so you can do
1201
- #
1202
- # foo_url(bar, baz, bang)
1203
- #
1204
- # instead of
1205
- #
1206
- # foo_url(:bar => bar, :baz => baz, :bang => bang)
1207
- #
1208
- # Also allow options hash, so you can do
1209
- #
1210
- # foo_url(bar, baz, bang, :sort_by => 'baz')
1211
- #
1212
- @module.module_eval <<-end_eval # We use module_eval to avoid leaks
1213
- def #{selector}(*args)
1214
- #{generate_optimisation_block(route, kind)}
1215
-
1216
- opts = if args.empty? || Hash === args.first
1217
- args.first || {}
1218
- else
1219
- options = args.extract_options!
1220
- args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
1221
- h[k] = v
1222
- h
1223
- end
1224
- options.merge(args)
1225
- end
1226
-
1227
- url_for(#{hash_access_method}(opts))
1228
- end
1229
- protected :#{selector}
1230
- end_eval
1231
- helpers << selector
1232
- end
1233
- end
1234
-
1235
- attr_accessor :routes, :named_routes
1236
-
1237
- def initialize
1238
- self.routes = []
1239
- self.named_routes = NamedRouteCollection.new
1240
- end
1241
-
1242
- # Subclasses and plugins may override this method to specify a different
1243
- # RouteBuilder instance, so that other route DSL's can be created.
1244
- def builder
1245
- @builder ||= RouteBuilder.new
1246
- end
1247
-
1248
- def draw
1249
- clear!
1250
- yield Mapper.new(self)
1251
- install_helpers
1252
- end
1253
-
1254
- def clear!
1255
- routes.clear
1256
- named_routes.clear
1257
- @combined_regexp = nil
1258
- @routes_by_controller = nil
1259
- end
1260
-
1261
- def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
1262
- Array(destinations).each { |d| d.module_eval { include Helpers } }
1263
- named_routes.install(destinations, regenerate_code)
1264
- end
1265
-
1266
- def empty?
1267
- routes.empty?
1268
- end
1269
-
1270
- def load!
1271
- Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
1272
- clear!
1273
- load_routes!
1274
- install_helpers
1275
- end
1276
-
1277
- # reload! will always force a reload whereas load checks the timestamp first
1278
- alias reload! load!
1279
-
1280
- def reload
1281
- if @routes_last_modified && defined?(RAILS_ROOT)
1282
- mtime = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime
1283
- # if it hasn't been changed, then just return
1284
- return if mtime == @routes_last_modified
1285
- # if it has changed then record the new time and fall to the load! below
1286
- @routes_last_modified = mtime
1287
- end
1288
- load!
1289
- end
1290
-
1291
- def load_routes!
1292
- if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes
1293
- load File.join("#{RAILS_ROOT}/config/routes.rb")
1294
- @routes_last_modified = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime
1295
- else
1296
- add_route ":controller/:action/:id"
1297
- end
1298
- end
1299
-
1300
- def add_route(path, options = {})
1301
- route = builder.build(path, options)
1302
- routes << route
1303
- route
1304
- end
1305
-
1306
- def add_named_route(name, path, options = {})
1307
- # TODO - is options EVER used?
1308
- name = options[:name_prefix] + name.to_s if options[:name_prefix]
1309
- named_routes[name.to_sym] = add_route(path, options)
1310
- end
1311
-
1312
- def options_as_params(options)
1313
- # If an explicit :controller was given, always make :action explicit
1314
- # too, so that action expiry works as expected for things like
1315
- #
1316
- # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
1317
- #
1318
- # (the above is from the unit tests). In the above case, because the
1319
- # controller was explicitly given, but no action, the action is implied to
1320
- # be "index", not the recalled action of "show".
1321
- #
1322
- # great fun, eh?
1323
-
1324
- options_as_params = options.clone
1325
- options_as_params[:action] ||= 'index' if options[:controller]
1326
- options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
1327
- options_as_params
1328
- end
1329
-
1330
- def build_expiry(options, recall)
1331
- recall.inject({}) do |expiry, (key, recalled_value)|
1332
- expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
1333
- expiry
1334
- end
1335
- end
1336
-
1337
- # Generate the path indicated by the arguments, and return an array of
1338
- # the keys that were not used to generate it.
1339
- def extra_keys(options, recall={})
1340
- generate_extras(options, recall).last
1341
- end
1342
-
1343
- def generate_extras(options, recall={})
1344
- generate(options, recall, :generate_extras)
1345
- end
1346
-
1347
- def generate(options, recall = {}, method=:generate)
1348
- named_route_name = options.delete(:use_route)
1349
- generate_all = options.delete(:generate_all)
1350
- if named_route_name
1351
- named_route = named_routes[named_route_name]
1352
- options = named_route.parameter_shell.merge(options)
1353
- end
1354
-
1355
- options = options_as_params(options)
1356
- expire_on = build_expiry(options, recall)
1357
-
1358
- if options[:controller]
1359
- options[:controller] = options[:controller].to_s
1360
- end
1361
- # if the controller has changed, make sure it changes relative to the
1362
- # current controller module, if any. In other words, if we're currently
1363
- # on admin/get, and the new controller is 'set', the new controller
1364
- # should really be admin/set.
1365
- if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
1366
- old_parts = recall[:controller].split('/')
1367
- new_parts = options[:controller].split('/')
1368
- parts = old_parts[0..-(new_parts.length + 1)] + new_parts
1369
- options[:controller] = parts.join('/')
1370
- end
1371
-
1372
- # drop the leading '/' on the controller name
1373
- options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
1374
- merged = recall.merge(options)
1375
-
1376
- if named_route
1377
- path = named_route.generate(options, merged, expire_on)
1378
- if path.nil?
1379
- raise_named_route_error(options, named_route, named_route_name)
1380
- else
1381
- return path
1382
- end
1383
- else
1384
- merged[:action] ||= 'index'
1385
- options[:action] ||= 'index'
1386
-
1387
- controller = merged[:controller]
1388
- action = merged[:action]
1389
-
1390
- raise RoutingError, "Need controller and action!" unless controller && action
1391
-
1392
- if generate_all
1393
- # Used by caching to expire all paths for a resource
1394
- return routes.collect do |route|
1395
- route.send!(method, options, merged, expire_on)
1396
- end.compact
1397
- end
1398
-
1399
- # don't use the recalled keys when determining which routes to check
1400
- routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
1401
-
1402
- routes.each do |route|
1403
- results = route.send!(method, options, merged, expire_on)
1404
- return results if results && (!results.is_a?(Array) || results.first)
1405
- end
1406
- end
1407
-
1408
- raise RoutingError, "No route matches #{options.inspect}"
1409
- end
1410
-
1411
- # try to give a helpful error message when named route generation fails
1412
- def raise_named_route_error(options, named_route, named_route_name)
1413
- diff = named_route.requirements.diff(options)
1414
- unless diff.empty?
1415
- raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
1416
- else
1417
- required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
1418
- required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
1419
- raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?"
1420
- end
1421
- end
1422
-
1423
- def recognize(request)
1424
- params = recognize_path(request.path, extract_request_environment(request))
1425
- request.path_parameters = params.with_indifferent_access
1426
- "#{params[:controller].camelize}Controller".constantize
1427
- end
1428
-
1429
- def recognize_path(path, environment={})
1430
- routes.each do |route|
1431
- result = route.recognize(path, environment) and return result
1432
- end
1433
-
1434
- allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
1435
-
1436
- if environment[:method] && !HTTP_METHODS.include?(environment[:method])
1437
- raise NotImplemented.new(*allows)
1438
- elsif !allows.empty?
1439
- raise MethodNotAllowed.new(*allows)
1440
- else
1441
- raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
1442
- end
1443
- end
1444
-
1445
- def routes_by_controller
1446
- @routes_by_controller ||= Hash.new do |controller_hash, controller|
1447
- controller_hash[controller] = Hash.new do |action_hash, action|
1448
- action_hash[action] = Hash.new do |key_hash, keys|
1449
- key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
1450
- end
1451
- end
1452
- end
1453
- end
1454
-
1455
- def routes_for(options, merged, expire_on)
1456
- raise "Need controller and action!" unless controller && action
1457
- controller = merged[:controller]
1458
- merged = options if expire_on[:controller]
1459
- action = merged[:action] || 'index'
1460
-
1461
- routes_by_controller[controller][action][merged.keys]
1462
- end
1463
-
1464
- def routes_for_controller_and_action(controller, action)
1465
- selected = routes.select do |route|
1466
- route.matches_controller_and_action? controller, action
1467
- end
1468
- (selected.length == routes.length) ? routes : selected
1469
- end
1470
-
1471
- def routes_for_controller_and_action_and_keys(controller, action, keys)
1472
- selected = routes.select do |route|
1473
- route.matches_controller_and_action? controller, action
1474
- end
1475
- selected.sort_by do |route|
1476
- (keys - route.significant_keys).length
1477
- end
1478
- end
1479
-
1480
- # Subclasses and plugins may override this method to extract further attributes
1481
- # from the request, for use by route conditions and such.
1482
- def extract_request_environment(request)
1483
- { :method => request.method }
1484
- end
1485
- end
368
+
1486
369
 
1487
370
  Routes = RouteSet.new
1488
371
 
1489
372
  ::Inflector.module_eval do
373
+ # Ensures that routes are reloaded when Rails inflections are updated.
1490
374
  def inflections_with_route_reloading(&block)
1491
375
  returning(inflections_without_route_reloading(&block)) {
1492
376
  ActionController::Routing::Routes.reload! if block_given?