1 module ActionController
3 class RouteBuilder
#:nodoc:
4 attr_reader
:separators, :optional_separators
5 attr_reader
:separator_regexp, :nonseparator_regexp, :interval_regexp
8 @separators = Routing
::SEPARATORS
9 @optional_separators = %w( / )
11 @separator_regexp = /[#{Regexp.escape(separators.join)}]/
12 @nonseparator_regexp = /\A([^#{Regexp.escape(separators.join)}]+)/
13 @interval_regexp = /(.*?)(#{separator_regexp}|$)/
16 # Accepts a "route path" (a string defining a route), and returns the array
17 # of segments that corresponds to it. Note that the segment array is only
18 # partially initialized--the defaults and requirements, for instance, need
19 # to be set separately, via the +assign_route_options+ method, and the
20 # <tt>optional?</tt> method for each segment will not be reliable until after
21 # +assign_route_options+ is called, as well.
22 def segments_for_route_path(path
)
23 rest
, segments
= path
, []
26 segment
, rest
= segment_for(rest
)
32 # A factory method that returns a new segment instance appropriate for the
33 # format of the given string.
34 def segment_for(string
)
39 key
== :controller ? ControllerSegment
.new(key
) : DynamicSegment
.new(key
)
41 PathSegment
.new($1.to_sym
, :optional => true)
43 StaticSegment
.new($1, :optional => true)
44 when nonseparator_regexp
47 DividerSegment
.new($
&, :optional => optional_separators
.include?($
&))
49 [segment
, $~
.post_match
]
52 # Split the given hash of options into requirement and default hashes. The
53 # segments are passed alongside in order to distinguish between default values
55 def divide_route_options(segments
, options
)
56 options
= options
.except(:path_prefix, :name_prefix)
58 if options
[:namespace]
59 options
[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}"
62 requirements
= (options
.delete(:requirements) || {}).dup
63 defaults
= (options
.delete(:defaults) || {}).dup
64 conditions
= (options
.delete(:conditions) || {}).dup
66 validate_route_conditions(conditions
)
68 path_keys
= segments
.collect
{ |segment
| segment
.key
if segment
.respond_to
?(:key) }.compact
69 options
.each
do |key
, value
|
70 hash
= (path_keys
.include?(key
) && ! value
.is_a
?(Regexp
)) ? defaults
: requirements
74 [defaults
, requirements
, conditions
]
77 # Takes a hash of defaults and a hash of requirements, and assigns them to
78 # the segments. Any unused requirements (which do not correspond to a segment)
79 # are returned as a hash.
80 def assign_route_options(segments
, defaults
, requirements
)
81 route_requirements
= {} # Requirements that do not belong to a segment
83 segment_named
= Proc
.new
do |key
|
84 segments
.detect
{ |segment
| segment
.key
== key
if segment
.respond_to
?(:key) }
87 requirements
.each
do |key
, requirement
|
88 segment
= segment_named
[key
]
90 raise TypeError
, "#{key}: requirements on a path segment must be regular expressions" unless requirement
.is_a
?(Regexp
)
91 if requirement
.source
=~
%r
{\
A(\\A
|\^
)|(\\Z
|\\z
|\$
)\Z
}
92 raise ArgumentError
, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
94 if requirement
.multiline
?
95 raise ArgumentError
, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
97 segment
.regexp
= requirement
99 route_requirements
[key
] = requirement
103 defaults
.each
do |key
, default
|
104 segment
= segment_named
[key
]
105 raise ArgumentError
, "#{key}: No matching segment exists; cannot assign default" unless segment
106 segment
.is_optional
= true
107 segment
.default
= default
.to_param
if default
110 assign_default_route_options(segments
)
111 ensure_required_segments(segments
)
115 # Assign default options, such as 'index' as a default for <tt>:action</tt>. This
116 # method must be run *after* user supplied requirements and defaults have
117 # been applied to the segments.
118 def assign_default_route_options(segments
)
119 segments
.each
do |segment
|
120 next unless segment
.is_a
? DynamicSegment
123 if segment
.regexp
.nil? || segment
.regexp
.match('index').to_s
== 'index'
124 segment
.default
||= 'index'
125 segment
.is_optional
= true
128 if segment
.default
.nil? && segment
.regexp
.nil? || segment
.regexp
=~
''
129 segment
.is_optional
= true
135 # Makes sure that there are no optional segments that precede a required
136 # segment. If any are found that precede a required segment, they are
138 def ensure_required_segments(segments
)
139 allow_optional
= true
140 segments
.reverse_each
do |segment
|
141 allow_optional
&&= segment
.optional
?
142 if !allow_optional
&& segment
.optional
?
143 unless segment
.optionality_implied
?
144 warn
"Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
146 segment
.is_optional
= false
147 elsif allow_optional
&& segment
.respond_to
?(:default) && segment
.default
148 # if a segment has a default, then it is optional
149 segment
.is_optional
= true
154 # Construct and return a route with the given path and options.
155 def build(path
, options
)
156 # Wrap the path with slashes
157 path
= "/#{path}" unless path
[0] == ?/
158 path
= "#{path}/" unless path
[-1] == ?/
160 path
= "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options
[:path_prefix]
162 segments
= segments_for_route_path(path
)
163 defaults
, requirements
, conditions
= divide_route_options(segments
, options
)
164 requirements
= assign_route_options(segments
, defaults
, requirements
)
166 # TODO: Segments should be frozen on initialize
167 segments
.each
{ |segment
| segment
.freeze
}
169 route
= Route
.new(segments
, requirements
, conditions
)
171 if !route
.significant_keys
.include?(:controller)
172 raise ArgumentError
, "Illegal route: the :controller must be specified!"
179 def validate_route_conditions(conditions
)
180 if method
= conditions
[:method]
181 [method
].flatten
.each
do |m
|
183 raise ArgumentError
, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
186 unless HTTP_METHODS
.include?(m
.to_sym
)
187 raise ArgumentError
, "Invalid HTTP method specified in route conditions: #{conditions.inspect}"