d9590c88b8aa52b284d482d9056ffb195b02c632
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
)
37 when /\A\.(:format)?\//
38 OptionalFormatSegment
.new
41 key
== :controller ? ControllerSegment
.new(key
) : DynamicSegment
.new(key
)
43 PathSegment
.new($1.to_sym
, :optional => true)
45 StaticSegment
.new($1, :optional => true)
46 when nonseparator_regexp
49 DividerSegment
.new($
&, :optional => optional_separators
.include?($
&))
51 [segment
, $~
.post_match
]
54 # Split the given hash of options into requirement and default hashes. The
55 # segments are passed alongside in order to distinguish between default values
57 def divide_route_options(segments
, options
)
58 options
= options
.except(:path_prefix, :name_prefix)
60 if options
[:namespace]
61 options
[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}"
64 requirements
= (options
.delete(:requirements) || {}).dup
65 defaults
= (options
.delete(:defaults) || {}).dup
66 conditions
= (options
.delete(:conditions) || {}).dup
68 validate_route_conditions(conditions
)
70 path_keys
= segments
.collect
{ |segment
| segment
.key
if segment
.respond_to
?(:key) }.compact
71 options
.each
do |key
, value
|
72 hash
= (path_keys
.include?(key
) && ! value
.is_a
?(Regexp
)) ? defaults
: requirements
76 [defaults
, requirements
, conditions
]
79 # Takes a hash of defaults and a hash of requirements, and assigns them to
80 # the segments. Any unused requirements (which do not correspond to a segment)
81 # are returned as a hash.
82 def assign_route_options(segments
, defaults
, requirements
)
83 route_requirements
= {} # Requirements that do not belong to a segment
85 segment_named
= Proc
.new
do |key
|
86 segments
.detect
{ |segment
| segment
.key
== key
if segment
.respond_to
?(:key) }
89 requirements
.each
do |key
, requirement
|
90 segment
= segment_named
[key
]
92 raise TypeError
, "#{key}: requirements on a path segment must be regular expressions" unless requirement
.is_a
?(Regexp
)
93 if requirement
.source
=~
%r
{\
A(\\A
|\^
)|(\\Z
|\\z
|\$
)\Z
}
94 raise ArgumentError
, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
96 if requirement
.multiline
?
97 raise ArgumentError
, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
99 segment
.regexp
= requirement
101 route_requirements
[key
] = requirement
105 defaults
.each
do |key
, default
|
106 segment
= segment_named
[key
]
107 raise ArgumentError
, "#{key}: No matching segment exists; cannot assign default" unless segment
108 segment
.is_optional
= true
109 segment
.default
= default
.to_param
if default
112 assign_default_route_options(segments
)
113 ensure_required_segments(segments
)
117 # Assign default options, such as 'index' as a default for <tt>:action</tt>. This
118 # method must be run *after* user supplied requirements and defaults have
119 # been applied to the segments.
120 def assign_default_route_options(segments
)
121 segments
.each
do |segment
|
122 next unless segment
.is_a
? DynamicSegment
125 if segment
.regexp
.nil? || segment
.regexp
.match('index').to_s
== 'index'
126 segment
.default
||= 'index'
127 segment
.is_optional
= true
130 if segment
.default
.nil? && segment
.regexp
.nil? || segment
.regexp
=~
''
131 segment
.is_optional
= true
137 # Makes sure that there are no optional segments that precede a required
138 # segment. If any are found that precede a required segment, they are
140 def ensure_required_segments(segments
)
141 allow_optional
= true
142 segments
.reverse_each
do |segment
|
143 allow_optional
&&= segment
.optional
?
144 if !allow_optional
&& segment
.optional
?
145 unless segment
.optionality_implied
?
146 warn
"Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
148 segment
.is_optional
= false
149 elsif allow_optional
&& segment
.respond_to
?(:default) && segment
.default
150 # if a segment has a default, then it is optional
151 segment
.is_optional
= true
156 # Construct and return a route with the given path and options.
157 def build(path
, options
)
158 # Wrap the path with slashes
159 path
= "/#{path}" unless path
[0] == ?/
160 path
= "#{path}/" unless path
[-1] == ?/
162 prefix
= options
[:path_prefix].to_s
.gsub(/^\//,'')
163 path
= "/#{prefix}#{path}" unless prefix
.blank
?
165 segments
= segments_for_route_path(path
)
166 defaults
, requirements
, conditions
= divide_route_options(segments
, options
)
167 requirements
= assign_route_options(segments
, defaults
, requirements
)
169 # TODO: Segments should be frozen on initialize
170 segments
.each
{ |segment
| segment
.freeze
}
172 route
= Route
.new(segments
, requirements
, conditions
)
174 if !route
.significant_keys
.include?(:controller)
175 raise ArgumentError
, "Illegal route: the :controller must be specified!"
182 def validate_route_conditions(conditions
)
183 if method
= conditions
[:method]
184 [method
].flatten
.each
do |m
|
186 raise ArgumentError
, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
189 unless HTTP_METHODS
.include?(m
.to_sym
)
190 raise ArgumentError
, "Invalid HTTP method specified in route conditions: #{conditions.inspect}"