2008-05-18 06:22:34 +02:00
module ActionController
module Routing
class RouteBuilder #:nodoc:
2008-11-24 22:53:39 +01:00
attr_reader :separators , :optional_separators
attr_reader :separator_regexp , :nonseparator_regexp , :interval_regexp
2008-05-18 06:22:34 +02:00
def initialize
2008-11-24 22:53:39 +01:00
@separators = Routing :: SEPARATORS
@optional_separators = %w( / )
2008-05-18 06:22:34 +02:00
2008-11-24 22:53:39 +01:00
@separator_regexp = / [ #{ Regexp . escape ( separators . join ) } ] /
@nonseparator_regexp = / \ A([^ #{ Regexp . escape ( separators . join ) } ]+) /
@interval_regexp = / (.*?)( #{ separator_regexp } |$) /
2008-05-18 06:22:34 +02:00
end
# Accepts a "route path" (a string defining a route), and returns the array
# of segments that corresponds to it. Note that the segment array is only
# partially initialized--the defaults and requirements, for instance, need
2008-06-02 08:35:38 +02:00
# to be set separately, via the +assign_route_options+ method, and the
# <tt>optional?</tt> method for each segment will not be reliable until after
# +assign_route_options+ is called, as well.
2008-05-18 06:22:34 +02:00
def segments_for_route_path ( path )
rest , segments = path , [ ]
until rest . empty?
2008-11-24 22:53:39 +01:00
segment , rest = segment_for ( rest )
2008-05-18 06:22:34 +02:00
segments << segment
end
segments
end
# A factory method that returns a new segment instance appropriate for the
# format of the given string.
def segment_for ( string )
2008-11-24 22:53:39 +01:00
segment =
case string
2009-02-04 21:26:08 +01:00
when / \ A \ .(:format)? \/ /
OptionalFormatSegment . new
2008-11-24 22:53:39 +01:00
when / \ A:( \ w+) /
key = $1 . to_sym
key == :controller ? ControllerSegment . new ( key ) : DynamicSegment . new ( key )
when / \ A \ *( \ w+) /
PathSegment . new ( $1 . to_sym , :optional = > true )
when / \ A \ ?(.*?) \ ? /
StaticSegment . new ( $1 , :optional = > true )
when nonseparator_regexp
StaticSegment . new ( $1 )
when separator_regexp
DividerSegment . new ( $& , :optional = > optional_separators . include? ( $& ) )
end
2008-05-18 06:22:34 +02:00
[ segment , $~ . post_match ]
end
# Split the given hash of options into requirement and default hashes. The
# segments are passed alongside in order to distinguish between default values
# and requirements.
def divide_route_options ( segments , options )
2008-10-27 07:47:01 +01:00
options = options . except ( :path_prefix , :name_prefix )
2008-05-18 06:22:34 +02:00
if options [ :namespace ]
2008-09-07 07:54:05 +02:00
options [ :controller ] = " #{ options . delete ( :namespace ) . sub ( / \/ $ / , '' ) } / #{ options [ :controller ] } "
2008-05-18 06:22:34 +02:00
end
requirements = ( options . delete ( :requirements ) || { } ) . dup
defaults = ( options . delete ( :defaults ) || { } ) . dup
conditions = ( options . delete ( :conditions ) || { } ) . dup
2008-10-27 07:47:01 +01:00
validate_route_conditions ( conditions )
2008-05-18 06:22:34 +02:00
path_keys = segments . collect { | segment | segment . key if segment . respond_to? ( :key ) } . compact
options . each do | key , value |
hash = ( path_keys . include? ( key ) && ! value . is_a? ( Regexp ) ) ? defaults : requirements
hash [ key ] = value
end
[ defaults , requirements , conditions ]
end
# Takes a hash of defaults and a hash of requirements, and assigns them to
# the segments. Any unused requirements (which do not correspond to a segment)
# are returned as a hash.
def assign_route_options ( segments , defaults , requirements )
route_requirements = { } # Requirements that do not belong to a segment
segment_named = Proc . new do | key |
segments . detect { | segment | segment . key == key if segment . respond_to? ( :key ) }
end
requirements . each do | key , requirement |
segment = segment_named [ key ]
if segment
raise TypeError , " #{ key } : requirements on a path segment must be regular expressions " unless requirement . is_a? ( Regexp )
if requirement . source =~ %r{ \ A( \\ A| \ ^)|( \\ Z| \\ z| \ $) \ Z }
raise ArgumentError , " Regexp anchor characters are not allowed in routing requirements: #{ requirement . inspect } "
end
2008-11-24 22:53:39 +01:00
if requirement . multiline?
2008-05-18 06:22:34 +02:00
raise ArgumentError , " Regexp multiline option not allowed in routing requirements: #{ requirement . inspect } "
end
segment . regexp = requirement
else
route_requirements [ key ] = requirement
end
end
defaults . each do | key , default |
segment = segment_named [ key ]
raise ArgumentError , " #{ key } : No matching segment exists; cannot assign default " unless segment
segment . is_optional = true
segment . default = default . to_param if default
end
assign_default_route_options ( segments )
ensure_required_segments ( segments )
route_requirements
end
# Assign default options, such as 'index' as a default for <tt>:action</tt>. This
# method must be run *after* user supplied requirements and defaults have
# been applied to the segments.
def assign_default_route_options ( segments )
segments . each do | segment |
next unless segment . is_a? DynamicSegment
case segment . key
when :action
if segment . regexp . nil? || segment . regexp . match ( 'index' ) . to_s == 'index'
segment . default || = 'index'
segment . is_optional = true
end
when :id
if segment . default . nil? && segment . regexp . nil? || segment . regexp =~ ''
segment . is_optional = true
end
end
end
end
# Makes sure that there are no optional segments that precede a required
# segment. If any are found that precede a required segment, they are
# made required.
def ensure_required_segments ( segments )
allow_optional = true
segments . reverse_each do | segment |
allow_optional && = segment . optional?
if ! allow_optional && segment . optional?
unless segment . optionality_implied?
warn " Route segment \" #{ segment . to_s } \" cannot be optional because it precedes a required segment. This segment will be required. "
end
segment . is_optional = false
elsif allow_optional && segment . respond_to? ( :default ) && segment . default
# if a segment has a default, then it is optional
segment . is_optional = true
end
end
end
# Construct and return a route with the given path and options.
def build ( path , options )
# Wrap the path with slashes
path = " / #{ path } " unless path [ 0 ] == ?/
path = " #{ path } / " unless path [ - 1 ] == ?/
path = " / #{ options [ :path_prefix ] . to_s . gsub ( / ^ \/ / , '' ) } #{ path } " if options [ :path_prefix ]
segments = segments_for_route_path ( path )
defaults , requirements , conditions = divide_route_options ( segments , options )
requirements = assign_route_options ( segments , defaults , requirements )
2008-10-27 07:47:01 +01:00
# TODO: Segments should be frozen on initialize
segments . each { | segment | segment . freeze }
2008-05-18 06:22:34 +02:00
2008-10-27 07:47:01 +01:00
route = Route . new ( segments , requirements , conditions )
2008-05-18 06:22:34 +02:00
if ! route . significant_keys . include? ( :controller )
raise ArgumentError , " Illegal route: the :controller must be specified! "
end
2008-10-27 07:47:01 +01:00
route . freeze
2008-05-18 06:22:34 +02:00
end
2008-10-27 07:47:01 +01:00
private
def validate_route_conditions ( conditions )
if method = conditions [ :method ]
[ method ] . flatten . each do | m |
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 . to_sym )
raise ArgumentError , " Invalid HTTP method specified in route conditions: #{ conditions . inspect } "
end
end
end
end
2008-05-18 06:22:34 +02:00
end
end
end