Updated README.rdoc again
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / routing / route_set.rb
1 module ActionController
2 module Routing
3 class RouteSet #:nodoc:
4 # Mapper instances are used to build routes. The object passed to the draw
5 # block in config/routes.rb is a Mapper instance.
6 #
7 # Mapper instances have relatively few instance methods, in order to avoid
8 # clashes with named routes.
9 class Mapper #:doc:
10 include ActionController::Resources
11
12 def initialize(set) #:nodoc:
13 @set = set
14 end
15
16 # Create an unnamed route with the provided +path+ and +options+. See
17 # ActionController::Routing for an introduction to routes.
18 def connect(path, options = {})
19 @set.add_route(path, options)
20 end
21
22 # Creates a named route called "root" for matching the root level request.
23 def root(options = {})
24 if options.is_a?(Symbol)
25 if source_route = @set.named_routes.routes[options]
26 options = source_route.defaults.merge({ :conditions => source_route.conditions })
27 end
28 end
29 named_route("root", '', options)
30 end
31
32 def named_route(name, path, options = {}) #:nodoc:
33 @set.add_named_route(name, path, options)
34 end
35
36 # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
37 # Example:
38 #
39 # map.namespace(:admin) do |admin|
40 # admin.resources :products,
41 # :has_many => [ :tags, :images, :variants ]
42 # end
43 #
44 # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
45 # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
46 # Admin::TagsController.
47 def namespace(name, options = {}, &block)
48 if options[:namespace]
49 with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
50 else
51 with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
52 end
53 end
54
55 def method_missing(route_name, *args, &proc) #:nodoc:
56 super unless args.length >= 1 && proc.nil?
57 @set.add_named_route(route_name, *args)
58 end
59 end
60
61 # A NamedRouteCollection instance is a collection of named routes, and also
62 # maintains an anonymous module that can be used to install helpers for the
63 # named routes.
64 class NamedRouteCollection #:nodoc:
65 include Enumerable
66 include ActionController::Routing::Optimisation
67 attr_reader :routes, :helpers
68
69 def initialize
70 clear!
71 end
72
73 def clear!
74 @routes = {}
75 @helpers = []
76
77 @module ||= Module.new
78 @module.instance_methods.each do |selector|
79 @module.class_eval { remove_method selector }
80 end
81 end
82
83 def add(name, route)
84 routes[name.to_sym] = route
85 define_named_route_methods(name, route)
86 end
87
88 def get(name)
89 routes[name.to_sym]
90 end
91
92 alias []= add
93 alias [] get
94 alias clear clear!
95
96 def each
97 routes.each { |name, route| yield name, route }
98 self
99 end
100
101 def names
102 routes.keys
103 end
104
105 def length
106 routes.length
107 end
108
109 def reset!
110 old_routes = routes.dup
111 clear!
112 old_routes.each do |name, route|
113 add(name, route)
114 end
115 end
116
117 def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
118 reset! if regenerate
119 Array(destinations).each do |dest|
120 dest.__send__(:include, @module)
121 end
122 end
123
124 private
125 def url_helper_name(name, kind = :url)
126 :"#{name}_#{kind}"
127 end
128
129 def hash_access_name(name, kind = :url)
130 :"hash_for_#{name}_#{kind}"
131 end
132
133 def define_named_route_methods(name, route)
134 {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
135 hash = route.defaults.merge(:use_route => name).merge(opts)
136 define_hash_access route, name, kind, hash
137 define_url_helper route, name, kind, hash
138 end
139 end
140
141 def named_helper_module_eval(code, *args)
142 @module.module_eval(code, *args)
143 end
144
145 def define_hash_access(route, name, kind, options)
146 selector = hash_access_name(name, kind)
147 named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
148 def #{selector}(options = nil) # def hash_for_users_url(options = nil)
149 options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false}
150 end # end
151 protected :#{selector} # protected :hash_for_users_url
152 end_eval
153 helpers << selector
154 end
155
156 def define_url_helper(route, name, kind, options)
157 selector = url_helper_name(name, kind)
158 # The segment keys used for positional paramters
159
160 hash_access_method = hash_access_name(name, kind)
161
162 # allow ordered parameters to be associated with corresponding
163 # dynamic segments, so you can do
164 #
165 # foo_url(bar, baz, bang)
166 #
167 # instead of
168 #
169 # foo_url(:bar => bar, :baz => baz, :bang => bang)
170 #
171 # Also allow options hash, so you can do
172 #
173 # foo_url(bar, baz, bang, :sort_by => 'baz')
174 #
175 named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
176 def #{selector}(*args) # def users_url(*args)
177 #
178 #{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)}
179 #
180 opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first
181 args.first || {} # args.first || {}
182 else # else
183 options = args.extract_options! # options = args.extract_options!
184 args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)|
185 h[k] = v # h[k] = v
186 h # h
187 end # end
188 options.merge(args) # options.merge(args)
189 end # end
190 #
191 url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts))
192 #
193 end # end
194 #Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL.
195 def formatted_#{selector}(*args) # def formatted_users_url(*args)
196 ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn(
197 "formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " +
198 "Please pass format to the standard " + # "Please pass format to the standard " +
199 "#{selector} method instead.", caller) # "users_url method instead.", caller)
200 #{selector}(*args) # users_url(*args)
201 end # end
202 protected :#{selector} # protected :users_url
203 end_eval
204 helpers << selector
205 end
206 end
207
208 attr_accessor :routes, :named_routes, :configuration_files
209
210 def initialize
211 self.configuration_files = []
212
213 self.routes = []
214 self.named_routes = NamedRouteCollection.new
215
216 clear_recognize_optimized!
217 end
218
219 # Subclasses and plugins may override this method to specify a different
220 # RouteBuilder instance, so that other route DSL's can be created.
221 def builder
222 @builder ||= RouteBuilder.new
223 end
224
225 def draw
226 yield Mapper.new(self)
227 install_helpers
228 end
229
230 def clear!
231 routes.clear
232 named_routes.clear
233 @combined_regexp = nil
234 @routes_by_controller = nil
235 # This will force routing/recognition_optimization.rb
236 # to refresh optimisations.
237 clear_recognize_optimized!
238 end
239
240 def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
241 Array(destinations).each { |d| d.module_eval { include Helpers } }
242 named_routes.install(destinations, regenerate_code)
243 end
244
245 def empty?
246 routes.empty?
247 end
248
249 def add_configuration_file(path)
250 self.configuration_files << path
251 end
252
253 # Deprecated accessor
254 def configuration_file=(path)
255 add_configuration_file(path)
256 end
257
258 # Deprecated accessor
259 def configuration_file
260 configuration_files
261 end
262
263 def load!
264 Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones
265 clear!
266 load_routes!
267 end
268
269 # reload! will always force a reload whereas load checks the timestamp first
270 alias reload! load!
271
272 def reload
273 if configuration_files.any? && @routes_last_modified
274 if routes_changed_at == @routes_last_modified
275 return # routes didn't change, don't reload
276 else
277 @routes_last_modified = routes_changed_at
278 end
279 end
280
281 load!
282 end
283
284 def load_routes!
285 if configuration_files.any?
286 configuration_files.each { |config| load(config) }
287 @routes_last_modified = routes_changed_at
288 else
289 add_route ":controller/:action/:id"
290 end
291 end
292
293 def routes_changed_at
294 routes_changed_at = nil
295
296 configuration_files.each do |config|
297 config_changed_at = File.stat(config).mtime
298
299 if routes_changed_at.nil? || config_changed_at > routes_changed_at
300 routes_changed_at = config_changed_at
301 end
302 end
303
304 routes_changed_at
305 end
306
307 def add_route(path, options = {})
308 route = builder.build(path, options)
309 routes << route
310 route
311 end
312
313 def add_named_route(name, path, options = {})
314 # TODO - is options EVER used?
315 name = options[:name_prefix] + name.to_s if options[:name_prefix]
316 named_routes[name.to_sym] = add_route(path, options)
317 end
318
319 def options_as_params(options)
320 # If an explicit :controller was given, always make :action explicit
321 # too, so that action expiry works as expected for things like
322 #
323 # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
324 #
325 # (the above is from the unit tests). In the above case, because the
326 # controller was explicitly given, but no action, the action is implied to
327 # be "index", not the recalled action of "show".
328 #
329 # great fun, eh?
330
331 options_as_params = options.clone
332 options_as_params[:action] ||= 'index' if options[:controller]
333 options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
334 options_as_params
335 end
336
337 def build_expiry(options, recall)
338 recall.inject({}) do |expiry, (key, recalled_value)|
339 expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
340 expiry
341 end
342 end
343
344 # Generate the path indicated by the arguments, and return an array of
345 # the keys that were not used to generate it.
346 def extra_keys(options, recall={})
347 generate_extras(options, recall).last
348 end
349
350 def generate_extras(options, recall={})
351 generate(options, recall, :generate_extras)
352 end
353
354 def generate(options, recall = {}, method=:generate)
355 named_route_name = options.delete(:use_route)
356 generate_all = options.delete(:generate_all)
357 if named_route_name
358 named_route = named_routes[named_route_name]
359 options = named_route.parameter_shell.merge(options)
360 end
361
362 options = options_as_params(options)
363 expire_on = build_expiry(options, recall)
364
365 if options[:controller]
366 options[:controller] = options[:controller].to_s
367 end
368 # if the controller has changed, make sure it changes relative to the
369 # current controller module, if any. In other words, if we're currently
370 # on admin/get, and the new controller is 'set', the new controller
371 # should really be admin/set.
372 if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
373 old_parts = recall[:controller].split('/')
374 new_parts = options[:controller].split('/')
375 parts = old_parts[0..-(new_parts.length + 1)] + new_parts
376 options[:controller] = parts.join('/')
377 end
378
379 # drop the leading '/' on the controller name
380 options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
381 merged = recall.merge(options)
382
383 if named_route
384 path = named_route.generate(options, merged, expire_on)
385 if path.nil?
386 raise_named_route_error(options, named_route, named_route_name)
387 else
388 return path
389 end
390 else
391 merged[:action] ||= 'index'
392 options[:action] ||= 'index'
393
394 controller = merged[:controller]
395 action = merged[:action]
396
397 raise RoutingError, "Need controller and action!" unless controller && action
398
399 if generate_all
400 # Used by caching to expire all paths for a resource
401 return routes.collect do |route|
402 route.__send__(method, options, merged, expire_on)
403 end.compact
404 end
405
406 # don't use the recalled keys when determining which routes to check
407 routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }]
408
409 routes.each do |route|
410 results = route.__send__(method, options, merged, expire_on)
411 return results if results && (!results.is_a?(Array) || results.first)
412 end
413 end
414
415 raise RoutingError, "No route matches #{options.inspect}"
416 end
417
418 # try to give a helpful error message when named route generation fails
419 def raise_named_route_error(options, named_route, named_route_name)
420 diff = named_route.requirements.diff(options)
421 unless diff.empty?
422 raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
423 else
424 required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
425 required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
426 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?"
427 end
428 end
429
430 def call(env)
431 request = Request.new(env)
432 app = Routing::Routes.recognize(request)
433 app.call(env).to_a
434 end
435
436 def recognize(request)
437 params = recognize_path(request.path, extract_request_environment(request))
438 request.path_parameters = params.with_indifferent_access
439 "#{params[:controller].camelize}Controller".constantize
440 end
441
442 def recognize_path(path, environment={})
443 raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path."
444 end
445
446 def routes_by_controller
447 @routes_by_controller ||= Hash.new do |controller_hash, controller|
448 controller_hash[controller] = Hash.new do |action_hash, action|
449 action_hash[action] = Hash.new do |key_hash, keys|
450 key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
451 end
452 end
453 end
454 end
455
456 def routes_for(options, merged, expire_on)
457 raise "Need controller and action!" unless controller && action
458 controller = merged[:controller]
459 merged = options if expire_on[:controller]
460 action = merged[:action] || 'index'
461
462 routes_by_controller[controller][action][merged.keys]
463 end
464
465 def routes_for_controller_and_action(controller, action)
466 selected = routes.select do |route|
467 route.matches_controller_and_action? controller, action
468 end
469 (selected.length == routes.length) ? routes : selected
470 end
471
472 def routes_for_controller_and_action_and_keys(controller, action, keys)
473 selected = routes.select do |route|
474 route.matches_controller_and_action? controller, action
475 end
476 selected.sort_by do |route|
477 (keys - route.significant_keys).length
478 end
479 end
480
481 # Subclasses and plugins may override this method to extract further attributes
482 # from the request, for use by route conditions and such.
483 def extract_request_environment(request)
484 { :method => request.method }
485 end
486 end
487 end
488 end