1 module ActionController
4 RESERVED_PCHAR
= ':@&=+$
,;'
5 SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
6 UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N
').freeze
8 # TODO: Convert :is_optional accessor to read only
9 attr_accessor :is_optional
10 alias_method :optional?, :is_optional
16 def number_of_captures
17 Regexp.new(regexp_chunk).number_of_captures
24 # Continue generating string for the prior segments.
25 def continue_string_structure(prior_segments)
26 if prior_segments.empty?
27 interpolation_statement(prior_segments)
29 new_priors = prior_segments[0..-2]
30 prior_segments.last.string_structure(new_priors)
34 def interpolation_chunk
35 URI.escape(value, UNSAFE_PCHAR)
38 # Return a string interpolation statement for this segment and those before it.
39 def interpolation_statement(prior_segments)
40 chunks = prior_segments.collect { |s| s.interpolation_chunk }
41 chunks << interpolation_chunk
42 "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
45 def string_structure(prior_segments)
46 optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
49 # Return an if condition that is true if all the prior segments can be generated.
50 # If there are no optional segments before this one, then nil is returned.
51 def all_optionals_available_condition(prior_segments)
52 optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
53 optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
58 def match_extraction(next_capture)
64 # Returns true if this segment is optional? because of a default. If so, then
65 # no warning will be emitted regarding this segment.
66 def optionality_implied?
71 class StaticSegment < Segment #:nodoc:
72 attr_reader :value, :raw
73 alias_method :raw?, :raw
75 def initialize(value = nil, options = {})
78 @raw = options[:raw] if options.key?(:raw)
79 @is_optional = options[:optional] if options.key?(:optional)
82 def interpolation_chunk
87 chunk = Regexp.escape(value)
88 optional? ? Regexp.optionalize(chunk) : chunk
91 def number_of_captures
95 def build_pattern(pattern)
96 escaped = Regexp.escape(value)
97 if optional? && ! pattern
.empty
?
98 "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
100 Regexp
.optionalize escaped
111 class DividerSegment
< StaticSegment
#:nodoc:
112 def initialize(value
= nil, options
= {})
113 super(value
, {:raw => true, :optional => true}.merge(options
))
116 def optionality_implied
?
121 class DynamicSegment
< Segment
#:nodoc:
124 # TODO: Convert these accessors to read only
125 attr_accessor
:default, :regexp
127 def initialize(key
= nil, options
= {})
130 @default = options
[:default] if options
.key
?(:default)
131 @regexp = options
[:regexp] if options
.key
?(:regexp)
132 @is_optional = true if options
[:optional] || options
.key
?(:default)
139 # The local variable name that the value of this segment will be extracted to.
145 "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default
}"
149 if default # Then we know it won't be nil
150 "#{value_regexp.inspect} =~
#{local_name}" if regexp
152 # If we have a regexp check that the value is not given, or that it matches.
153 # If we have no regexp, return nil since we do not require a condition.
154 "#{local_name}.nil? || #{value_regexp.inspect} =~
#{local_name}" if regexp
155 else # Then it must be present, and if we have a regexp, it must match too.
156 "#{local_name} #{"&& #{value_regexp.inspect} =~
#{local_name}" if regexp}"
161 "expired, hash = true, options if !expired
&& expire_on
[:#{key}]"
167 s << "\nreturn
[nil,nil] unless #{vc}" if vc
168 s << "\n#{expiry_statement}"
171 def interpolation_chunk(value_code = local_name)
172 "\
#{URI.escape(#{value_code}.to_s
, ActionController
::Routing::Segment::UNSAFE_PCHAR)}"
175 def string_structure(prior_segments)
176 if optional? # We have a conditional to do...
177 # If we should not appear in the url, just write the code for the prior
178 # segments. This occurs if our value is the default value, or, if we are
179 # optional, if we have nil as our value.
180 "if #{local_name} == #{default.inspect}\n" +
181 continue_string_structure(prior_segments
) +
182 "\nelse\n" +
# Otherwise, write the code up to here
183 "#{interpolation_statement(prior_segments)}\nend"
185 interpolation_statement(prior_segments
)
190 Regexp
.new
"\\A#{regexp.to_s}\\Z" if regexp
195 if regexp_has_modifiers
?
201 "([^#{Routing::SEPARATORS.join}]+)"
205 def number_of_captures
207 regexp.number_of_captures + 1
213 def build_pattern(pattern
)
214 pattern
= "#{regexp_chunk}#{pattern}"
215 optional
? ? Regexp
.optionalize(pattern
) : pattern
218 def match_extraction(next_capture
)
219 # All non code-related keys (such as :id, :slug) are URI-unescaped as
221 default_value
= default
? default
.inspect
: nil
223 value
= if (m
= match
[#{next_capture}])
228 params
[:#{key}] = value
if value
232 def optionality_implied
?
233 [:action, :id].include? key
236 def regexp_has_modifiers
?
237 regexp
.options
& (Regexp
::IGNORECASE | Regexp
::EXTENDED) !
= 0
241 class ControllerSegment
< DynamicSegment
#:nodoc:
243 possible_names
= Routing
.possible_controllers
.collect
{ |name
| Regexp
.escape name
}
244 "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
247 def number_of_captures
251 # Don't URI.escape the controller name since it may contain slashes.
252 def interpolation_chunk(value_code
= local_name
)
253 "\#{#{value_code}.to_s}"
256 # Make sure controller names like Admin/Content are correctly normalized to
259 "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default
}).downcase
"
262 def match_extraction(next_capture)
264 "params
[:#{key}] = match
[#{next_capture}] ? match
[#{next_capture}].downcase
: '#{default}'"
266 "params
[:#{key}] = match
[#{next_capture}].downcase
if match
[#{next_capture}]"
271 class PathSegment < DynamicSegment #:nodoc:
272 def interpolation_chunk(value_code = local_name)
277 "#{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}"
285 raise RoutingError
, "paths cannot have non-empty default values" unless path
.blank
?
288 def match_extraction(next_capture
)
289 "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
296 def number_of_captures
297 regexp
? regexp
.number_of_captures
: 1
300 def optionality_implied
?
304 class Result
< ::Array #:nodoc:
305 def to_s() join
'/' end
306 def self.new_escaped(strings
)
307 new strings
.collect
{|str
| URI
.unescape str
}