2008-01-17 17:45:54 -05:00
module ActionController
module Routing
2008-07-28 14:38:20 -04:00
class RouteSet #:nodoc:
2008-01-17 17:45:54 -05:00
# Mapper instances are used to build routes. The object passed to the draw
# block in config/routes.rb is a Mapper instance.
#
# Mapper instances have relatively few instance methods, in order to avoid
# clashes with named routes.
class Mapper #:doc:
2008-11-23 17:35:13 -05:00
include ActionController :: Resources
2008-01-17 17:45:54 -05:00
def initialize ( set ) #:nodoc:
@set = set
end
# Create an unnamed route with the provided +path+ and +options+. See
# ActionController::Routing for an introduction to routes.
def connect ( path , options = { } )
@set . add_route ( path , options )
end
# Creates a named route called "root" for matching the root level request.
def root ( options = { } )
2008-01-26 03:41:19 -05:00
if options . is_a? ( Symbol )
if source_route = @set . named_routes . routes [ options ]
options = source_route . defaults . merge ( { :conditions = > source_route . conditions } )
end
end
2008-01-17 17:45:54 -05:00
named_route ( " root " , '' , options )
end
def named_route ( name , path , options = { } ) #:nodoc:
@set . add_named_route ( name , path , options )
end
# Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
# Example:
#
# map.namespace(:admin) do |admin|
# admin.resources :products,
# :has_many => [ :tags, :images, :variants ]
# end
#
# This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
# It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
# Admin::TagsController.
def namespace ( name , options = { } , & block )
if options [ :namespace ]
with_options ( { :path_prefix = > " #{ options . delete ( :path_prefix ) } / #{ name } " , :name_prefix = > " #{ options . delete ( :name_prefix ) } #{ name } _ " , :namespace = > " #{ options . delete ( :namespace ) } #{ name } / " } . merge ( options ) , & block )
else
with_options ( { :path_prefix = > name , :name_prefix = > " #{ name } _ " , :namespace = > " #{ name } / " } . merge ( options ) , & block )
end
end
def method_missing ( route_name , * args , & proc ) #:nodoc:
super unless args . length > = 1 && proc . nil?
@set . add_named_route ( route_name , * args )
end
end
# A NamedRouteCollection instance is a collection of named routes, and also
# maintains an anonymous module that can be used to install helpers for the
# named routes.
class NamedRouteCollection #:nodoc:
include Enumerable
include ActionController :: Routing :: Optimisation
attr_reader :routes , :helpers
def initialize
clear!
end
def clear!
@routes = { }
@helpers = [ ]
@module || = Module . new
@module . instance_methods . each do | selector |
@module . class_eval { remove_method selector }
end
end
def add ( name , route )
routes [ name . to_sym ] = route
define_named_route_methods ( name , route )
end
def get ( name )
routes [ name . to_sym ]
end
alias [ ] = add
alias [ ] get
alias clear clear!
def each
routes . each { | name , route | yield name , route }
self
end
def names
routes . keys
end
def length
routes . length
end
def reset!
old_routes = routes . dup
clear!
old_routes . each do | name , route |
add ( name , route )
end
end
def install ( destinations = [ ActionController :: Base , ActionView :: Base ] , regenerate = false )
reset! if regenerate
Array ( destinations ) . each do | dest |
2008-08-31 16:15:26 -04:00
dest . __send__ ( :include , @module )
2008-01-17 17:45:54 -05:00
end
end
private
def url_helper_name ( name , kind = :url )
:" #{ name } _ #{ kind } "
end
def hash_access_name ( name , kind = :url )
:" hash_for_ #{ name } _ #{ kind } "
end
def define_named_route_methods ( name , route )
{ :url = > { :only_path = > false } , :path = > { :only_path = > true } } . each do | kind , opts |
hash = route . defaults . merge ( :use_route = > name ) . merge ( opts )
define_hash_access route , name , kind , hash
define_url_helper route , name , kind , hash
end
end
2008-11-29 22:32:13 -05:00
def named_helper_module_eval ( code , * args )
@module . module_eval ( code , * args )
end
2008-01-17 17:45:54 -05:00
def define_hash_access ( route , name , kind , options )
selector = hash_access_name ( name , kind )
2008-11-29 22:32:13 -05:00
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
2008-12-28 14:48:05 -05:00
def #{selector}(options = nil) # def hash_for_users_url(options = nil)
options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false}
end # end
protected : #{selector} # protected :hash_for_users_url
2008-01-17 17:45:54 -05:00
end_eval
helpers << selector
end
def define_url_helper ( route , name , kind , options )
selector = url_helper_name ( name , kind )
# The segment keys used for positional paramters
hash_access_method = hash_access_name ( name , kind )
# allow ordered parameters to be associated with corresponding
# dynamic segments, so you can do
#
# foo_url(bar, baz, bang)
#
# instead of
#
# foo_url(:bar => bar, :baz => baz, :bang => bang)
#
# Also allow options hash, so you can do
#
# foo_url(bar, baz, bang, :sort_by => 'baz')
#
2008-11-29 22:32:13 -05:00
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
2008-12-28 14:48:05 -05:00
def #{selector}(*args) # def users_url(*args)
#
#{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)}
#
opts = if args . empty? || Hash === args . first # opts = if args.empty? || Hash === args.first
args . first || { } # args.first || {}
else # else
options = args . extract_options! # options = args.extract_options!
args = args . zip ( #{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)|
h [ k ] = v # h[k] = v
h # h
end # end
options . merge ( args ) # options.merge(args)
end # end
#
url_for ( #{hash_access_method}(opts)) # url_for(hash_for_users_url(opts))
#
end # end
#Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL.
def formatted_ #{selector}(*args) # def formatted_users_url(*args)
ActiveSupport :: Deprecation . warn ( # ActiveSupport::Deprecation.warn(
" formatted_ #{ selector } () has been deprecated. " + # "formatted_users_url() has been deprecated. " +
2008-12-30 18:16:51 -05:00
" Please pass format to the standard " + # "Please pass format to the standard " +
" #{ selector } method instead. " , caller ) # "users_url method instead.", caller)
2008-12-28 14:48:05 -05:00
#{selector}(*args) # users_url(*args)
end # end
protected : #{selector} # protected :users_url
2008-01-17 17:45:54 -05:00
end_eval
helpers << selector
end
end
2008-11-26 09:57:36 -05:00
attr_accessor :routes , :named_routes , :configuration_files
2008-01-17 17:45:54 -05:00
def initialize
2008-11-26 09:57:36 -05:00
self . configuration_files = [ ]
2008-01-17 17:45:54 -05:00
self . routes = [ ]
self . named_routes = NamedRouteCollection . new
2008-07-28 14:41:42 -04:00
2008-09-07 11:21:34 -04:00
clear_recognize_optimized!
2008-01-17 17:45:54 -05:00
end
# Subclasses and plugins may override this method to specify a different
# RouteBuilder instance, so that other route DSL's can be created.
def builder
@builder || = RouteBuilder . new
end
def draw
yield Mapper . new ( self )
install_helpers
end
def clear!
routes . clear
named_routes . clear
@combined_regexp = nil
@routes_by_controller = nil
2008-01-19 00:25:20 -05:00
# This will force routing/recognition_optimization.rb
# to refresh optimisations.
2008-09-07 11:21:34 -04:00
clear_recognize_optimized!
2008-01-17 17:45:54 -05:00
end
def install_helpers ( destinations = [ ActionController :: Base , ActionView :: Base ] , regenerate_code = false )
Array ( destinations ) . each { | d | d . module_eval { include Helpers } }
named_routes . install ( destinations , regenerate_code )
end
def empty?
routes . empty?
end
2008-11-26 09:57:36 -05:00
def add_configuration_file ( path )
self . configuration_files << path
end
# Deprecated accessor
def configuration_file = ( path )
add_configuration_file ( path )
end
# Deprecated accessor
def configuration_file
configuration_files
end
2008-01-17 17:45:54 -05:00
def load!
2008-11-26 09:57:36 -05:00
Routing . use_controllers! ( nil ) # Clear the controller cache so we may discover new ones
2008-01-17 17:45:54 -05:00
clear!
load_routes!
end
# reload! will always force a reload whereas load checks the timestamp first
alias reload! load !
def reload
2008-11-26 09:57:36 -05:00
if configuration_files . any? && @routes_last_modified
if routes_changed_at == @routes_last_modified
return # routes didn't change, don't reload
else
@routes_last_modified = routes_changed_at
end
2008-01-17 17:45:54 -05:00
end
2008-11-26 09:57:36 -05:00
2008-01-17 17:45:54 -05:00
load !
end
def load_routes!
2008-11-26 09:57:36 -05:00
if configuration_files . any?
configuration_files . each { | config | load ( config ) }
@routes_last_modified = routes_changed_at
2008-01-17 17:45:54 -05:00
else
add_route " :controller/:action/:id "
end
end
2008-11-26 09:57:36 -05:00
def routes_changed_at
routes_changed_at = nil
configuration_files . each do | config |
config_changed_at = File . stat ( config ) . mtime
if routes_changed_at . nil? || config_changed_at > routes_changed_at
routes_changed_at = config_changed_at
end
end
routes_changed_at
end
2008-01-17 17:45:54 -05:00
def add_route ( path , options = { } )
route = builder . build ( path , options )
routes << route
route
end
def add_named_route ( name , path , options = { } )
# TODO - is options EVER used?
name = options [ :name_prefix ] + name . to_s if options [ :name_prefix ]
named_routes [ name . to_sym ] = add_route ( path , options )
end
def options_as_params ( options )
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
#
# generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
#
# (the above is from the unit tests). In the above case, because the
# controller was explicitly given, but no action, the action is implied to
# be "index", not the recalled action of "show".
#
# great fun, eh?
options_as_params = options . clone
options_as_params [ :action ] || = 'index' if options [ :controller ]
options_as_params [ :action ] = options_as_params [ :action ] . to_s if options_as_params [ :action ]
options_as_params
end
def build_expiry ( options , recall )
recall . inject ( { } ) do | expiry , ( key , recalled_value ) |
expiry [ key ] = ( options . key? ( key ) && options [ key ] . to_param != recalled_value . to_param )
expiry
end
end
# Generate the path indicated by the arguments, and return an array of
# the keys that were not used to generate it.
def extra_keys ( options , recall = { } )
generate_extras ( options , recall ) . last
end
def generate_extras ( options , recall = { } )
generate ( options , recall , :generate_extras )
end
def generate ( options , recall = { } , method = :generate )
named_route_name = options . delete ( :use_route )
generate_all = options . delete ( :generate_all )
if named_route_name
named_route = named_routes [ named_route_name ]
options = named_route . parameter_shell . merge ( options )
end
options = options_as_params ( options )
expire_on = build_expiry ( options , recall )
if options [ :controller ]
options [ :controller ] = options [ :controller ] . to_s
end
# if the controller has changed, make sure it changes relative to the
# current controller module, if any. In other words, if we're currently
# on admin/get, and the new controller is 'set', the new controller
# should really be admin/set.
if ! named_route && expire_on [ :controller ] && options [ :controller ] && options [ :controller ] [ 0 ] != ?/
old_parts = recall [ :controller ] . split ( '/' )
new_parts = options [ :controller ] . split ( '/' )
parts = old_parts [ 0 .. - ( new_parts . length + 1 ) ] + new_parts
options [ :controller ] = parts . join ( '/' )
end
# drop the leading '/' on the controller name
options [ :controller ] = options [ :controller ] [ 1 .. - 1 ] if options [ :controller ] && options [ :controller ] [ 0 ] == ?/
merged = recall . merge ( options )
if named_route
path = named_route . generate ( options , merged , expire_on )
if path . nil?
raise_named_route_error ( options , named_route , named_route_name )
else
return path
end
else
merged [ :action ] || = 'index'
options [ :action ] || = 'index'
controller = merged [ :controller ]
action = merged [ :action ]
raise RoutingError , " Need controller and action! " unless controller && action
if generate_all
# Used by caching to expire all paths for a resource
return routes . collect do | route |
2008-08-31 16:15:26 -04:00
route . __send__ ( method , options , merged , expire_on )
2008-01-17 17:45:54 -05:00
end . compact
end
# don't use the recalled keys when determining which routes to check
2008-11-24 02:24:19 -05:00
routes = routes_by_controller [ controller ] [ action ] [ options . reject { | k , v | ! v } . keys . sort_by { | x | x . object_id } ]
2008-01-17 17:45:54 -05:00
routes . each do | route |
2008-08-31 16:15:26 -04:00
results = route . __send__ ( method , options , merged , expire_on )
2008-01-17 17:45:54 -05:00
return results if results && ( ! results . is_a? ( Array ) || results . first )
end
end
raise RoutingError , " No route matches #{ options . inspect } "
end
# try to give a helpful error message when named route generation fails
def raise_named_route_error ( options , named_route , named_route_name )
diff = named_route . requirements . diff ( options )
unless diff . empty?
raise RoutingError , " #{ named_route_name } _url failed to generate from #{ options . inspect } , expected: #{ named_route . requirements . inspect } , diff: #{ named_route . requirements . diff ( options ) . inspect } "
else
required_segments = named_route . segments . select { | seg | ( ! seg . optional? ) && ( ! seg . is_a? ( DividerSegment ) ) }
required_keys_or_values = required_segments . map { | seg | seg . key rescue seg . value } # we want either the key or the value from the segment
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? "
end
end
2008-12-28 16:31:03 -05:00
def call ( env )
2009-01-27 19:54:01 -05:00
request = ActionDispatch :: Request . new ( env )
2008-12-28 16:31:03 -05:00
app = Routing :: Routes . recognize ( request )
2009-05-02 05:15:09 -04:00
app . action ( request . parameters [ :action ] || 'index' ) . call ( env )
2008-12-28 16:31:03 -05:00
end
2008-01-17 17:45:54 -05:00
def recognize ( request )
params = recognize_path ( request . path , extract_request_environment ( request ) )
request . path_parameters = params . with_indifferent_access
" #{ params [ :controller ] . camelize } Controller " . constantize
end
def recognize_path ( path , environment = { } )
2008-01-19 00:25:20 -05:00
raise " Not optimized! Check that routing/recognition_optimisation overrides RouteSet # recognize_path. "
2008-01-17 17:45:54 -05:00
end
def routes_by_controller
@routes_by_controller || = Hash . new do | controller_hash , controller |
controller_hash [ controller ] = Hash . new do | action_hash , action |
action_hash [ action ] = Hash . new do | key_hash , keys |
key_hash [ keys ] = routes_for_controller_and_action_and_keys ( controller , action , keys )
end
end
end
end
def routes_for ( options , merged , expire_on )
raise " Need controller and action! " unless controller && action
controller = merged [ :controller ]
merged = options if expire_on [ :controller ]
action = merged [ :action ] || 'index'
routes_by_controller [ controller ] [ action ] [ merged . keys ]
end
def routes_for_controller_and_action ( controller , action )
selected = routes . select do | route |
route . matches_controller_and_action? controller , action
end
( selected . length == routes . length ) ? routes : selected
end
def routes_for_controller_and_action_and_keys ( controller , action , keys )
selected = routes . select do | route |
route . matches_controller_and_action? controller , action
end
selected . sort_by do | route |
( keys - route . significant_keys ) . length
end
end
# Subclasses and plugins may override this method to extract further attributes
# from the request, for use by route conditions and such.
def extract_request_environment ( request )
{ :method = > request . method }
end
end
end
2008-07-28 14:38:20 -04:00
end