1
module ActionController
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.
7
# Mapper instances have relatively few instance methods, in order to avoid
8
# clashes with named routes.
10
include ActionController::Resources
12
def initialize(set) #:nodoc:
16
# Create an unnamed route with the provided +path+ and +options+. See
17
# ActionController::Routing for an introduction to routes.
18
def connect(path, options = {})
19
@set.add_route(path, options)
22
# Creates a named route called "root" for matching the root level request.
23
def root(options = {})
24
if options.is_a?(Symbol)
25
if source_route = @set.named_routes.routes[options]
26
options = source_route.defaults.merge({ :conditions => source_route.conditions })
29
named_route("root", '', options)
32
def named_route(name, path, options = {}) #:nodoc:
33
@set.add_named_route(name, path, options)
36
# Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model.
39
# map.namespace(:admin) do |admin|
40
# admin.resources :products,
41
# :has_many => [ :tags, :images, :variants ]
44
# This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController.
45
# It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for
46
# Admin::TagsController.
47
def namespace(name, options = {}, &block)
48
if options[:namespace]
49
with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
51
with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
55
def method_missing(route_name, *args, &proc) #:nodoc:
56
super unless args.length >= 1 && proc.nil?
57
@set.add_named_route(route_name, *args)
61
# A NamedRouteCollection instance is a collection of named routes, and also
62
# maintains an anonymous module that can be used to install helpers for the
64
class NamedRouteCollection #:nodoc:
66
include ActionController::Routing::Optimisation
67
attr_reader :routes, :helpers
77
@module ||= Module.new
78
@module.instance_methods.each do |selector|
79
@module.class_eval { remove_method selector }
84
routes[name.to_sym] = route
85
define_named_route_methods(name, route)
97
routes.each { |name, route| yield name, route }
110
old_routes = routes.dup
112
old_routes.each do |name, route|
117
def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
119
Array(destinations).each do |dest|
120
dest.__send__(:include, @module)
125
def url_helper_name(name, kind = :url)
129
def hash_access_name(name, kind = :url)
130
:"hash_for_#{name}_#{kind}"
133
def define_named_route_methods(name, route)
134
{:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
135
hash = route.defaults.merge(:use_route => name).merge(opts)
136
define_hash_access route, name, kind, hash
137
define_url_helper route, name, kind, hash
141
def named_helper_module_eval(code, *args)
142
@module.module_eval(code, *args)
145
def define_hash_access(route, name, kind, options)
146
selector = hash_access_name(name, kind)
147
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
148
def #{selector}(options = nil) # def hash_for_users_url(options = nil)
149
options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false}
151
protected :#{selector} # protected :hash_for_users_url
156
def define_url_helper(route, name, kind, options)
157
selector = url_helper_name(name, kind)
158
# The segment keys used for positional paramters
160
hash_access_method = hash_access_name(name, kind)
162
# allow ordered parameters to be associated with corresponding
163
# dynamic segments, so you can do
165
# foo_url(bar, baz, bang)
169
# foo_url(:bar => bar, :baz => baz, :bang => bang)
171
# Also allow options hash, so you can do
173
# foo_url(bar, baz, bang, :sort_by => 'baz')
175
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
176
def #{selector}(*args) # def users_url(*args)
178
#{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)}
180
opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first
181
args.first || {} # args.first || {}
183
options = args.extract_options! # options = args.extract_options!
184
args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)|
188
options.merge(args) # options.merge(args)
191
url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts))
194
#Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL.
195
def formatted_#{selector}(*args) # def formatted_users_url(*args)
196
ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn(
197
"formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " +
198
"Please pass format to the standard " + # "Please pass format to the standard " +
199
"#{selector} method instead.", caller) # "users_url method instead.", caller)
200
#{selector}(*args) # users_url(*args)
202
protected :#{selector} # protected :users_url
208
attr_accessor :routes, :named_routes, :configuration_files
211
self.configuration_files = []
214
self.named_routes = NamedRouteCollection.new
216
clear_recognize_optimized!
219
# Subclasses and plugins may override this method to specify a different
220
# RouteBuilder instance, so that other route DSL's can be created.
222
@builder ||= RouteBuilder.new
226
yield Mapper.new(self)
233
@combined_regexp = nil
234
@routes_by_controller = nil
235
# This will force routing/recognition_optimization.rb
236
# to refresh optimisations.
237
clear_recognize_optimized!
240
def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
241
Array(destinations).each { |d| d.module_eval { include Helpers } }
242
named_routes.install(destinations, regenerate_code)
249
def add_configuration_file(path)
250
self.configuration_files << path
253
# Deprecated accessor
254
def configuration_file=(path)
255
add_configuration_file(path)
258
# Deprecated accessor
259
def configuration_file
264
Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones
269
# reload! will always force a reload whereas load checks the timestamp first
273
if configuration_files.any? && @routes_last_modified
274
if routes_changed_at == @routes_last_modified
275
return # routes didn't change, don't reload
277
@routes_last_modified = routes_changed_at
285
if configuration_files.any?
286
configuration_files.each { |config| load(config) }
287
@routes_last_modified = routes_changed_at
289
add_route ":controller/:action/:id"
293
def routes_changed_at
294
routes_changed_at = nil
296
configuration_files.each do |config|
297
config_changed_at = File.stat(config).mtime
299
if routes_changed_at.nil? || config_changed_at > routes_changed_at
300
routes_changed_at = config_changed_at
307
def add_route(path, options = {})
308
options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) }
309
route = builder.build(path, options)
314
def add_named_route(name, path, options = {})
315
# TODO - is options EVER used?
316
name = options[:name_prefix] + name.to_s if options[:name_prefix]
317
named_routes[name.to_sym] = add_route(path, options)
320
def options_as_params(options)
321
# If an explicit :controller was given, always make :action explicit
322
# too, so that action expiry works as expected for things like
324
# generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
326
# (the above is from the unit tests). In the above case, because the
327
# controller was explicitly given, but no action, the action is implied to
328
# be "index", not the recalled action of "show".
332
options_as_params = options.clone
333
options_as_params[:action] ||= 'index' if options[:controller]
334
options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
338
def build_expiry(options, recall)
339
recall.inject({}) do |expiry, (key, recalled_value)|
340
expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
345
# Generate the path indicated by the arguments, and return an array of
346
# the keys that were not used to generate it.
347
def extra_keys(options, recall={})
348
generate_extras(options, recall).last
351
def generate_extras(options, recall={})
352
generate(options, recall, :generate_extras)
355
def generate(options, recall = {}, method=:generate)
356
named_route_name = options.delete(:use_route)
357
generate_all = options.delete(:generate_all)
359
named_route = named_routes[named_route_name]
360
options = named_route.parameter_shell.merge(options)
363
options = options_as_params(options)
364
expire_on = build_expiry(options, recall)
366
if options[:controller]
367
options[:controller] = options[:controller].to_s
369
# if the controller has changed, make sure it changes relative to the
370
# current controller module, if any. In other words, if we're currently
371
# on admin/get, and the new controller is 'set', the new controller
372
# should really be admin/set.
373
if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
374
old_parts = recall[:controller].split('/')
375
new_parts = options[:controller].split('/')
376
parts = old_parts[0..-(new_parts.length + 1)] + new_parts
377
options[:controller] = parts.join('/')
380
# drop the leading '/' on the controller name
381
options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
382
merged = recall.merge(options)
385
path = named_route.generate(options, merged, expire_on)
387
raise_named_route_error(options, named_route, named_route_name)
392
merged[:action] ||= 'index'
393
options[:action] ||= 'index'
395
controller = merged[:controller]
396
action = merged[:action]
398
raise RoutingError, "Need controller and action!" unless controller && action
401
# Used by caching to expire all paths for a resource
402
return routes.collect do |route|
403
route.__send__(method, options, merged, expire_on)
407
# don't use the recalled keys when determining which routes to check
408
future_routes, deprecated_routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }]
409
routes = Routing.generate_best_match ? deprecated_routes : future_routes
411
routes.each_with_index do |route, index|
412
results = route.__send__(method, options, merged, expire_on)
413
if results && (!results.is_a?(Array) || results.first)
419
raise RoutingError, "No route matches #{options.inspect}"
422
# try to give a helpful error message when named route generation fails
423
def raise_named_route_error(options, named_route, named_route_name)
424
diff = named_route.requirements.diff(options)
426
raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}"
428
required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) }
429
required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment
430
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?"
435
request = Request.new(env)
436
app = Routing::Routes.recognize(request)
440
def recognize(request)
441
params = recognize_path(request.path, extract_request_environment(request))
442
request.path_parameters = params.with_indifferent_access
443
"#{params[:controller].to_s.camelize}Controller".constantize
446
def recognize_path(path, environment={})
447
raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path."
450
def routes_by_controller
451
@routes_by_controller ||= Hash.new do |controller_hash, controller|
452
controller_hash[controller] = Hash.new do |action_hash, action|
453
action_hash[action] = Hash.new do |key_hash, keys|
455
routes_for_controller_and_action_and_keys(controller, action, keys),
456
deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
463
def routes_for(options, merged, expire_on)
464
raise "Need controller and action!" unless controller && action
465
controller = merged[:controller]
466
merged = options if expire_on[:controller]
467
action = merged[:action] || 'index'
469
routes_by_controller[controller][action][merged.keys][1]
472
def routes_for_controller_and_action(controller, action)
473
ActiveSupport::Deprecation.warn "routes_for_controller_and_action() has been deprecated. Please use routes_for()"
474
selected = routes.select do |route|
475
route.matches_controller_and_action? controller, action
477
(selected.length == routes.length) ? routes : selected
480
def routes_for_controller_and_action_and_keys(controller, action, keys)
481
routes.select do |route|
482
route.matches_controller_and_action? controller, action
486
def deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
487
selected = routes.select do |route|
488
route.matches_controller_and_action? controller, action
490
selected.sort_by do |route|
491
(keys - route.significant_keys).length
495
# Subclasses and plugins may override this method to extract further attributes
496
# from the request, for use by route conditions and such.
497
def extract_request_environment(request)
498
{ :method => request.method }