d9590c88b8aa52b284d482d9056ffb195b02c632
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / routing / builder.rb
1 module ActionController
2 module Routing
3 class RouteBuilder #:nodoc:
4 attr_reader :separators, :optional_separators
5 attr_reader :separator_regexp, :nonseparator_regexp, :interval_regexp
6
7 def initialize
8 @separators = Routing::SEPARATORS
9 @optional_separators = %w( / )
10
11 @separator_regexp = /[#{Regexp.escape(separators.join)}]/
12 @nonseparator_regexp = /\A([^#{Regexp.escape(separators.join)}]+)/
13 @interval_regexp = /(.*?)(#{separator_regexp}|$)/
14 end
15
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, []
24
25 until rest.empty?
26 segment, rest = segment_for(rest)
27 segments << segment
28 end
29 segments
30 end
31
32 # A factory method that returns a new segment instance appropriate for the
33 # format of the given string.
34 def segment_for(string)
35 segment =
36 case string
37 when /\A\.(:format)?\//
38 OptionalFormatSegment.new
39 when /\A:(\w+)/
40 key = $1.to_sym
41 key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key)
42 when /\A\*(\w+)/
43 PathSegment.new($1.to_sym, :optional => true)
44 when /\A\?(.*?)\?/
45 StaticSegment.new($1, :optional => true)
46 when nonseparator_regexp
47 StaticSegment.new($1)
48 when separator_regexp
49 DividerSegment.new($&, :optional => optional_separators.include?($&))
50 end
51 [segment, $~.post_match]
52 end
53
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
56 # and requirements.
57 def divide_route_options(segments, options)
58 options = options.except(:path_prefix, :name_prefix)
59
60 if options[:namespace]
61 options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}"
62 end
63
64 requirements = (options.delete(:requirements) || {}).dup
65 defaults = (options.delete(:defaults) || {}).dup
66 conditions = (options.delete(:conditions) || {}).dup
67
68 validate_route_conditions(conditions)
69
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
73 hash[key] = value
74 end
75
76 [defaults, requirements, conditions]
77 end
78
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
84
85 segment_named = Proc.new do |key|
86 segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
87 end
88
89 requirements.each do |key, requirement|
90 segment = segment_named[key]
91 if segment
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}"
95 end
96 if requirement.multiline?
97 raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
98 end
99 segment.regexp = requirement
100 else
101 route_requirements[key] = requirement
102 end
103 end
104
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
110 end
111
112 assign_default_route_options(segments)
113 ensure_required_segments(segments)
114 route_requirements
115 end
116
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
123 case segment.key
124 when :action
125 if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
126 segment.default ||= 'index'
127 segment.is_optional = true
128 end
129 when :id
130 if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
131 segment.is_optional = true
132 end
133 end
134 end
135 end
136
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
139 # made required.
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."
147 end
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
152 end
153 end
154 end
155
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] == ?/
161
162 prefix = options[:path_prefix].to_s.gsub(/^\//,'')
163 path = "/#{prefix}#{path}" unless prefix.blank?
164
165 segments = segments_for_route_path(path)
166 defaults, requirements, conditions = divide_route_options(segments, options)
167 requirements = assign_route_options(segments, defaults, requirements)
168
169 # TODO: Segments should be frozen on initialize
170 segments.each { |segment| segment.freeze }
171
172 route = Route.new(segments, requirements, conditions)
173
174 if !route.significant_keys.include?(:controller)
175 raise ArgumentError, "Illegal route: the :controller must be specified!"
176 end
177
178 route.freeze
179 end
180
181 private
182 def validate_route_conditions(conditions)
183 if method = conditions[:method]
184 [method].flatten.each do |m|
185 if m == :head
186 raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
187 end
188
189 unless HTTP_METHODS.include?(m.to_sym)
190 raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}"
191 end
192 end
193 end
194 end
195 end
196 end
197 end