1 module ActionController
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.
7 # Mapper instances have relatively few instance methods, in order to avoid
8 # clashes with named routes.
10 def initialize(set
) #:nodoc:
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
)
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
})
27 named_route("root", '', options
)
30 def named_route(name
, path
, options
= {}) #:nodoc:
31 @set.add_named_route(name
, path
, options
)
34 # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
37 # map.namespace(:admin) do |admin|
38 # admin.resources :products,
39 # :has_many => [ :tags, :images, :variants ]
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
)
49 with_options({:path_prefix => name
, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options
), &block
)
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
)
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
62 class NamedRouteCollection
#:nodoc:
64 include ActionController
::Routing::Optimisation
65 attr_reader
:routes, :helpers
75 @module ||= Module
.new
76 @module.instance_methods
.each
do |selector
|
77 @module.class_eval
{ remove_method selector
}
82 routes
[name
.to_sym
] = route
83 define_named_route_methods(name
, route
)
95 routes
.each
{ |name
, route
| yield name
, route
}
108 old_routes
= routes
.dup
110 old_routes
.each
do |name
, route
|
115 def install(destinations
= [ActionController
::Base, ActionView
::Base], regenerate
= false)
117 Array(destinations
).each
do |dest
|
118 dest
.__send__(:include, @module)
123 def url_helper_name(name
, kind
= :url)
127 def hash_access_name(name
, kind
= :url)
128 :"hash_for_#{name}_#{kind}"
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
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}
145 protected :#{selector}
150 def define_url_helper(route
, name
, kind
, options
)
151 selector
= url_helper_name(name
, kind
)
152 # The segment keys used for positional paramters
154 hash_access_method
= hash_access_name(name
, kind
)
156 # allow ordered parameters to be associated with corresponding
157 # dynamic segments, so you can do
159 # foo_url(bar, baz, bang)
163 # foo_url(:bar => bar, :baz => baz, :bang => bang)
165 # Also allow options hash, so you can do
167 # foo_url(bar, baz, bang, :sort_by => 'baz')
169 @module.module_eval
<<-end_eval # We use module_eval to avoid leaks
170 def #{selector}(*args)
172 #{generate_optimisation_block(route, kind)}
174 opts = if args.empty? || Hash === args.first
177 options = args.extract_options!
178 args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
185 url_for(#{hash_access_method}(opts))
187 protected :#{selector}
193 attr_accessor
:routes, :named_routes, :configuration_file
197 self.named_routes
= NamedRouteCollection
.new
199 clear_recognize_optimized
!
202 # Subclasses and plugins may override this method to specify a different
203 # RouteBuilder instance, so that other route DSL's can be created.
205 @builder ||= RouteBuilder
.new
210 yield Mapper
.new(self)
217 @combined_regexp = nil
218 @routes_by_controller = nil
219 # This will force routing/recognition_optimization.rb
220 # to refresh optimisations.
221 clear_recognize_optimized
!
224 def install_helpers(destinations
= [ActionController
::Base, ActionView
::Base], regenerate_code
= false)
225 Array(destinations
).each
{ |d
| d
.module_eval
{ include Helpers
} }
226 named_routes
.install(destinations
, regenerate_code
)
234 Routing
.use_controllers
! nil # Clear the controller cache so we may discover new ones
239 # reload! will always force a reload whereas load checks the timestamp first
243 if @routes_last_modified && configuration_file
244 mtime
= File
.stat(configuration_file
).mtime
245 # if it hasn't been changed, then just return
246 return if mtime
== @routes_last_modified
247 # if it has changed then record the new time and fall to the load! below
248 @routes_last_modified = mtime
254 if configuration_file
255 load configuration_file
256 @routes_last_modified = File
.stat(configuration_file
).mtime
258 add_route
":controller/:action/:id"
262 def add_route(path
, options
= {})
263 route
= builder
.build(path
, options
)
268 def add_named_route(name
, path
, options
= {})
269 # TODO - is options EVER used?
270 name
= options
[:name_prefix] + name
.to_s
if options
[:name_prefix]
271 named_routes
[name
.to_sym
] = add_route(path
, options
)
274 def options_as_params(options
)
275 # If an explicit :controller was given, always make :action explicit
276 # too, so that action expiry works as expected for things like
278 # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
280 # (the above is from the unit tests). In the above case, because the
281 # controller was explicitly given, but no action, the action is implied to
282 # be "index", not the recalled action of "show".
286 options_as_params
= options
.clone
287 options_as_params
[:action] ||= 'index' if options
[:controller]
288 options_as_params
[:action] = options_as_params
[:action].to_s
if options_as_params
[:action]
292 def build_expiry(options
, recall
)
293 recall
.inject({}) do |expiry
, (key
, recalled_value
)|
294 expiry
[key
] = (options
.key
?(key
) && options
[key
].to_param
!= recalled_value
.to_param
)
299 # Generate the path indicated by the arguments, and return an array of
300 # the keys that were not used to generate it.
301 def extra_keys(options
, recall
={})
302 generate_extras(options
, recall
).last
305 def generate_extras(options
, recall
={})
306 generate(options
, recall
, :generate_extras)
309 def generate(options
, recall
= {}, method
=:generate)
310 named_route_name
= options
.delete(:use_route)
311 generate_all
= options
.delete(:generate_all)
313 named_route
= named_routes
[named_route_name
]
314 options
= named_route
.parameter_shell
.merge(options
)
317 options
= options_as_params(options
)
318 expire_on
= build_expiry(options
, recall
)
320 if options
[:controller]
321 options
[:controller] = options
[:controller].to_s
323 # if the controller has changed, make sure it changes relative to the
324 # current controller module, if any. In other words, if we're currently
325 # on admin/get, and the new controller is 'set', the new controller
326 # should really be admin/set.
327 if !named_route
&& expire_on
[:controller] && options
[:controller] && options
[:controller][0] != ?/
328 old_parts
= recall
[:controller].split('/')
329 new_parts
= options
[:controller].split('/')
330 parts
= old_parts
[0..-(new_parts
.length
+ 1)] + new_parts
331 options
[:controller] = parts
.join('/')
334 # drop the leading '/' on the controller name
335 options
[:controller] = options
[:controller][1..-1] if options
[:controller] && options
[:controller][0] == ?/
336 merged
= recall
.merge(options
)
339 path
= named_route
.generate(options
, merged
, expire_on
)
341 raise_named_route_error(options
, named_route
, named_route_name
)
346 merged
[:action] ||= 'index'
347 options
[:action] ||= 'index'
349 controller
= merged
[:controller]
350 action
= merged
[:action]
352 raise RoutingError
, "Need controller and action!" unless controller
&& action
355 # Used by caching to expire all paths for a resource
356 return routes
.collect
do |route
|
357 route
.__send__(method
, options
, merged
, expire_on
)
361 # don't use the recalled keys when determining which routes to check
362 routes
= routes_by_controller
[controller
][action
][options
.keys
.sort_by
{ |x
| x
.object_id
}]
364 routes
.each
do |route
|
365 results
= route
.__send__(method
, options
, merged
, expire_on
)
366 return results
if results
&& (!results
.is_a
?(Array
) || results
.first
)
370 raise RoutingError
, "No route matches #{options.inspect}"
373 # try to give a helpful error message when named route generation fails
374 def raise_named_route_error(options
, named_route
, named_route_name
)
375 diff
= named_route
.requirements
.diff(options
)
377 raise RoutingError
, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
379 required_segments
= named_route
.segments
.select
{|seg
| (!seg
.optional
?) && (!seg
.is_a
?(DividerSegment
)) }
380 required_keys_or_values
= required_segments
.map
{ |seg
| seg
.key
rescue seg
.value
} # we want either the key or the value from the segment
381 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?"
385 def recognize(request
)
386 params
= recognize_path(request
.path
, extract_request_environment(request
))
387 request
.path_parameters
= params
.with_indifferent_access
388 "#{params[:controller].camelize}Controller".constantize
391 def recognize_path(path
, environment
={})
392 raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path."
395 def routes_by_controller
396 @routes_by_controller ||= Hash
.new
do |controller_hash
, controller
|
397 controller_hash
[controller
] = Hash
.new
do |action_hash
, action
|
398 action_hash
[action
] = Hash
.new
do |key_hash
, keys
|
399 key_hash
[keys
] = routes_for_controller_and_action_and_keys(controller
, action
, keys
)
405 def routes_for(options
, merged
, expire_on
)
406 raise "Need controller and action!" unless controller
&& action
407 controller
= merged
[:controller]
408 merged
= options
if expire_on
[:controller]
409 action
= merged
[:action] || 'index'
411 routes_by_controller
[controller
][action
][merged
.keys
]
414 def routes_for_controller_and_action(controller
, action
)
415 selected
= routes
.select
do |route
|
416 route
.matches_controller_and_action
? controller
, action
418 (selected
.length
== routes
.length
) ? routes
: selected
421 def routes_for_controller_and_action_and_keys(controller
, action
, keys
)
422 selected
= routes
.select
do |route
|
423 route
.matches_controller_and_action
? controller
, action
425 selected
.sort_by
do |route
|
426 (keys
- route
.significant_keys
).length
430 # Subclasses and plugins may override this method to extract further attributes
431 # from the request, for use by route conditions and such.
432 def extract_request_environment(request
)
433 { :method => request
.method
}