Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/routing/builder.rb

Revision 9115, 7.8 kB (checked in by david, 3 months ago)

Added support for regexp flags like ignoring case in the :requirements part of routes declarations (closes #11421) [NeilW]

Line 
1 module ActionController
2   module Routing
3     class RouteBuilder #:nodoc:
4       attr_accessor :separators, :optional_separators
5
6       def initialize
7         self.separators = Routing::SEPARATORS
8         self.optional_separators = %w( / )
9       end
10
11       def separator_pattern(inverted = false)
12         "[#{'^' if inverted}#{Regexp.escape(separators.join)}]"
13       end
14
15       def interval_regexp
16         Regexp.new "(.*?)(#{separators.source}|$)"
17       end
18
19       def multiline_regexp?(expression)
20         expression.options & Regexp::MULTILINE == Regexp::MULTILINE
21       end
22
23       # Accepts a "route path" (a string defining a route), and returns the array
24       # of segments that corresponds to it. Note that the segment array is only
25       # partially initialized--the defaults and requirements, for instance, need
26       # to be set separately, via the #assign_route_options method, and the
27       # #optional? method for each segment will not be reliable until after
28       # #assign_route_options is called, as well.
29       def segments_for_route_path(path)
30         rest, segments = path, []
31
32         until rest.empty?
33           segment, rest = segment_for rest
34           segments << segment
35         end
36         segments
37       end
38
39       # A factory method that returns a new segment instance appropriate for the
40       # format of the given string.
41       def segment_for(string)
42         segment = case string
43           when /\A:(\w+)/
44             key = $1.to_sym
45             case key
46               when :controller then ControllerSegment.new(key)
47               else DynamicSegment.new key
48             end
49           when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
50           when /\A\?(.*?)\?/
51             returning segment = StaticSegment.new($1) do
52               segment.is_optional = true
53             end
54           when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
55           when Regexp.new(separator_pattern) then
56             returning segment = DividerSegment.new($&) do
57               segment.is_optional = (optional_separators.include? $&)
58             end
59         end
60         [segment, $~.post_match]
61       end
62
63       # Split the given hash of options into requirement and default hashes. The
64       # segments are passed alongside in order to distinguish between default values
65       # and requirements.
66       def divide_route_options(segments, options)
67         options = options.dup
68
69         if options[:namespace]
70           options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}"
71           options.delete(:path_prefix)
72           options.delete(:name_prefix)
73           options.delete(:namespace)
74         end
75
76         requirements = (options.delete(:requirements) || {}).dup
77         defaults     = (options.delete(:defaults)     || {}).dup
78         conditions   = (options.delete(:conditions)   || {}).dup
79
80         path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
81         options.each do |key, value|
82           hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
83           hash[key] = value
84         end
85
86         [defaults, requirements, conditions]
87       end
88
89       # Takes a hash of defaults and a hash of requirements, and assigns them to
90       # the segments. Any unused requirements (which do not correspond to a segment)
91       # are returned as a hash.
92       def assign_route_options(segments, defaults, requirements)
93         route_requirements = {} # Requirements that do not belong to a segment
94
95         segment_named = Proc.new do |key|
96           segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
97         end
98
99         requirements.each do |key, requirement|
100           segment = segment_named[key]
101           if segment
102             raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
103             if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
104               raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
105             end
106             if multiline_regexp?(requirement)
107               raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
108             end
109             segment.regexp = requirement
110           else
111             route_requirements[key] = requirement
112           end
113         end
114
115         defaults.each do |key, default|
116           segment = segment_named[key]
117           raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
118           segment.is_optional = true
119           segment.default = default.to_param if default
120         end
121
122         assign_default_route_options(segments)
123         ensure_required_segments(segments)
124         route_requirements
125       end
126
127       # Assign default options, such as 'index' as a default for :action. This
128       # method must be run *after* user supplied requirements and defaults have
129       # been applied to the segments.
130       def assign_default_route_options(segments)
131         segments.each do |segment|
132           next unless segment.is_a? DynamicSegment
133           case segment.key
134             when :action
135               if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
136                 segment.default ||= 'index'
137                 segment.is_optional = true
138               end
139             when :id
140               if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
141                 segment.is_optional = true
142               end
143           end
144         end
145       end
146
147       # Makes sure that there are no optional segments that precede a required
148       # segment. If any are found that precede a required segment, they are
149       # made required.
150       def ensure_required_segments(segments)
151         allow_optional = true
152         segments.reverse_each do |segment|
153           allow_optional &&= segment.optional?
154           if !allow_optional && segment.optional?
155             unless segment.optionality_implied?
156               warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
157             end
158             segment.is_optional = false
159           elsif allow_optional && segment.respond_to?(:default) && segment.default
160             # if a segment has a default, then it is optional
161             segment.is_optional = true
162           end
163         end
164       end
165
166       # Construct and return a route with the given path and options.
167       def build(path, options)
168         # Wrap the path with slashes
169         path = "/#{path}" unless path[0] == ?/
170         path = "#{path}/" unless path[-1] == ?/
171
172         path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]
173
174         segments = segments_for_route_path(path)
175         defaults, requirements, conditions = divide_route_options(segments, options)
176         requirements = assign_route_options(segments, defaults, requirements)
177
178         route = Route.new
179
180         route.segments = segments
181         route.requirements = requirements
182         route.conditions = conditions
183
184         if !route.significant_keys.include?(:action) && !route.requirements[:action]
185           route.requirements[:action] = "index"
186           route.significant_keys << :action
187         end
188
189         # Routes cannot use the current string interpolation method
190         # if there are user-supplied :requirements as the interpolation
191         # code won't raise RoutingErrors when generating
192         if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
193           route.optimise = false
194         end
195
196         if !route.significant_keys.include?(:controller)
197           raise ArgumentError, "Illegal route: the :controller must be specified!"
198         end
199
200         route
201       end
202     end
203   end
204 end
Note: See TracBrowser for help on using the browser.