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 include ActionController
::Resources
12 def initialize(set
) #:nodoc:
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
)
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
})
29 named_route("root", '', options
)
32 def named_route(name
, path
, options
= {}) #:nodoc:
33 @set.add_named_route(name
, path
, options
)
36 # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
39 # map.namespace(:admin) do |admin|
40 # admin.resources :products,
41 # :has_many => [ :tags, :images, :variants ]
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
)
51 with_options({:path_prefix => name
, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options
), &block
)
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
)
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
64 class NamedRouteCollection
#:nodoc:
66 include ActionController
::Routing::Optimisation
67 attr_reader
:routes, :helpers
77 @module ||= Module
.new
78 @module.instance_methods
.each
do |selector
|
79 @module.class_eval
{ remove_method selector
}
84 routes
[name
.to_sym
] = route
85 define_named_route_methods(name
, route
)
97 routes
.each
{ |name
, route
| yield name
, route
}
110 old_routes
= routes
.dup
112 old_routes
.each
do |name
, route
|
117 def install(destinations
= [ActionController
::Base, ActionView
::Base], regenerate
= false)
119 Array(destinations
).each
do |dest
|
120 dest
.__send__(:include, @module)
125 def url_helper_name(name
, kind
= :url)
129 def hash_access_name(name
, kind
= :url)
130 :"hash_for_#{name}_#{kind}"
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
141 def named_helper_module_eval(code
, *args
)
142 @module.module_eval(code
, *args
)
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}
151 protected :#{selector} # protected :hash_for_users_url
156 def define_url_helper(route
, name
, kind
, options
)
157 selector
= url_helper_name(name
, kind
)
158 # The segment keys used for positional paramters
160 hash_access_method
= hash_access_name(name
, kind
)
162 # allow ordered parameters to be associated with corresponding
163 # dynamic segments, so you can do
165 # foo_url(bar, baz, bang)
169 # foo_url(:bar => bar, :baz => baz, :bang => bang)
171 # Also allow options hash, so you can do
173 # foo_url(bar, baz, bang, :sort_by => 'baz')
175 named_helper_module_eval
<<-end_eval # We use module_eval to avoid leaks
176 def #{selector}(*args) # def users_url(*args)
178 #{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)}
180 opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first
181 args.first || {} # args.first || {}
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)|
188 options.merge(args) # options.merge(args)
191 url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts))
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)
202 protected :#{selector} # protected :users_url
208 attr_accessor
:routes, :named_routes, :configuration_files
211 self.configuration_files
= []
214 self.named_routes
= NamedRouteCollection
.new
216 clear_recognize_optimized
!
219 # Subclasses and plugins may override this method to specify a different
220 # RouteBuilder instance, so that other route DSL's can be created.
222 @builder ||= RouteBuilder
.new
226 yield Mapper
.new(self)
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
!
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
)
249 def add_configuration_file(path
)
250 self.configuration_files
<< path
253 # Deprecated accessor
254 def configuration_file
=(path
)
255 add_configuration_file(path
)
258 # Deprecated accessor
259 def configuration_file
264 Routing
.use_controllers
!(nil) # Clear the controller cache so we may discover new ones
269 # reload! will always force a reload whereas load checks the timestamp first
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
277 @routes_last_modified = routes_changed_at
285 if configuration_files
.any
?
286 configuration_files
.each
{ |config
| load(config
) }
287 @routes_last_modified = routes_changed_at
289 add_route
":controller/:action/:id"
293 def routes_changed_at
294 routes_changed_at
= nil
296 configuration_files
.each
do |config
|
297 config_changed_at
= File
.stat(config
).mtime
299 if routes_changed_at
.nil? || config_changed_at
> routes_changed_at
300 routes_changed_at
= config_changed_at
307 def add_route(path
, options
= {})
308 route
= builder
.build(path
, options
)
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
)
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
323 # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
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".
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]
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
)
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
350 def generate_extras(options
, recall
={})
351 generate(options
, recall
, :generate_extras)
354 def generate(options
, recall
= {}, method
=:generate)
355 named_route_name
= options
.delete(:use_route)
356 generate_all
= options
.delete(:generate_all)
358 named_route
= named_routes
[named_route_name
]
359 options
= named_route
.parameter_shell
.merge(options
)
362 options
= options_as_params(options
)
363 expire_on
= build_expiry(options
, recall
)
365 if options
[:controller]
366 options
[:controller] = options
[:controller].to_s
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('/')
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
)
384 path
= named_route
.generate(options
, merged
, expire_on
)
386 raise_named_route_error(options
, named_route
, named_route_name
)
391 merged
[:action] ||= 'index'
392 options
[:action] ||= 'index'
394 controller
= merged
[:controller]
395 action
= merged
[:action]
397 raise RoutingError
, "Need controller and action!" unless controller
&& action
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
)
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
}]
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
)
415 raise RoutingError
, "No route matches #{options.inspect}"
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
)
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}"
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?"
431 request
= Request
.new(env)
432 app
= Routing
::Routes.recognize(request
)
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
442 def recognize_path(path
, environment
={})
443 raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path."
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
)
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'
462 routes_by_controller
[controller
][action
][merged
.keys
]
465 def routes_for_controller_and_action(controller
, action
)
466 selected
= routes
.select
do |route
|
467 route
.matches_controller_and_action
? controller
, action
469 (selected
.length
== routes
.length
) ? routes
: selected
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
476 selected
.sort_by
do |route
|
477 (keys
- route
.significant_keys
).length
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
}