Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/routing/route.rb

Revision 9115, 9.1 kB (checked in by david, 3 months ago)

Added support for regexp flags like ignoring case in the :requirements part of routes declarations (closes #11421) [NeilW]

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