2005-02-14 20:45:35 -05:00
module ActionController
module Routing
ROUTE_FILE = defined? ( RAILS_ROOT ) ? File . expand_path ( File . join ( RAILS_ROOT , 'config' , 'routes' ) ) : nil
2005-02-23 20:29:43 -05:00
class Route #:nodoc:
2005-02-14 20:45:35 -05:00
attr_reader :defaults # The defaults hash
def initialize ( path , hash = { } )
raise ArgumentError , " Second argument must be a hash! " unless hash . kind_of? ( Hash )
2005-02-18 20:16:09 -05:00
@defaults = hash [ :defaults ] . kind_of? ( Hash ) ? hash . delete ( :defaults ) : { }
@requirements = hash [ :requirements ] . kind_of? ( Hash ) ? hash . delete ( :requirements ) : { }
2005-02-14 20:45:35 -05:00
self . items = path
hash . each do | k , v |
2005-02-18 20:16:09 -05:00
raise TypeError , " Hash keys must be symbols! " unless k . kind_of? Symbol
if v . kind_of? Regexp
raise ArgumentError , " Regexp requirement on #{ k } , but #{ k } is not in this route's path! " unless @items . include? k
@requirements [ k ] = v
else
( @items . include? ( k ) ? @defaults : @requirements ) [ k ] = v
end
end
@defaults . each do | k , v |
raise ArgumentError , " A default has been specified for #{ k } , but #{ k } is not in the path! " unless @items . include? k
@defaults [ k ] = v . to_s unless v . kind_of? ( String ) || v . nil?
2005-02-14 20:45:35 -05:00
end
2005-02-18 20:16:09 -05:00
@requirements . each { | k , v | raise ArgumentError , " A Regexp requirement has been specified for #{ k } , but #{ k } is not in the path! " if v . kind_of? ( Regexp ) && ! @items . include? ( k ) }
2005-02-14 20:45:35 -05:00
# Add in defaults for :action and :id.
[ [ :action , 'index' ] , [ :id , nil ] ] . each do | name , default |
@defaults [ name ] = default if @items . include? ( name ) && ! ( @requirements . key? ( name ) || @defaults . key? ( name ) )
end
end
# Generate a URL given the provided options.
# All values in options should be symbols.
# Returns the path and the unused names in a 2 element array.
# If generation fails, [nil, nil] is returned
# Generation can fail because of a missing value, or because an equality check fails.
#
# Generate urls will be as short as possible. If the last component of a url is equal to the default value,
# then that component is removed. This is applied as many times as possible. So, your index controller's
# index action will generate []
def generate ( options , defaults = { } )
2005-02-18 20:16:09 -05:00
non_matching = @requirements . keys . select { | name | ! passes_requirements? ( name , options [ name ] || defaults [ name ] ) }
non_matching . collect! { | name | requirements_for ( name ) }
return nil , " Mismatching option #{ 's' if non_matching . length > 1 } : \n #{ non_matching . join '\n ' } " unless non_matching . empty?
2005-02-14 20:45:35 -05:00
2005-02-18 20:16:09 -05:00
used_names = @requirements . inject ( { } ) { | hash , ( k , v ) | hash [ k ] = true ; hash } # Mark requirements as used so they don't get put in the query params
2005-02-14 20:45:35 -05:00
components = @items . collect do | item |
if item . kind_of? Symbol
used_names [ item ] = true
value = options [ item ] || defaults [ item ] || @defaults [ item ]
2005-02-18 20:16:09 -05:00
return nil , requirements_for ( item ) unless passes_requirements? ( item , value )
2005-02-14 20:45:35 -05:00
defaults = { } unless defaults == { } || value == defaults [ item ] # Stop using defaults if this component isn't the same as the default.
2005-02-18 18:52:28 -05:00
( value . nil? || item == :controller ) ? value : CGI . escape ( value . to_s )
2005-02-14 20:45:35 -05:00
else item
end
end
@items . reverse_each do | item | # Remove default components from the end of the generated url.
break unless item . kind_of? ( Symbol ) && @defaults [ item ] == components . last
components . pop
end
# If we have any nil components then we can't proceed.
# This might need to be changed. In some cases we may be able to return all componets after nil as extras.
missing = [ ] ; components . each_with_index { | c , i | missing << @items [ i ] if c . nil? }
return nil , " No values provided for component #{ 's' if missing . length > 1 } #{ missing . join ', ' } but values are required due to use of later components " unless missing . empty? # how wide is your screen?
unused = ( options . keys - used_names . keys ) . inject ( { } ) do | unused , key |
unused [ key ] = options [ key ] if options [ key ] != @defaults [ key ]
unused
end
components . collect! { | c | c . to_s }
return components , unused
end
# Recognize the provided path, returning a hash of recognized values, or [nil, reason] if the path isn't recognized.
# The path should be a list of component strings.
# Options is a hash of the ?k=v pairs
def recognize ( components , options = { } )
options = options . clone
components = components . clone
controller_class = nil
@items . each do | item |
if item == :controller # Special case for controller
if components . empty? && @defaults [ :controller ]
controller_class , leftover = eat_path_to_controller ( @defaults [ :controller ] . split ( '/' ) )
raise RoutingError , " Default controller does not exist: #{ @defaults [ :controller ] } " if controller_class . nil? || leftover . empty? == false
else
controller_class , remaining_components = eat_path_to_controller ( components )
return nil , " No controller found at subpath #{ components . join ( '/' ) } " if controller_class . nil?
components = remaining_components
end
options [ :controller ] = controller_class . controller_path
2005-02-18 20:16:09 -05:00
return nil , requirements_for ( :controller ) unless passes_requirements? ( :controller , options [ :controller ] )
2005-02-14 20:45:35 -05:00
elsif item . kind_of? Symbol
value = components . shift || @defaults [ item ]
2005-02-18 20:16:09 -05:00
return nil , requirements_for ( item ) unless passes_requirements? ( item , value )
2005-02-18 18:52:28 -05:00
options [ item ] = value . nil? ? value : CGI . unescape ( value )
2005-02-14 20:45:35 -05:00
else
return nil , " No value available for component #{ item . inspect } " if components . empty?
component = components . shift
return nil , " Value for component #{ item . inspect } doesn't match #{ component } " if component != item
end
end
if controller_class . nil? && @requirements [ :controller ] # Load a default controller
controller_class , extras = eat_path_to_controller ( @requirements [ :controller ] . split ( '/' ) )
raise RoutingError , " Illegal controller path for route default: #{ @requirements [ :controller ] } " unless controller_class && extras . empty?
options [ :controller ] = controller_class . controller_path
end
2005-02-18 20:16:09 -05:00
@requirements . each { | k , v | options [ k ] || = v unless v . kind_of? ( Regexp ) }
2005-02-14 20:45:35 -05:00
return nil , " Route recognition didn't find a controller class! " unless controller_class
return nil , " Unused components were left: #{ components . join '/' } " unless components . empty?
options . delete_if { | k , v | v . nil? } # Remove nil values.
return controller_class , options
end
def inspect
when_str = @requirements . empty? ? " " : " when #{ @requirements . inspect } "
default_str = @defaults . empty? ? " " : " || #{ @defaults . inspect } "
" < #{ self . class . to_s } #{ @items . collect { | c | c . kind_of? ( String ) ? c : c . inspect } . join ( '/' ) . inspect } #{ default_str } #{ when_str } > "
end
protected
# Find the controller given a list of path components.
# Return the controller class and the unused path components.
def eat_path_to_controller ( path )
path . inject ( [ Controllers , 1 ] ) do | ( mod , length ) , name |
name = name . camelize
controller_name = name + " Controller "
return mod . const_get ( controller_name ) , path [ length .. - 1 ] if mod . const_available? controller_name
return nil , nil unless mod . const_available? name
[ mod . const_get ( name ) , length + 1 ]
end
return nil , nil # Path ended, but no controller found.
end
def items = ( path )
items = path . split ( '/' ) . collect { | c | ( / ^:( \ w+)$ / =~ c ) ? $1 . intern : c } if path . kind_of? ( String ) # split and convert ':xyz' to symbols
items . shift if items . first == " "
items . pop if items . last == " "
@items = items
# Verify uniqueness of each component.
@items . inject ( { } ) do | seen , item |
if item . kind_of? Symbol
raise ArgumentError , " Illegal route path -- duplicate item #{ item } \n #{ path . inspect } " if seen . key? item
seen [ item ] = true
end
seen
end
end
2005-02-18 20:16:09 -05:00
# Verify that the given value passes this route's requirements
def passes_requirements? ( name , value )
return @defaults . key? ( name ) && @defaults [ name ] . nil? if value . nil? # Make sure it's there if it should be
case @requirements [ name ]
when nil then true
when Regexp then
value = value . to_s
match = @requirements [ name ] . match ( value )
match && match [ 0 ] . length == value . length
else
@requirements [ name ] == value . to_s
end
end
def requirements_for ( name )
presence = ( @defaults . key? ( name ) && @defaults [ name ] . nil? )
requirement = case @requirements [ name ]
when nil then nil
when Regexp then " match #{ @requirements [ name ] . inspect } "
else " be equal to #{ @requirements [ name ] . inspect } "
end
if presence && requirement then " #{ name } must be present and #{ requirement } "
elsif presence || requirement then " #{ name } must #{ requirement || 'be present' } "
else " #{ name } has no requirements "
end
end
2005-02-14 20:45:35 -05:00
end
2005-02-23 20:29:43 -05:00
class RouteSet #:nodoc:
2005-02-14 20:45:35 -05:00
def initialize
@routes = [ ]
end
def add_route ( route )
raise TypeError , " #{ route . inspect } is not a Route instance! " unless route . kind_of? ( Route )
@routes << route
end
def empty?
@routes . empty?
end
def each
@routes . each { | route | yield route }
end
# Generate a path for the provided options
# Returns the path as an array of components and a hash of unused names
# Raises RoutingError if not route can handle the provided components.
#
# Note that we don't return the first generated path. We do this so that when a route
# generates a path from a subset of the available options we can keep looking for a
# route which can generate a path that uses more options.
# Note that we *do* return immediately if
def generate ( options , request )
raise RoutingError , " There are no routes defined! " if @routes . empty?
2005-02-20 20:01:24 -05:00
2005-02-14 20:45:35 -05:00
options = options . symbolize_keys
defaults = request . path_parameters . symbolize_keys
2005-02-20 20:09:15 -05:00
options = defaults if options . empty? # Get back the current url if no options was passed
2005-02-14 20:45:35 -05:00
expand_controller_path! ( options , defaults )
2005-02-18 18:53:18 -05:00
defaults . delete_if { | k , v | options . key? ( k ) && options [ k ] . nil? } # Remove defaults that have been manually cleared using :name => nil
2005-02-14 20:45:35 -05:00
failures = [ ]
selected = nil
self . each do | route |
path , unused = route . generate ( options , defaults )
if path . nil?
failures << [ route , unused ] if ActionController :: Base . debug_routes
else
return path , unused if unused . empty? # Found a perfect route -- we're finished.
if selected . nil? || unused . length < selected . last . length
failures << [ selected . first , " A better url than #{ selected [ 1 ] } was found. " ] if selected
selected = [ route , path , unused ]
end
end
end
return selected [ 1 .. - 1 ] unless selected . nil?
raise RoutingError . new ( " Generation failure: No route for url_options #{ options . inspect } , defaults: #{ defaults . inspect } " , failures )
end
# Recognize the provided path.
# Raise RoutingError if the path can't be recognized.
def recognize! ( request )
path = ( ( %r{ ^/?(.*)/?$ } =~ request . path ) ? $1 : request . path ) . split ( '/' )
raise RoutingError , " There are no routes defined! " if @routes . empty?
failures = [ ]
self . each do | route |
controller , options = route . recognize ( path )
if controller . nil?
failures << [ route , options ] if ActionController :: Base . debug_routes
else
2005-02-18 18:52:28 -05:00
request . path_parameters = options
2005-02-14 20:45:35 -05:00
return controller
end
end
raise RoutingError . new ( " No route for path: #{ path . join ( '/' ) . inspect } " , failures )
end
def expand_controller_path! ( options , defaults )
if options [ :controller ]
if / ^ \/ / =~ options [ :controller ]
options [ :controller ] = options [ :controller ] [ 1 .. - 1 ]
defaults . clear # Sending to absolute controller implies fresh defaults
else
relative_to = defaults [ :controller ] ? defaults [ :controller ] . split ( '/' ) [ 0 .. - 2 ] . join ( '/' ) : ''
options [ :controller ] = relative_to . empty? ? options [ :controller ] : " #{ relative_to } / #{ options [ :controller ] } "
2005-02-16 20:55:59 -05:00
defaults . delete ( :action ) if options . key? ( :controller )
2005-02-14 20:45:35 -05:00
end
else
options [ :controller ] = defaults [ :controller ]
end
end
def route ( * args )
add_route ( Route . new ( * args ) )
end
alias :connect :route
def reload
2005-02-16 21:04:37 -05:00
begin
2005-02-17 14:20:44 -05:00
require_dependency ( ROUTE_FILE ) if ROUTE_FILE
2005-02-14 20:45:35 -05:00
rescue LoadError , ScriptError = > e
raise RoutingError , " Cannot load config/routes.rb: \n #{ e . message } "
ensure # Ensure that there is at least one route:
connect ( ':controller/:action/:id' , :action = > 'index' , :id = > nil ) if @routes . empty?
end
end
def draw
@routes . clear
yield self
end
end
def self . draw ( * args , & block )
Routes . draw ( * args ) { | * args | block . call ( * args ) }
end
Routes = RouteSet . new
end
2005-02-17 18:03:48 -05:00
end