Merged updates from trunk into stable branch
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / routing / route.rb
1 module ActionController
2 module Routing
3 class Route #:nodoc:
4 attr_accessor :segments, :requirements, :conditions, :optimise
5
6 def initialize(segments = [], requirements = {}, conditions = {})
7 @segments = segments
8 @requirements = requirements
9 @conditions = conditions
10
11 if !significant_keys.include?(:action) && !requirements[:action]
12 @requirements[:action] = "index"
13 @significant_keys << :action
14 end
15
16 # Routes cannot use the current string interpolation method
17 # if there are user-supplied <tt>:requirements</tt> as the interpolation
18 # code won't raise RoutingErrors when generating
19 has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp }
20 if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
21 @optimise = false
22 else
23 @optimise = true
24 end
25 end
26
27 # Indicates whether the routes should be optimised with the string interpolation
28 # version of the named routes methods.
29 def optimise?
30 @optimise && ActionController::Base::optimise_named_routes
31 end
32
33 def segment_keys
34 segments.collect do |segment|
35 segment.key if segment.respond_to? :key
36 end.compact
37 end
38
39 def required_segment_keys
40 required_segments = segments.select {|seg| (!seg.optional? && !seg.is_a?(DividerSegment)) || seg.is_a?(PathSegment) }
41 required_segments.collect { |seg| seg.key if seg.respond_to?(:key)}.compact
42 end
43
44 # Build a query string from the keys of the given hash. If +only_keys+
45 # is given (as an array), only the keys indicated will be used to build
46 # the query string. The query string will correctly build array parameter
47 # values.
48 def build_query_string(hash, only_keys = nil)
49 elements = []
50
51 (only_keys || hash.keys).each do |key|
52 if value = hash[key]
53 elements << value.to_query(key)
54 end
55 end
56
57 elements.empty? ? '' : "?#{elements.sort * '&'}"
58 end
59
60 # A route's parameter shell contains parameter values that are not in the
61 # route's path, but should be placed in the recognized hash.
62 #
63 # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
64 #
65 # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
66 #
67 def parameter_shell
68 @parameter_shell ||= returning({}) do |shell|
69 requirements.each do |key, requirement|
70 shell[key] = requirement unless requirement.is_a? Regexp
71 end
72 end
73 end
74
75 # Return an array containing all the keys that are used in this route. This
76 # includes keys that appear inside the path, and keys that have requirements
77 # placed upon them.
78 def significant_keys
79 @significant_keys ||= returning([]) do |sk|
80 segments.each { |segment| sk << segment.key if segment.respond_to? :key }
81 sk.concat requirements.keys
82 sk.uniq!
83 end
84 end
85
86 # Return a hash of key/value pairs representing the keys in the route that
87 # have defaults, or which are specified by non-regexp requirements.
88 def defaults
89 @defaults ||= returning({}) do |hash|
90 segments.each do |segment|
91 next unless segment.respond_to? :default
92 hash[segment.key] = segment.default unless segment.default.nil?
93 end
94 requirements.each do |key,req|
95 next if Regexp === req || req.nil?
96 hash[key] = req
97 end
98 end
99 end
100
101 def matches_controller_and_action?(controller, action)
102 prepare_matching!
103 (@controller_requirement.nil? || @controller_requirement === controller) &&
104 (@action_requirement.nil? || @action_requirement === action)
105 end
106
107 def to_s
108 @to_s ||= begin
109 segs = segments.inject("") { |str,s| str << s.to_s }
110 "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
111 end
112 end
113
114 # TODO: Route should be prepared and frozen on initialize
115 def freeze
116 unless frozen?
117 write_generation!
118 write_recognition!
119 prepare_matching!
120
121 parameter_shell
122 significant_keys
123 defaults
124 to_s
125 end
126
127 super
128 end
129
130 def generate(options, hash, expire_on = {})
131 path, hash = generate_raw(options, hash, expire_on)
132 append_query_string(path, hash, extra_keys(options))
133 end
134
135 def generate_extras(options, hash, expire_on = {})
136 path, hash = generate_raw(options, hash, expire_on)
137 [path, extra_keys(options)]
138 end
139
140 private
141 def requirement_for(key)
142 return requirements[key] if requirements.key? key
143 segments.each do |segment|
144 return segment.regexp if segment.respond_to?(:key) && segment.key == key
145 end
146 nil
147 end
148
149 # Write and compile a +generate+ method for this Route.
150 def write_generation!
151 # Build the main body of the generation
152 body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
153
154 # If we have conditions that must be tested first, nest the body inside an if
155 body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
156 args = "options, hash, expire_on = {}"
157
158 # Nest the body inside of a def block, and then compile it.
159 raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
160 instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
161
162 # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
163 # are the same as the keys that were recalled from the previous request. Thus,
164 # we can use the expire_on.keys to determine which keys ought to be used to build
165 # the query string. (Never use keys from the recalled request when building the
166 # query string.)
167
168 raw_method
169 end
170
171 # Build several lines of code that extract values from the options hash. If any
172 # of the values are missing or rejected then a return will be executed.
173 def generation_extraction
174 segments.collect do |segment|
175 segment.extraction_code
176 end.compact * "\n"
177 end
178
179 # Produce a condition expression that will check the requirements of this route
180 # upon generation.
181 def generation_requirements
182 requirement_conditions = requirements.collect do |key, req|
183 if req.is_a? Regexp
184 value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
185 "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
186 else
187 "hash[:#{key}] == #{req.inspect}"
188 end
189 end
190 requirement_conditions * ' && ' unless requirement_conditions.empty?
191 end
192
193 def generation_structure
194 segments.last.string_structure segments[0..-2]
195 end
196
197 # Write and compile a +recognize+ method for this Route.
198 def write_recognition!
199 # Create an if structure to extract the params from a match if it occurs.
200 body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
201 body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
202
203 # Build the method declaration and compile it
204 method_decl = "def recognize(path, env = {})\n#{body}\nend"
205 instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
206 method_decl
207 end
208
209 # Plugins may override this method to add other conditions, like checks on
210 # host, subdomain, and so forth. Note that changes here only affect route
211 # recognition, not generation.
212 def recognition_conditions
213 result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
214 result << "[conditions[:method]].flatten.include?(env[:method])" if conditions[:method]
215 result
216 end
217
218 # Build the regular expression pattern that will match this route.
219 def recognition_pattern(wrap = true)
220 pattern = ''
221 segments.reverse_each do |segment|
222 pattern = segment.build_pattern pattern
223 end
224 wrap ? ("\\A" + pattern + "\\Z") : pattern
225 end
226
227 # Write the code to extract the parameters from a matched route.
228 def recognition_extraction
229 next_capture = 1
230 extraction = segments.collect do |segment|
231 x = segment.match_extraction(next_capture)
232 next_capture += segment.number_of_captures
233 x
234 end
235 extraction.compact
236 end
237
238 # Generate the query string with any extra keys in the hash and append
239 # it to the given path, returning the new path.
240 def append_query_string(path, hash, query_keys = nil)
241 return nil unless path
242 query_keys ||= extra_keys(hash)
243 "#{path}#{build_query_string(hash, query_keys)}"
244 end
245
246 # Determine which keys in the given hash are "extra". Extra keys are
247 # those that were not used to generate a particular route. The extra
248 # keys also do not include those recalled from the prior request, nor
249 # do they include any keys that were implied in the route (like a
250 # <tt>:controller</tt> that is required, but not explicitly used in the
251 # text of the route.)
252 def extra_keys(hash, recall = {})
253 (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
254 end
255
256 def prepare_matching!
257 unless defined? @matching_prepared
258 @controller_requirement = requirement_for(:controller)
259 @action_requirement = requirement_for(:action)
260 @matching_prepared = true
261 end
262 end
263 end
264 end
265 end