2009-10-20 00:32:06 -04:00
|
|
|
require 'rack/mount'
|
|
|
|
require 'forwardable'
|
|
|
|
|
2009-10-20 11:14:46 -04:00
|
|
|
module ActionDispatch
|
2008-01-17 17:45:54 -05:00
|
|
|
module Routing
|
2008-07-28 14:38:20 -04:00
|
|
|
class RouteSet #:nodoc:
|
2009-10-20 00:32:06 -04:00
|
|
|
NotFound = lambda { |env|
|
2009-12-21 18:34:53 -05:00
|
|
|
raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}"
|
2009-10-20 00:32:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
|
|
|
|
|
|
|
|
class Dispatcher
|
|
|
|
def initialize(options = {})
|
|
|
|
defaults = options[:defaults]
|
|
|
|
@glob_param = options.delete(:glob)
|
|
|
|
end
|
|
|
|
|
|
|
|
def call(env)
|
|
|
|
params = env[PARAMETERS_KEY]
|
2009-12-11 01:11:16 -05:00
|
|
|
prepare_params!(params)
|
2010-02-16 16:09:42 -05:00
|
|
|
controller(params).action(params[:action]).call(env)
|
2009-12-11 01:11:16 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def prepare_params!(params)
|
2009-10-20 00:32:06 -04:00
|
|
|
merge_default_action!(params)
|
|
|
|
split_glob_param!(params) if @glob_param
|
2009-12-11 01:11:16 -05:00
|
|
|
|
2009-11-04 19:11:59 -05:00
|
|
|
params.each do |key, value|
|
|
|
|
if value.is_a?(String)
|
|
|
|
value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
|
|
|
|
params[key] = URI.unescape(value)
|
|
|
|
end
|
|
|
|
end
|
2009-12-11 01:11:16 -05:00
|
|
|
end
|
2009-10-20 00:32:06 -04:00
|
|
|
|
2010-02-16 16:09:42 -05:00
|
|
|
def controller(params, swallow=false)
|
2009-12-11 01:11:16 -05:00
|
|
|
if params && params.has_key?(:controller)
|
|
|
|
controller = "#{params[:controller].camelize}Controller"
|
|
|
|
ActiveSupport::Inflector.constantize(controller)
|
2009-10-20 00:32:06 -04:00
|
|
|
end
|
2010-02-06 05:39:51 -05:00
|
|
|
rescue NameError => e
|
2010-02-16 16:09:42 -05:00
|
|
|
raise ActionController::RoutingError, e.message, e.backtrace unless swallow
|
2009-10-20 00:32:06 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
def merge_default_action!(params)
|
|
|
|
params[:action] ||= 'index'
|
|
|
|
end
|
|
|
|
|
|
|
|
def split_glob_param!(params)
|
|
|
|
params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2008-01-17 17:45:54 -05:00
|
|
|
# A NamedRouteCollection instance is a collection of named routes, and also
|
|
|
|
# maintains an anonymous module that can be used to install helpers for the
|
|
|
|
# named routes.
|
|
|
|
class NamedRouteCollection #:nodoc:
|
|
|
|
include Enumerable
|
|
|
|
attr_reader :routes, :helpers
|
|
|
|
|
|
|
|
def initialize
|
|
|
|
clear!
|
|
|
|
end
|
|
|
|
|
|
|
|
def clear!
|
|
|
|
@routes = {}
|
|
|
|
@helpers = []
|
|
|
|
|
2010-01-07 09:26:31 -05:00
|
|
|
@module ||= Module.new do
|
|
|
|
instance_methods.each { |selector| remove_method(selector) }
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def add(name, route)
|
|
|
|
routes[name.to_sym] = route
|
|
|
|
define_named_route_methods(name, route)
|
|
|
|
end
|
|
|
|
|
|
|
|
def get(name)
|
|
|
|
routes[name.to_sym]
|
|
|
|
end
|
|
|
|
|
|
|
|
alias []= add
|
|
|
|
alias [] get
|
|
|
|
alias clear clear!
|
|
|
|
|
|
|
|
def each
|
|
|
|
routes.each { |name, route| yield name, route }
|
|
|
|
self
|
|
|
|
end
|
|
|
|
|
|
|
|
def names
|
|
|
|
routes.keys
|
|
|
|
end
|
|
|
|
|
|
|
|
def length
|
|
|
|
routes.length
|
|
|
|
end
|
|
|
|
|
|
|
|
def reset!
|
|
|
|
old_routes = routes.dup
|
|
|
|
clear!
|
|
|
|
old_routes.each do |name, route|
|
|
|
|
add(name, route)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false)
|
|
|
|
reset! if regenerate
|
|
|
|
Array(destinations).each do |dest|
|
2008-08-31 16:15:26 -04:00
|
|
|
dest.__send__(:include, @module)
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
def url_helper_name(name, kind = :url)
|
|
|
|
:"#{name}_#{kind}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def hash_access_name(name, kind = :url)
|
|
|
|
:"hash_for_#{name}_#{kind}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def define_named_route_methods(name, route)
|
|
|
|
{:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
|
|
|
|
hash = route.defaults.merge(:use_route => name).merge(opts)
|
|
|
|
define_hash_access route, name, kind, hash
|
|
|
|
define_url_helper route, name, kind, hash
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def define_hash_access(route, name, kind, options)
|
|
|
|
selector = hash_access_name(name, kind)
|
2010-01-06 03:32:29 -05:00
|
|
|
|
|
|
|
# We use module_eval to avoid leaks
|
|
|
|
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
2008-12-28 14:48:05 -05:00
|
|
|
def #{selector}(options = nil) # def hash_for_users_url(options = nil)
|
|
|
|
options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false}
|
|
|
|
end # end
|
|
|
|
protected :#{selector} # protected :hash_for_users_url
|
2010-01-06 03:32:29 -05:00
|
|
|
END_EVAL
|
2008-01-17 17:45:54 -05:00
|
|
|
helpers << selector
|
|
|
|
end
|
|
|
|
|
2010-01-06 03:32:29 -05:00
|
|
|
# Create a url helper allowing ordered parameters to be associated
|
|
|
|
# with corresponding dynamic segments, so you can do:
|
|
|
|
#
|
|
|
|
# foo_url(bar, baz, bang)
|
|
|
|
#
|
|
|
|
# Instead of:
|
|
|
|
#
|
|
|
|
# foo_url(:bar => bar, :baz => baz, :bang => bang)
|
|
|
|
#
|
|
|
|
# Also allow options hash, so you can do:
|
|
|
|
#
|
|
|
|
# foo_url(bar, baz, bang, :sort_by => 'baz')
|
|
|
|
#
|
2008-01-17 17:45:54 -05:00
|
|
|
def define_url_helper(route, name, kind, options)
|
|
|
|
selector = url_helper_name(name, kind)
|
|
|
|
hash_access_method = hash_access_name(name, kind)
|
|
|
|
|
2010-01-07 09:26:31 -05:00
|
|
|
# We use module_eval to avoid leaks.
|
|
|
|
#
|
|
|
|
# def users_url(*args)
|
|
|
|
# if args.empty? || Hash === args.first
|
|
|
|
# options = hash_for_users_url(args.first || {})
|
|
|
|
# else
|
|
|
|
# options = hash_for_users_url(args.extract_options!)
|
|
|
|
# default = default_url_options(options) if self.respond_to?(:default_url_options, true)
|
|
|
|
# options = (default ||= {}).merge(options)
|
|
|
|
#
|
|
|
|
# keys = []
|
2010-01-07 11:17:06 -05:00
|
|
|
# keys -= options.keys if args.size < keys.size - 1
|
2010-01-07 09:26:31 -05:00
|
|
|
#
|
|
|
|
# args = args.zip(keys).inject({}) do |h, (v, k)|
|
|
|
|
# h[k] = v
|
|
|
|
# h
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Tell url_for to skip default_url_options
|
|
|
|
# options[:use_defaults] = false
|
|
|
|
# options.merge!(args)
|
|
|
|
# end
|
2010-01-13 21:21:04 -05:00
|
|
|
#
|
2010-01-07 09:26:31 -05:00
|
|
|
# url_for(options)
|
|
|
|
# end
|
2010-01-06 03:32:29 -05:00
|
|
|
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
2010-01-07 09:26:31 -05:00
|
|
|
def #{selector}(*args)
|
2010-01-13 21:21:04 -05:00
|
|
|
if args.empty? || Hash === args.first
|
2010-01-07 09:26:31 -05:00
|
|
|
options = #{hash_access_method}(args.first || {})
|
|
|
|
else
|
|
|
|
options = #{hash_access_method}(args.extract_options!)
|
|
|
|
default = default_url_options(options) if self.respond_to?(:default_url_options, true)
|
|
|
|
options = (default ||= {}).merge(options)
|
|
|
|
|
|
|
|
keys = #{route.segment_keys.inspect}
|
2010-01-07 11:17:06 -05:00
|
|
|
keys -= options.keys if args.size < keys.size - 1 # take format into account
|
2010-01-07 09:26:31 -05:00
|
|
|
|
|
|
|
args = args.zip(keys).inject({}) do |h, (v, k)|
|
|
|
|
h[k] = v
|
|
|
|
h
|
|
|
|
end
|
|
|
|
|
|
|
|
# Tell url_for to skip default_url_options
|
|
|
|
options[:use_defaults] = false
|
|
|
|
options.merge!(args)
|
|
|
|
end
|
|
|
|
|
|
|
|
url_for(options)
|
|
|
|
end
|
|
|
|
protected :#{selector}
|
2010-01-06 03:32:29 -05:00
|
|
|
END_EVAL
|
2008-01-17 17:45:54 -05:00
|
|
|
helpers << selector
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-01-24 05:06:06 -05:00
|
|
|
attr_accessor :routes, :named_routes, :controller_namespaces
|
|
|
|
attr_accessor :disable_clear_and_finalize, :resources_path_names
|
2008-01-17 17:45:54 -05:00
|
|
|
|
2010-01-13 21:21:04 -05:00
|
|
|
def self.default_resources_path_names
|
|
|
|
{ :new => 'new', :edit => 'edit' }
|
|
|
|
end
|
|
|
|
|
2008-01-17 17:45:54 -05:00
|
|
|
def initialize
|
|
|
|
self.routes = []
|
|
|
|
self.named_routes = NamedRouteCollection.new
|
2010-01-24 05:06:06 -05:00
|
|
|
self.resources_path_names = self.class.default_resources_path_names.dup
|
|
|
|
self.controller_namespaces = Set.new
|
2008-07-28 14:41:42 -04:00
|
|
|
|
2009-12-14 18:54:41 -05:00
|
|
|
@disable_clear_and_finalize = false
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
|
2009-10-20 13:31:23 -04:00
|
|
|
def draw(&block)
|
2009-12-14 18:54:41 -05:00
|
|
|
clear! unless @disable_clear_and_finalize
|
2009-12-09 21:46:32 -05:00
|
|
|
|
|
|
|
mapper = Mapper.new(self)
|
|
|
|
if block.arity == 1
|
|
|
|
mapper.instance_exec(DeprecatedMapper.new(self), &block)
|
|
|
|
else
|
|
|
|
mapper.instance_exec(&block)
|
|
|
|
end
|
|
|
|
|
2009-12-14 18:54:41 -05:00
|
|
|
finalize! unless @disable_clear_and_finalize
|
2009-12-14 17:51:13 -05:00
|
|
|
|
|
|
|
nil
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
|
2009-12-14 18:54:41 -05:00
|
|
|
def finalize!
|
|
|
|
@set.add_route(NotFound)
|
|
|
|
install_helpers
|
|
|
|
@set.freeze
|
|
|
|
end
|
|
|
|
|
2008-01-17 17:45:54 -05:00
|
|
|
def clear!
|
2009-12-14 18:54:41 -05:00
|
|
|
# Clear the controller cache so we may discover new ones
|
|
|
|
@controller_constraints = nil
|
2008-01-17 17:45:54 -05:00
|
|
|
routes.clear
|
|
|
|
named_routes.clear
|
2009-10-20 00:32:06 -04:00
|
|
|
@set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY)
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false)
|
|
|
|
Array(destinations).each { |d| d.module_eval { include Helpers } }
|
|
|
|
named_routes.install(destinations, regenerate_code)
|
|
|
|
end
|
|
|
|
|
|
|
|
def empty?
|
|
|
|
routes.empty?
|
|
|
|
end
|
|
|
|
|
2009-12-02 15:10:22 -05:00
|
|
|
CONTROLLER_REGEXP = /[_a-zA-Z0-9]+/
|
|
|
|
|
|
|
|
def controller_constraints
|
|
|
|
@controller_constraints ||= begin
|
2010-01-24 05:06:06 -05:00
|
|
|
namespaces = controller_namespaces + in_memory_controller_namespaces
|
|
|
|
source = namespaces.map { |ns| "#{Regexp.escape(ns)}/#{CONTROLLER_REGEXP.source}" }
|
2009-12-02 15:10:22 -05:00
|
|
|
source << CONTROLLER_REGEXP.source
|
|
|
|
Regexp.compile(source.sort.reverse.join('|'))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-01-24 05:06:06 -05:00
|
|
|
def in_memory_controller_namespaces
|
2009-12-02 15:10:22 -05:00
|
|
|
namespaces = Set.new
|
|
|
|
ActionController::Base.subclasses.each do |klass|
|
|
|
|
controller_name = klass.underscore
|
|
|
|
namespaces << controller_name.split('/')[0...-1].join('/')
|
|
|
|
end
|
|
|
|
namespaces.delete('')
|
|
|
|
namespaces
|
|
|
|
end
|
|
|
|
|
2009-10-24 19:08:54 -04:00
|
|
|
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil)
|
|
|
|
route = Route.new(app, conditions, requirements, defaults, name)
|
|
|
|
@set.add_route(*route)
|
2009-10-20 11:46:27 -04:00
|
|
|
named_routes[name] = route if name
|
2008-01-17 17:45:54 -05:00
|
|
|
routes << route
|
|
|
|
route
|
|
|
|
end
|
|
|
|
|
|
|
|
def options_as_params(options)
|
|
|
|
# If an explicit :controller was given, always make :action explicit
|
|
|
|
# too, so that action expiry works as expected for things like
|
|
|
|
#
|
|
|
|
# generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
|
|
|
|
#
|
|
|
|
# (the above is from the unit tests). In the above case, because the
|
|
|
|
# controller was explicitly given, but no action, the action is implied to
|
|
|
|
# be "index", not the recalled action of "show".
|
|
|
|
#
|
|
|
|
# great fun, eh?
|
|
|
|
|
|
|
|
options_as_params = options.clone
|
|
|
|
options_as_params[:action] ||= 'index' if options[:controller]
|
|
|
|
options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action]
|
|
|
|
options_as_params
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_expiry(options, recall)
|
|
|
|
recall.inject({}) do |expiry, (key, recalled_value)|
|
|
|
|
expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param)
|
|
|
|
expiry
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Generate the path indicated by the arguments, and return an array of
|
|
|
|
# the keys that were not used to generate it.
|
|
|
|
def extra_keys(options, recall={})
|
|
|
|
generate_extras(options, recall).last
|
|
|
|
end
|
|
|
|
|
|
|
|
def generate_extras(options, recall={})
|
|
|
|
generate(options, recall, :generate_extras)
|
|
|
|
end
|
|
|
|
|
2009-10-20 00:32:06 -04:00
|
|
|
def generate(options, recall = {}, method = :generate)
|
|
|
|
options, recall = options.dup, recall.dup
|
|
|
|
named_route = options.delete(:use_route)
|
2008-01-17 17:45:54 -05:00
|
|
|
|
|
|
|
options = options_as_params(options)
|
|
|
|
expire_on = build_expiry(options, recall)
|
|
|
|
|
2009-10-20 00:32:06 -04:00
|
|
|
recall[:action] ||= 'index' if options[:controller] || recall[:controller]
|
|
|
|
|
|
|
|
if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller])
|
|
|
|
options[:controller] = recall.delete(:controller)
|
|
|
|
|
|
|
|
if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action])
|
|
|
|
options[:action] = recall.delete(:action)
|
|
|
|
|
|
|
|
if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id])
|
|
|
|
options[:id] = recall.delete(:id)
|
|
|
|
end
|
|
|
|
end
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
2009-10-20 00:32:06 -04:00
|
|
|
|
|
|
|
options[:controller] = options[:controller].to_s if options[:controller]
|
|
|
|
|
2008-01-17 17:45:54 -05:00
|
|
|
if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/
|
|
|
|
old_parts = recall[:controller].split('/')
|
|
|
|
new_parts = options[:controller].split('/')
|
|
|
|
parts = old_parts[0..-(new_parts.length + 1)] + new_parts
|
|
|
|
options[:controller] = parts.join('/')
|
|
|
|
end
|
|
|
|
|
|
|
|
options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/
|
|
|
|
|
2009-10-20 00:32:06 -04:00
|
|
|
merged = options.merge(recall)
|
|
|
|
if options.has_key?(:action) && options[:action].nil?
|
|
|
|
options.delete(:action)
|
|
|
|
recall[:action] = 'index'
|
|
|
|
end
|
|
|
|
recall[:action] = options.delete(:action) if options[:action] == 'index'
|
|
|
|
|
2009-12-02 13:33:33 -05:00
|
|
|
opts = {}
|
|
|
|
opts[:parameterize] = lambda { |name, value|
|
2009-11-14 23:00:38 -05:00
|
|
|
if name == :controller
|
|
|
|
value
|
|
|
|
elsif value.is_a?(Array)
|
|
|
|
value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/')
|
|
|
|
else
|
|
|
|
Rack::Mount::Utils.escape_uri(value.to_param)
|
|
|
|
end
|
|
|
|
}
|
|
|
|
|
2009-12-02 13:33:33 -05:00
|
|
|
unless result = @set.generate(:path_info, named_route, options, recall, opts)
|
|
|
|
raise ActionController::RoutingError, "No route matches #{options.inspect}"
|
|
|
|
end
|
|
|
|
|
2009-12-10 23:57:07 -05:00
|
|
|
path, params = result
|
2009-12-02 13:33:33 -05:00
|
|
|
params.each do |k, v|
|
|
|
|
if v
|
|
|
|
params[k] = v
|
|
|
|
else
|
|
|
|
params.delete(k)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-10-20 00:32:06 -04:00
|
|
|
if path && method == :generate_extras
|
2009-12-10 23:57:07 -05:00
|
|
|
[path, params.keys]
|
2009-10-20 00:32:06 -04:00
|
|
|
elsif path
|
2009-12-10 23:57:07 -05:00
|
|
|
path << "?#{params.to_query}" if params.any?
|
2009-10-20 00:32:06 -04:00
|
|
|
path
|
2008-01-17 17:45:54 -05:00
|
|
|
else
|
2009-10-20 11:14:46 -04:00
|
|
|
raise ActionController::RoutingError, "No route matches #{options.inspect}"
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
2009-10-20 00:32:06 -04:00
|
|
|
rescue Rack::Mount::RoutingError
|
2009-10-20 11:14:46 -04:00
|
|
|
raise ActionController::RoutingError, "No route matches #{options.inspect}"
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
|
2009-10-20 00:32:06 -04:00
|
|
|
def call(env)
|
|
|
|
@set.call(env)
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
|
2009-12-11 01:01:22 -05:00
|
|
|
def recognize_path(path, environment = {})
|
2009-10-20 00:32:06 -04:00
|
|
|
method = (environment[:method] || "GET").to_s.upcase
|
2009-12-11 01:11:16 -05:00
|
|
|
path = Rack::Mount::Utils.normalize_path(path)
|
2008-01-17 17:45:54 -05:00
|
|
|
|
2009-10-20 00:32:06 -04:00
|
|
|
begin
|
|
|
|
env = Rack::MockRequest.env_for(path, {:method => method})
|
|
|
|
rescue URI::InvalidURIError => e
|
2009-10-20 11:14:46 -04:00
|
|
|
raise ActionController::RoutingError, e.message
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
|
2009-12-11 01:11:16 -05:00
|
|
|
req = Rack::Request.new(env)
|
|
|
|
@set.recognize(req) do |route, params|
|
|
|
|
dispatcher = route.app
|
2010-02-16 16:09:42 -05:00
|
|
|
if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, true)
|
2009-12-11 01:11:16 -05:00
|
|
|
dispatcher.prepare_params!(params)
|
|
|
|
return params
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-12-21 18:34:53 -05:00
|
|
|
raise ActionController::RoutingError, "No route matches #{path.inspect}"
|
2009-08-09 23:38:50 -04:00
|
|
|
end
|
2008-01-17 17:45:54 -05:00
|
|
|
end
|
|
|
|
end
|
2008-07-28 14:38:20 -04:00
|
|
|
end
|