Froze rails gems
[depot.git] / vendor / rails / actionpack / lib / action_controller / routing / route_set.rb
1 module ActionController
2 module Routing
3 class RouteSet #:nodoc:
4 # Mapper instances are used to build routes. The object passed to the draw
5 # block in config/routes.rb is a Mapper instance.
6 #
7 # Mapper instances have relatively few instance methods, in order to avoid
8 # clashes with named routes.
9 class Mapper #:doc:
10 def initialize(set) #:nodoc:
11 @set = set
12 end
13
14 # Create an unnamed route with the provided +path+ and +options+. See
15 # ActionController::Routing for an introduction to routes.
16 def connect(path, options = {})
17 @set.add_route(path, options)
18 end
19
20 # Creates a named route called "root" for matching the root level request.
21 def root(options = {})
22 if options.is_a?(Symbol)
23 if source_route = @set.named_routes.routes[options]
24 options = source_route.defaults.merge({ :conditions => source_route.conditions })
25 end
26 end
27 named_route("root", '', options)
28 end
29
30 def named_route(name, path, options = {}) #:nodoc:
31 @set.add_named_route(name, path, options)
32 end
33
34 # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
35 # Example:
36 #
37 # map.namespace(:admin) do |admin|
38 # admin.resources :products,
39 # :has_many => [ :tags, :images, :variants ]
40 # end
41 #
42 # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
43 # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
44 # Admin::TagsController.
45 def namespace(name, options = {}, &block)
46 if options[:namespace]
47 with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
48 else
49 with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
50 end
51 end
52
53 def method_missing(route_name, *args, &proc) #:nodoc:
54 super unless args.length >= 1 && proc.nil?
55 @set.add_named_route(route_name, *args)
56 end
57 end
58
59 # A NamedRouteCollection instance is a collection of named routes, and also
60 # maintains an anonymous module that can be used to install helpers for the
61 # named routes.
62 class NamedRouteCollection #:nodoc:
63 include Enumerable
64 include ActionController::Routing::Optimisation
65 attr_reader :routes, :helpers
66
67 def initialize
68 clear!
69 end
70
71 def clear!
72 @routes = {}
73 @helpers = []
74
75 @module ||= Module.new
76 @module.instance_methods.each do |selector|
77 @module.class_eval { remove_method selector }
78 end
79 end
80
81 def add(name, route)
82 routes[name.to_sym] = route
83 define_named_route_methods(name, route)
84 end
85
86 def get(name)
87 routes[name.to_sym]
88 end
89
90 alias []= add
91 alias [] get
92 alias clear clear!
93
94 def each
95 routes.each { |name, route| yield name, route }
96 self
97 end
98
99 def names
100 routes.keys
101 end
102
103 def length
104 routes.length
105 end
106
107 def reset!
108 old_routes = routes.dup
109 clear!
110 old_routes.each do |name, route|
111 add(name, route)
112 end
113 end
114
115 def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
116 reset! if regenerate
117 Array(destinations).each do |dest|
118 dest.__send__(:include, @module)
119 end
120 end
121
122 private
123 def url_helper_name(name, kind = :url)
124 :"#{name}_#{kind}"
125 end
126
127 def hash_access_name(name, kind = :url)
128 :"hash_for_#{name}_#{kind}"
129 end
130
131 def define_named_route_methods(name, route)
132 {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
133 hash = route.defaults.merge(:use_route => name).merge(opts)
134 define_hash_access route, name, kind, hash
135 define_url_helper route, name, kind, hash
136 end
137 end
138
139 def define_hash_access(route, name, kind, options)
140 selector = hash_access_name(name, kind)
141 @module.module_eval <<-end_eval # We use module_eval to avoid leaks
142 def #{selector}(options = nil)
143 options ? #{options.inspect}.merge(options) : #{options.inspect}
144 end
145 protected :#{selector}
146 end_eval
147 helpers << selector
148 end
149
150 def define_url_helper(route, name, kind, options)
151 selector = url_helper_name(name, kind)
152 # The segment keys used for positional paramters
153
154 hash_access_method = hash_access_name(name, kind)
155
156 # allow ordered parameters to be associated with corresponding
157 # dynamic segments, so you can do
158 #
159 # foo_url(bar, baz, bang)
160 #
161 # instead of
162 #
163 # foo_url(:bar => bar, :baz => baz, :bang => bang)
164 #
165 # Also allow options hash, so you can do
166 #
167 # foo_url(bar, baz, bang, :sort_by => 'baz')
168 #
169 @module.module_eval <<-end_eval # We use module_eval to avoid leaks
170 def #{selector}(*args)
171
172 #{generate_optimisation_block(route, kind)}
173
174 opts = if args.empty? || Hash === args.first
175 args.first || {}
176 else
177 options = args.extract_options!
178 args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
179 h[k] = v
180 h
181 end
182 options.merge(args)
183 end
184
185 url_for(#{hash_access_method}(opts))
186 end
187 protected :#{selector}
188 end_eval
189 helpers << selector
190 end
191 end
192
193 attr_accessor :routes, :named_routes, :configuration_file
194
195 def initialize
196 self.routes = []
197 self.named_routes = NamedRouteCollection.new
198
199 clear_recognize_optimized!
200 end
201
202 # Subclasses and plugins may override this method to specify a different
203 # RouteBuilder instance, so that other route DSL's can be created.
204 def builder
205 @builder ||= RouteBuilder.new
206 end
207
208 def draw
209 clear!
210 yield Mapper.new(self)
211 install_helpers
212 end
213
214 def clear!
215 routes.clear
216 named_routes.clear
217 @combined_regexp = nil
218 @routes_by_controller = nil
219 # This will force routing/recognition_optimization.rb
220 # to refresh optimisations.
221 clear_recognize_optimized!
222 end
223
224 def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
225 Array(destinations).each { |d| d.module_eval { include Helpers } }
226 named_routes.install(destinations, regenerate_code)
227 end
228
229 def empty?
230 routes.empty?
231 end
232
233 def load!
234 Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
235 clear!
236 load_routes!
237 end
238
239 # reload! will always force a reload whereas load checks the timestamp first
240 alias reload! load!
241
242 def reload
243 if @routes_last_modified && configuration_file
244 mtime = File.stat(configuration_file).mtime
245 # if it hasn't been changed, then just return
246 return if mtime == @routes_last_modified
247 # if it has changed then record the new time and fall to the load! below
248 @routes_last_modified = mtime
249 end
250 load!
251 end
252
253 def load_routes!
254 if configuration_file
255 load configuration_file
256 @routes_last_modified = File.stat(configuration_file).mtime
257 else
258 add_route ":controller/:action/:id"
259 end
260 end
261
262 def add_route(path, options = {})
263 route = builder.build(path, options)
264 routes << route
265 route
266 end
267
268 def add_named_route(name, path, options = {})
269 # TODO - is options EVER used?
270 name = options[:name_prefix] + name.to_s if options[:name_prefix]
271 named_routes[name.to_sym] = add_route(path, options)
272 end
273
274 def options_as_params(options)
275 # If an explicit :controller was given, always make :action explicit
276 # too, so that action expiry works as expected for things like
277 #
278 # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
279 #
280 # (the above is from the unit tests). In the above case, because the
281 # controller was explicitly given, but no action, the action is implied to
282 # be "index", not the recalled action of "show".
283 #
284 # great fun, eh?
285
286 options_as_params = options.clone
287 options_as_params[:action] ||= 'index' if options[:controller]
288 options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
289 options_as_params
290 end
291
292 def build_expiry(options, recall)
293 recall.inject({}) do |expiry, (key, recalled_value)|
294 expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
295 expiry
296 end
297 end
298
299 # Generate the path indicated by the arguments, and return an array of
300 # the keys that were not used to generate it.
301 def extra_keys(options, recall={})
302 generate_extras(options, recall).last
303 end
304
305 def generate_extras(options, recall={})
306 generate(options, recall, :generate_extras)
307 end
308
309 def generate(options, recall = {}, method=:generate)
310 named_route_name = options.delete(:use_route)
311 generate_all = options.delete(:generate_all)
312 if named_route_name
313 named_route = named_routes[named_route_name]
314 options = named_route.parameter_shell.merge(options)
315 end
316
317 options = options_as_params(options)
318 expire_on = build_expiry(options, recall)
319
320 if options[:controller]
321 options[:controller] = options[:controller].to_s
322 end
323 # if the controller has changed, make sure it changes relative to the
324 # current controller module, if any. In other words, if we're currently
325 # on admin/get, and the new controller is 'set', the new controller
326 # should really be admin/set.
327 if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
328 old_parts = recall[:controller].split('/')
329 new_parts = options[:controller].split('/')
330 parts = old_parts[0..-(new_parts.length + 1)] + new_parts
331 options[:controller] = parts.join('/')
332 end
333
334 # drop the leading '/' on the controller name
335 options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
336 merged = recall.merge(options)
337
338 if named_route
339 path = named_route.generate(options, merged, expire_on)
340 if path.nil?
341 raise_named_route_error(options, named_route, named_route_name)
342 else
343 return path
344 end
345 else
346 merged[:action] ||= 'index'
347 options[:action] ||= 'index'
348
349 controller = merged[:controller]
350 action = merged[:action]
351
352 raise RoutingError, "Need controller and action!" unless controller && action
353
354 if generate_all
355 # Used by caching to expire all paths for a resource
356 return routes.collect do |route|
357 route.__send__(method, options, merged, expire_on)
358 end.compact
359 end
360
361 # don't use the recalled keys when determining which routes to check
362 routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
363
364 routes.each do |route|
365 results = route.__send__(method, options, merged, expire_on)
366 return results if results && (!results.is_a?(Array) || results.first)
367 end
368 end
369
370 raise RoutingError, "No route matches #{options.inspect}"
371 end
372
373 # try to give a helpful error message when named route generation fails
374 def raise_named_route_error(options, named_route, named_route_name)
375 diff = named_route.requirements.diff(options)
376 unless diff.empty?
377 raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
378 else
379 required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
380 required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
381 raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?"
382 end
383 end
384
385 def recognize(request)
386 params = recognize_path(request.path, extract_request_environment(request))
387 request.path_parameters = params.with_indifferent_access
388 "#{params[:controller].camelize}Controller".constantize
389 end
390
391 def recognize_path(path, environment={})
392 raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path."
393 end
394
395 def routes_by_controller
396 @routes_by_controller ||= Hash.new do |controller_hash, controller|
397 controller_hash[controller] = Hash.new do |action_hash, action|
398 action_hash[action] = Hash.new do |key_hash, keys|
399 key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
400 end
401 end
402 end
403 end
404
405 def routes_for(options, merged, expire_on)
406 raise "Need controller and action!" unless controller && action
407 controller = merged[:controller]
408 merged = options if expire_on[:controller]
409 action = merged[:action] || 'index'
410
411 routes_by_controller[controller][action][merged.keys]
412 end
413
414 def routes_for_controller_and_action(controller, action)
415 selected = routes.select do |route|
416 route.matches_controller_and_action? controller, action
417 end
418 (selected.length == routes.length) ? routes : selected
419 end
420
421 def routes_for_controller_and_action_and_keys(controller, action, keys)
422 selected = routes.select do |route|
423 route.matches_controller_and_action? controller, action
424 end
425 selected.sort_by do |route|
426 (keys - route.significant_keys).length
427 end
428 end
429
430 # Subclasses and plugins may override this method to extract further attributes
431 # from the request, for use by route conditions and such.
432 def extract_request_environment(request)
433 { :method => request.method }
434 end
435 end
436 end
437 end