Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/routing/route_set.rb

Revision 8738, 15.5 kB (checked in by nzkoz, 5 months ago)

Make it simpler to make the root route an alias for another route. Closes #10818 [bscofield]

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