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

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

Revision 9115, 8.5 kB (checked in by david, 5 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 Segment #:nodoc:
4       RESERVED_PCHAR = ':@&=+$,;'
5       UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
6
7       attr_accessor :is_optional
8       alias_method :optional?, :is_optional
9
10       def initialize
11         self.is_optional = false
12       end
13
14       def extraction_code
15         nil
16       end
17
18       # Continue generating string for the prior segments.
19       def continue_string_structure(prior_segments)
20         if prior_segments.empty?
21           interpolation_statement(prior_segments)
22         else
23           new_priors = prior_segments[0..-2]
24           prior_segments.last.string_structure(new_priors)
25         end
26       end
27
28       def interpolation_chunk
29         URI.escape(value, UNSAFE_PCHAR)
30       end
31
32       # Return a string interpolation statement for this segment and those before it.
33       def interpolation_statement(prior_segments)
34         chunks = prior_segments.collect { |s| s.interpolation_chunk }
35         chunks << interpolation_chunk
36         "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
37       end
38
39       def string_structure(prior_segments)
40         optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
41       end
42
43       # Return an if condition that is true if all the prior segments can be generated.
44       # If there are no optional segments before this one, then nil is returned.
45       def all_optionals_available_condition(prior_segments)
46         optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
47         optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
48       end
49
50       # Recognition
51
52       def match_extraction(next_capture)
53         nil
54       end
55
56       # Warning
57
58       # Returns true if this segment is optional? because of a default. If so, then
59       # no warning will be emitted regarding this segment.
60       def optionality_implied?
61         false
62       end
63     end
64
65     class StaticSegment < Segment #:nodoc:
66       attr_accessor :value, :raw
67       alias_method :raw?, :raw
68
69       def initialize(value = nil)
70         super()
71         self.value = value
72       end
73
74       def interpolation_chunk
75         raw? ? value : super
76       end
77
78       def regexp_chunk
79         chunk = Regexp.escape(value)
80         optional? ? Regexp.optionalize(chunk) : chunk
81       end
82
83       def build_pattern(pattern)
84         escaped = Regexp.escape(value)
85         if optional? && ! pattern.empty?
86           "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
87         elsif optional?
88           Regexp.optionalize escaped
89         else
90           escaped + pattern
91         end
92       end
93
94       def to_s
95         value
96       end
97     end
98
99     class DividerSegment < StaticSegment #:nodoc:
100       def initialize(value = nil)
101         super(value)
102         self.raw = true
103         self.is_optional = true
104       end
105
106       def optionality_implied?
107         true
108       end
109     end
110
111     class DynamicSegment < Segment #:nodoc:
112       attr_accessor :key, :default, :regexp
113
114       def initialize(key = nil, options = {})
115         super()
116         self.key = key
117         self.default = options[:default] if options.key? :default
118         self.is_optional = true if options[:optional] || options.key?(:default)
119       end
120
121       def to_s
122         ":#{key}"
123       end
124
125       # The local variable name that the value of this segment will be extracted to.
126       def local_name
127         "#{key}_value"
128       end
129
130       def extract_value
131         "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
132       end
133       def value_check
134         if default # Then we know it won't be nil
135           "#{value_regexp.inspect} =~ #{local_name}" if regexp
136         elsif optional?
137           # If we have a regexp check that the value is not given, or that it matches.
138           # If we have no regexp, return nil since we do not require a condition.
139           "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
140         else # Then it must be present, and if we have a regexp, it must match too.
141           "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
142         end
143       end
144       def expiry_statement
145         "expired, hash = true, options if !expired && expire_on[:#{key}]"
146       end
147
148       def extraction_code
149         s = extract_value
150         vc = value_check
151         s << "\nreturn [nil,nil] unless #{vc}" if vc
152         s << "\n#{expiry_statement}"
153       end
154
155       def interpolation_chunk(value_code = "#{local_name}")
156         "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
157       end
158
159       def string_structure(prior_segments)
160         if optional? # We have a conditional to do...
161           # If we should not appear in the url, just write the code for the prior
162           # segments. This occurs if our value is the default value, or, if we are
163           # optional, if we have nil as our value.
164           "if #{local_name} == #{default.inspect}\n" +
165             continue_string_structure(prior_segments) +
166           "\nelse\n" + # Otherwise, write the code up to here
167             "#{interpolation_statement(prior_segments)}\nend"
168         else
169           interpolation_statement(prior_segments)
170         end
171       end
172
173       def value_regexp
174         Regexp.new "\\A#{regexp.to_s}\\Z" if regexp
175       end
176
177       def regexp_chunk
178         if regexp
179           if regexp_has_modifiers?
180             "(#{regexp.to_s})"
181           else
182             "(#{regexp.source})"
183           end
184         else
185           "([^#{Routing::SEPARATORS.join}]+)"
186         end
187       end
188
189       def build_pattern(pattern)
190         chunk = regexp_chunk
191         chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0
192         pattern = "#{chunk}#{pattern}"
193         optional? ? Regexp.optionalize(pattern) : pattern
194       end
195
196       def match_extraction(next_capture)
197         # All non code-related keys (such as :id, :slug) are URI-unescaped as
198         # path parameters.
199         default_value = default ? default.inspect : nil
200         %[
201           value = if (m = match[#{next_capture}])
202             URI.unescape(m)
203           else
204             #{default_value}
205           end
206           params[:#{key}] = value if value
207         ]
208       end
209
210       def optionality_implied?
211         [:action, :id].include? key
212       end
213
214       def regexp_has_modifiers?
215         regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
216       end
217
218     end
219
220     class ControllerSegment < DynamicSegment #:nodoc:
221       def regexp_chunk
222         possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
223         "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
224       end
225
226       # Don't URI.escape the controller name since it may contain slashes.
227       def interpolation_chunk(value_code = "#{local_name}")
228         "\#{#{value_code}.to_s}"
229       end
230
231       # Make sure controller names like Admin/Content are correctly normalized to
232       # admin/content
233       def extract_value
234         "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
235       end
236
237       def match_extraction(next_capture)
238         if default
239           "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
240         else
241           "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
242         end
243       end
244     end
245
246     class PathSegment < DynamicSegment #:nodoc:
247       RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/"
248       UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
249
250       def interpolation_chunk(value_code = "#{local_name}")
251         "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}"
252       end
253
254       def default
255         ''
256       end
257
258       def default=(path)
259         raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
260       end
261
262       def match_extraction(next_capture)
263         "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
264       end
265
266       def regexp_chunk
267         regexp || "(.*)"
268       end
269
270       def optionality_implied?
271         true
272       end
273
274       class Result < ::Array #:nodoc:
275         def to_s() join '/' end
276         def self.new_escaped(strings)
277           new strings.collect {|str| URI.unescape str}
278         end
279       end
280     end
281   end
282 end
Note: See TracBrowser for help on using the browser.