2009-10-20 00:32:06 -04:00
require 'rack/mount'
require 'forwardable'
2008-01-17 17:45:54 -05:00
module ActionController
module Routing
2008-07-28 14:38:20 -04:00
class RouteSet #:nodoc:
2009-10-20 00:32:06 -04:00
NotFound = lambda { | env |
raise RoutingError , " No route matches #{ env [ :: Rack :: Mount :: Const :: PATH_INFO ] . inspect } with #{ env . inspect } "
}
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
class Dispatcher
def initialize ( options = { } )
defaults = options [ :defaults ]
@glob_param = options . delete ( :glob )
end
def call ( env )
params = env [ PARAMETERS_KEY ]
merge_default_action! ( params )
split_glob_param! ( params ) if @glob_param
params . each { | key , value | params [ key ] = URI . unescape ( value ) if value . is_a? ( String ) }
if env [ 'action_controller.recognize' ]
[ 200 , { } , params ]
else
controller = controller ( params )
controller . action ( params [ :action ] ) . call ( env )
end
end
private
def controller ( params )
if params && params . has_key? ( :controller )
controller = " #{ params [ :controller ] . camelize } Controller "
ActiveSupport :: Inflector . constantize ( controller )
end
end
def merge_default_action! ( params )
params [ :action ] || = 'index'
end
def split_glob_param! ( params )
params [ @glob_param ] = params [ @glob_param ] . split ( '/' ) . map { | v | URI . unescape ( v ) }
end
end
module RouteExtensions
def segment_keys
conditions [ :path_info ] . names . compact . map { | key | key . to_sym }
end
end
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
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 )
2009-07-25 11:03:58 -04:00
# The segment keys used for positional parameters
2008-01-17 17:45:54 -05:00
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)
#
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
2009-08-16 22:14:26 -04:00
clear!
2008-01-17 17:45:54 -05:00
end
def draw
2009-08-16 22:14:26 -04:00
clear!
2008-01-17 17:45:54 -05:00
yield Mapper . new ( self )
2009-10-20 00:32:06 -04:00
@set . add_route ( NotFound )
2008-01-17 17:45:54 -05:00
install_helpers
2009-10-20 00:32:06 -04:00
@set . freeze
2008-01-17 17:45:54 -05:00
end
def clear!
routes . clear
named_routes . clear
2009-10-20 00:32:06 -04:00
@set = :: Rack :: Mount :: RouteSet . new ( :parameters_key = > PARAMETERS_KEY )
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
2009-10-20 00:32:06 -04:00
2008-11-26 09:57:36 -05:00
# 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
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
2009-08-16 22:14:26 -04:00
draw do | map |
map . connect " :controller/:action/:id "
end
2008-01-17 17:45:54 -05:00
end
end
2009-08-16 22:14:26 -04:00
2008-11-26 09:57:36 -05:00
def routes_changed_at
routes_changed_at = nil
2009-10-20 00:32:06 -04:00
2008-11-26 09:57:36 -05:00
configuration_files . each do | config |
config_changed_at = File . stat ( config ) . mtime
if routes_changed_at . nil? || config_changed_at > routes_changed_at
2009-10-20 00:32:06 -04:00
routes_changed_at = config_changed_at
2008-11-26 09:57:36 -05:00
end
end
2009-10-20 00:32:06 -04:00
2008-11-26 09:57:36 -05:00
routes_changed_at
end
2008-01-17 17:45:54 -05:00
def add_route ( path , options = { } )
2009-10-20 00:32:06 -04:00
options = options . dup
if conditions = options . delete ( :conditions )
conditions = conditions . dup
method = [ conditions . delete ( :method ) ] . flatten . compact
method . map! { | m |
m = m . to_s . upcase
if m == " HEAD "
raise ArgumentError , " HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers "
end
unless HTTP_METHODS . include? ( m . downcase . to_sym )
raise ArgumentError , " Invalid HTTP method specified in route conditions "
end
m
}
if method . length > 1
method = Regexp . union ( * method )
elsif method . length == 1
method = method . first
else
method = nil
end
end
path_prefix = options . delete ( :path_prefix )
name_prefix = options . delete ( :name_prefix )
namespace = options . delete ( :namespace )
name = options . delete ( :_name )
name = " #{ name_prefix } #{ name } " if name_prefix
requirements = options . delete ( :requirements ) || { }
defaults = options . delete ( :defaults ) || { }
options . each do | k , v |
if v . is_a? ( Regexp )
if value = options . delete ( k )
requirements [ k . to_sym ] = value
end
else
value = options . delete ( k )
defaults [ k . to_sym ] = value . is_a? ( Symbol ) ? value : value . to_param
end
end
requirements . each do | _ , requirement |
if requirement . source =~ %r{ \ A( \\ A| \ ^)|( \\ Z| \\ z| \ $) \ Z }
raise ArgumentError , " Regexp anchor characters are not allowed in routing requirements: #{ requirement . inspect } "
end
if requirement . multiline?
raise ArgumentError , " Regexp multiline option not allowed in routing requirements: #{ requirement . inspect } "
end
end
possible_names = Routing . possible_controllers . collect { | n | Regexp . escape ( n ) }
requirements [ :controller ] || = Regexp . union ( * possible_names )
if defaults [ :controller ]
defaults [ :action ] || = 'index'
defaults [ :controller ] = defaults [ :controller ] . to_s
defaults [ :controller ] = " #{ namespace } #{ defaults [ :controller ] } " if namespace
end
if defaults [ :action ]
defaults [ :action ] = defaults [ :action ] . to_s
end
if path . is_a? ( String )
path = " #{ path_prefix } / #{ path } " if path_prefix
path = path . gsub ( '.:format' , '(.:format)' )
path = optionalize_trailing_dynamic_segments ( path , requirements , defaults )
glob = $1 . to_sym if path =~ / \/ \ *( \ w+)$ /
path = :: Rack :: Mount :: Utils . normalize_path ( path )
path = :: Rack :: Mount :: Strexp . compile ( path , requirements , %w( / . ? ) )
if glob && ! defaults [ glob ] . blank?
raise RoutingError , " paths cannot have non-empty default values "
end
end
app = Dispatcher . new ( :defaults = > defaults , :glob = > glob )
conditions = { }
conditions [ :request_method ] = method if method
conditions [ :path_info ] = path if path
route = @set . add_route ( app , conditions , defaults , name )
route . extend ( RouteExtensions )
2008-01-17 17:45:54 -05:00
routes << route
route
end
def add_named_route ( name , path , options = { } )
2009-10-20 00:32:06 -04:00
options [ :_name ] = name
route = add_route ( path , options )
named_routes [ route . name ] = route
route
2008-01-17 17:45:54 -05:00
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
2009-10-20 00:32:06 -04:00
def generate ( options , recall = { } , method = :generate )
options , recall = options . dup , recall . dup
named_route = options . delete ( :use_route )
2008-01-17 17:45:54 -05:00
options = options_as_params ( options )
expire_on = build_expiry ( options , recall )
2009-10-20 00:32:06 -04:00
recall [ :action ] || = 'index' if options [ :controller ] || recall [ :controller ]
if recall [ :controller ] && ( ! options . has_key? ( :controller ) || options [ :controller ] == recall [ :controller ] )
options [ :controller ] = recall . delete ( :controller )
if recall [ :action ] && ( ! options . has_key? ( :action ) || options [ :action ] == recall [ :action ] )
options [ :action ] = recall . delete ( :action )
if recall [ :id ] && ( ! options . has_key? ( :id ) || options [ :id ] == recall [ :id ] )
options [ :id ] = recall . delete ( :id )
end
end
2008-01-17 17:45:54 -05:00
end
2009-10-20 00:32:06 -04:00
options [ :controller ] = options [ :controller ] . to_s if options [ :controller ]
2008-01-17 17:45:54 -05:00
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
options [ :controller ] = options [ :controller ] [ 1 .. - 1 ] if options [ :controller ] && options [ :controller ] [ 0 ] == ?/
2009-10-20 00:32:06 -04:00
merged = options . merge ( recall )
if options . has_key? ( :action ) && options [ :action ] . nil?
options . delete ( :action )
recall [ :action ] = 'index'
end
recall [ :action ] = options . delete ( :action ) if options [ :action ] == 'index'
path = _uri ( named_route , options , recall )
if path && method == :generate_extras
uri = URI ( path )
extras = uri . query ?
Rack :: Utils . parse_nested_query ( uri . query ) . keys . map { | k | k . to_sym } :
[ ]
[ uri . path , extras ]
elsif path
path
2008-01-17 17:45:54 -05:00
else
2009-10-20 00:32:06 -04:00
raise RoutingError , " No route matches #{ options . inspect } "
2008-01-17 17:45:54 -05:00
end
2009-10-20 00:32:06 -04:00
rescue Rack :: Mount :: RoutingError
2008-01-17 17:45:54 -05:00
raise RoutingError , " No route matches #{ options . inspect } "
end
2009-10-20 00:32:06 -04:00
def call ( env )
@set . call ( env )
rescue ActionController :: RoutingError = > e
raise e if env [ 'action_controller.rescue_error' ] == false
method , path = env [ 'REQUEST_METHOD' ] . downcase . to_sym , env [ 'PATH_INFO' ]
# Route was not recognized. Try to find out why (maybe wrong verb).
allows = HTTP_METHODS . select { | verb |
begin
recognize_path ( path , { :method = > verb } , false )
rescue ActionController :: RoutingError
nil
end
}
if ! HTTP_METHODS . include? ( method )
raise NotImplemented . new ( * allows )
elsif ! allows . empty?
raise MethodNotAllowed . new ( * allows )
2008-01-17 17:45:54 -05:00
else
2009-10-20 00:32:06 -04:00
raise e
2008-01-17 17:45:54 -05:00
end
end
def recognize ( request )
params = recognize_path ( request . path , extract_request_environment ( request ) )
request . path_parameters = params . with_indifferent_access
2009-07-01 14:16:18 -04:00
" #{ params [ :controller ] . to_s . camelize } Controller " . constantize
2008-01-17 17:45:54 -05:00
end
2009-10-20 00:32:06 -04:00
def recognize_path ( path , environment = { } , rescue_error = true )
method = ( environment [ :method ] || " GET " ) . to_s . upcase
2008-01-17 17:45:54 -05:00
2009-10-20 00:32:06 -04:00
begin
env = Rack :: MockRequest . env_for ( path , { :method = > method } )
rescue URI :: InvalidURIError = > e
raise RoutingError , e . message
2008-01-17 17:45:54 -05:00
end
2009-10-20 00:32:06 -04:00
env [ 'action_controller.recognize' ] = true
env [ 'action_controller.rescue_error' ] = rescue_error
status , headers , body = call ( env )
body
2009-08-09 23:38:50 -04:00
end
2008-01-17 17:45:54 -05:00
# 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
2009-10-20 00:32:06 -04:00
private
def _uri ( named_route , params , recall )
params = URISegment . wrap_values ( params )
recall = URISegment . wrap_values ( recall )
unless result = @set . generate ( :path_info , named_route , params , recall )
return
end
uri , params = result
params . each do | k , v |
if v . _value
params [ k ] = v . _value
else
params . delete ( k )
end
end
uri << " ? #{ Rack :: Mount :: Utils . build_nested_query ( params ) } " if uri && params . any?
uri
end
class URISegment < Struct . new ( :_value , :_escape )
EXCLUDED = [ :controller ]
def self . wrap_values ( hash )
hash . inject ( { } ) { | h , ( k , v ) |
h [ k ] = new ( v , ! EXCLUDED . include? ( k . to_sym ) )
h
}
end
extend Forwardable
def_delegators :_value , :== , :eql? , :hash
def to_param
@to_param || = begin
if _value . is_a? ( Array )
_value . map { | v | _escaped ( v ) } . join ( '/' )
else
_escaped ( _value )
end
end
end
alias_method :to_s , :to_param
private
def _escaped ( value )
v = value . respond_to? ( :to_param ) ? value . to_param : value
_escape ? Rack :: Mount :: Utils . escape_uri ( v ) : v . to_s
end
end
def optionalize_trailing_dynamic_segments ( path , requirements , defaults )
path = ( path =~ / ^ \/ / ) ? path . dup : " / #{ path } "
optional , segments = true , [ ]
required_segments = requirements . keys
required_segments -= defaults . keys . compact
old_segments = path . split ( '/' )
old_segments . shift
length = old_segments . length
old_segments . reverse . each_with_index do | segment , index |
required_segments . each do | required |
if segment =~ / #{ required } /
optional = false
break
end
end
if optional
if segment == " :id " && segments . include? ( " :action " )
optional = false
elsif segment == " :controller " || segment == " :action " || segment == " :id "
# Ignore
elsif ! ( segment =~ / ^: \ w+$ / ) &&
! ( segment =~ / ^: \ w+ \ ( \ .:format \ )$ / )
optional = false
elsif segment =~ / ^:( \ w+)$ /
if defaults . has_key? ( $1 . to_sym )
defaults . delete ( $1 . to_sym )
else
optional = false
end
end
end
if optional && index < length - 1
segments . unshift ( '(/' , segment )
segments . push ( ')' )
elsif optional
segments . unshift ( '/(' , segment )
segments . push ( ')' )
else
segments . unshift ( '/' , segment )
end
end
segments . join
end
2008-01-17 17:45:54 -05:00
end
end
2008-07-28 14:38:20 -04:00
end