1 module ActionController
4 attr_accessor
:segments, :requirements, :conditions, :optimise
6 def initialize(segments
= [], requirements
= {}, conditions
= {})
8 @requirements = requirements
9 @conditions = conditions
11 if !significant_keys
.include?(:action) && !requirements
[:action]
12 @requirements[:action] = "index"
13 @significant_keys << :action
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
27 # Indicates whether the routes should be optimised with the string interpolation
28 # version of the named routes methods.
30 @optimise && ActionController
::Base::optimise_named_routes
34 segments
.collect
do |segment
|
35 segment
.key
if segment
.respond_to
? :key
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
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
48 def build_query_string(hash
, only_keys
= nil)
51 (only_keys
|| hash
.keys
).each
do |key
|
53 elements
<< value
.to_query(key
)
57 elements
.empty
? ? '' : "?#{elements.sort * '&'}"
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.
63 # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
65 # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
68 @parameter_shell ||= returning({}) do |shell
|
69 requirements
.each
do |key
, requirement
|
70 shell
[key
] = requirement
unless requirement
.is_a
? Regexp
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
79 @significant_keys ||= returning([]) do |sk
|
80 segments
.each
{ |segment
| sk
<< segment
.key
if segment
.respond_to
? :key }
81 sk
.concat requirements
.keys
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.
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?
94 requirements
.each
do |key
,req
|
95 next if Regexp
=== req
|| req
.nil?
101 def matches_controller_and_action
?(controller
, action
)
103 (@controller_requirement.nil? || @controller_requirement === controller
) &&
104 (@action_requirement.nil? || @action_requirement === action
)
109 segs
= segments
.inject("") { |str
,s
| str
<< s
.to_s
}
110 "%-6s %-40s %s" % [(conditions
[:method] || :any).to_s
.upcase
, segs
, requirements
.inspect
]
114 # TODO: Route should be prepared and frozen on initialize
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
))
135 def generate_extras(options
, hash
, expire_on
= {})
136 path
, hash
= generate_raw(options
, hash
, expire_on
)
137 [path
, extra_keys(options
)]
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
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}"
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 = {}"
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__})"
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
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
179 # Produce a condition expression that will check the requirements of this route
181 def generation_requirements
182 requirement_conditions
= requirements
.collect
do |key
, req
|
184 value_regexp
= Regexp
.new
"\\A#{req.to_s}\\Z"
185 "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
187 "hash[:#{key}] == #{req.inspect}"
190 requirement_conditions
* ' && ' unless requirement_conditions
.empty
?
193 def generation_structure
194 segments
.last
.string_structure segments
[0..-2]
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"
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__})"
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]
218 # Build the regular expression pattern that will match this route.
219 def recognition_pattern(wrap
= true)
221 segments
.reverse_each
do |segment
|
222 pattern
= segment
.build_pattern pattern
224 wrap
? ("\\A" + pattern
+ "\\Z") : pattern
227 # Write the code to extract the parameters from a matched route.
228 def recognition_extraction
230 extraction
= segments
.collect
do |segment
|
231 x
= segment
.match_extraction(next_capture
)
232 next_capture
+= segment
.number_of_captures
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)}"
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
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