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