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
class Route
attr_reader :defaults # The defaults hash
def initialize ( path , hash = { } )
raise ArgumentError , " Second argument must be a hash! " unless hash . kind_of? ( Hash )
@defaults = { }
@requirements = { }
self . items = path
hash . each do | k , v |
raise TypeError , " Hash may only contain symbols! " unless k . kind_of? Symbol
( @items . include? ( k ) ? @defaults : @requirements ) [ k ] = v
end
# 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 = { } )
non_matching = @requirements . inject ( [ ] ) { | a , ( k , v ) | ( ( options [ k ] || defaults [ k ] ) == v ) ? a : a << k }
return nil , " Options mismatch requirements: #{ non_matching . join ', ' } " unless non_matching . empty?
used_names = @requirements . inject ( { } ) { | hash , ( k , v ) | hash [ k ] = true ; hash }
components = @items . collect do | item |
if item . kind_of? Symbol
used_names [ item ] = true
value = options [ item ] || defaults [ item ] || @defaults [ item ]
return nil , " #{ item . inspect } was not given and has no default. " if value . nil? && ! ( @defaults . key? ( item ) && @defaults [ item ] . nil? ) # Don't leave if nil value.
defaults = { } unless defaults == { } || value == defaults [ item ] # Stop using defaults if this component isn't the same as the default.
value
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 }
components . unshift ( components . shift + '/' ) if components . length == 1 && @items . first == :controller # Add '/' to controllers
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
elsif item . kind_of? Symbol
value = components . shift || @defaults [ item ]
return nil , " No value or default for parameter #{ item . inspect } " if value . nil? && ! ( @defaults . key? ( item ) && @defaults [ item ] . nil? )
options [ item ] = value
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
options = @requirements . merge ( options )
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
end
class RouteSet
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?
options = options . symbolize_keys
defaults = request . path_parameters . symbolize_keys
expand_controller_path! ( options , defaults )
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
options . each { | k , v | request . path_parameters [ k ] = CGI . unescape ( v ) }
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-16 21:14:32 -05:00
require_dependency ( ROUTE_FILE )
rescue NoMethodError
# ignore for unit tests
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
2005-02-16 20:55:59 -05:00
Routes . reload # Server will die on load if SyntaxError
2005-02-14 20:45:35 -05:00
end
end