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}"