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