aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/routing/route_set.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/routing/route_set.rb')
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb316
1 files changed, 188 insertions, 128 deletions
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 705314f8ab..40c767e685 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,10 +1,13 @@
require 'action_dispatch/journey'
require 'forwardable'
require 'thread_safe'
+require 'active_support/concern'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/array/extract_options'
require 'action_controller/metal/exceptions'
+require 'action_dispatch/http/request'
module ActionDispatch
module Routing
@@ -18,18 +21,19 @@ module ActionDispatch
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
class Dispatcher #:nodoc:
- def initialize(options={})
- @defaults = options[:defaults]
- @glob_param = options.delete(:glob)
+ def initialize(defaults)
+ @defaults = defaults
@controller_class_names = ThreadSafe::Cache.new
end
def call(env)
params = env[PARAMETERS_KEY]
- # If any of the path parameters has a invalid encoding then
+ # If any of the path parameters has an invalid encoding then
# raise since it's likely to trigger errors further on.
params.each do |key, value|
+ next unless value.respond_to?(:valid_encoding?)
+
unless value.valid_encoding?
raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
end
@@ -48,7 +52,6 @@ module ActionDispatch
def prepare_params!(params)
normalize_controller!(params)
merge_default_action!(params)
- split_glob_param!(params) if @glob_param
end
# If this is a default_controller (i.e. a controller specified by the user)
@@ -84,10 +87,6 @@ module ActionDispatch
def merge_default_action!(params)
params[:action] ||= 'index'
end
-
- def split_glob_param!(params)
- params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
- end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -100,25 +99,7 @@ module ActionDispatch
def initialize
@routes = {}
@helpers = []
- @module = Module.new do
- protected
-
- def handle_positional_args(args, options, segment_keys)
- inner_options = args.extract_options!
- result = options.dup
-
- if args.size > 0
- keys = segment_keys
- if args.size < keys.size - 1 # take format into account
- keys -= self.url_options.keys if self.respond_to?(:url_options)
- keys -= options.keys
- end
- result.merge!(Hash[keys.zip(args)])
- end
-
- result.merge!(inner_options)
- end
- end
+ @module = Module.new
end
def helper_names
@@ -160,68 +141,137 @@ module ActionDispatch
routes.length
end
- private
+ class UrlHelper # :nodoc:
+ def self.create(route, options)
+ if optimize_helper?(route)
+ OptimizedUrlHelper.new(route, options)
+ else
+ new route, options
+ end
+ end
- def define_named_route_methods(name, route)
- define_url_helper route, :"#{name}_path",
- route.defaults.merge(:use_route => name, :only_path => true)
- define_url_helper route, :"#{name}_url",
- route.defaults.merge(:use_route => name, :only_path => false)
+ def self.optimize_helper?(route)
+ !route.glob? && route.path.requirements.empty?
end
- # 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')
- #
- def define_url_helper(route, name, options)
- @module.remove_possible_method name
- @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
- def #{name}(*args)
- if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation?
- options = #{options.inspect}
- options.merge!(url_options) if respond_to?(:url_options)
- options[:path] = "#{optimized_helper(route)}"
- ActionDispatch::Http::URL.url_for(options)
- else
- url_for(handle_positional_args(args, #{options.inspect}, #{route.segment_keys.inspect}))
- end
+ class OptimizedUrlHelper < UrlHelper # :nodoc:
+ attr_reader :arg_size
+
+ def initialize(route, options)
+ super
+ @required_parts = @route.required_parts
+ @arg_size = @required_parts.size
+ end
+
+ def call(t, args)
+ if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t)
+ options = t.url_options.merge @options
+ options[:path] = optimized_helper(args)
+ ActionDispatch::Http::URL.url_for(options)
+ else
+ super
end
- END_EVAL
+ end
+
+ private
+
+ def optimized_helper(args)
+ params = parameterize_args(args)
+ missing_keys = missing_keys(params)
+
+ unless missing_keys.empty?
+ raise_generation_error(params, missing_keys)
+ end
+
+ @route.format params
+ end
+
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
+
+ def parameterize_args(args)
+ params = {}
+ @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v }
+ params
+ end
+
+ def missing_keys(args)
+ args.select{ |part, arg| arg.nil? || arg.empty? }.keys
+ end
+
+ def raise_generation_error(args, missing_keys)
+ constraints = Hash[@route.requirements.merge(args).sort]
+ message = "No route matches #{constraints.inspect}"
+ message << " missing required keys: #{missing_keys.sort.inspect}"
+
+ raise ActionController::UrlGenerationError, message
+ end
+ end
- helpers << name
+ def initialize(route, options)
+ @options = options
+ @segment_keys = route.segment_keys.uniq
+ @route = route
end
- # Clause check about when we need to generate an optimized helper.
- def optimize_helper?(route) #:nodoc:
- route.requirements.except(:controller, :action).empty?
+ def call(t, args)
+ controller_options = t.url_options
+ options = controller_options.merge @options
+ hash = handle_positional_args(controller_options, args, options, @segment_keys)
+ t._routes.url_for(hash)
end
- # Generates the interpolation to be used in the optimized helper.
- def optimized_helper(route)
- string_route = route.ast.to_s
+ def handle_positional_args(controller_options, args, result, path_params)
+ inner_options = args.extract_options!
- while string_route.gsub!(/\([^\)]*\)/, "")
- true
+ if args.size > 0
+ if args.size < path_params.size - 1 # take format into account
+ path_params -= controller_options.keys
+ path_params -= result.keys
+ end
+ path_params.each { |param|
+ result[param] = inner_options[param] || args.shift
+ }
end
- route.required_parts.each_with_index do |part, i|
- # Replace each route parameter
- # e.g. :id for regular parameter or *path for globbing
- # with ruby string interpolation code
- string_route.gsub!(/(\*|:)#{part}/, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}")
- end
+ result.merge!(inner_options)
+ end
+ end
+
+ private
+ # 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')
+ #
+ def define_url_helper(route, name, options)
+ helper = UrlHelper.create(route, options.dup)
- string_route
+ @module.remove_possible_method name
+ @module.module_eval do
+ define_method(name) do |*args|
+ helper.call self, args
+ end
end
+
+ helpers << name
+ end
+
+ def define_named_route_methods(name, route)
+ define_url_helper route, :"#{name}_path",
+ route.defaults.merge(:use_route => name, :only_path => true)
+ define_url_helper route, :"#{name}_url",
+ route.defaults.merge(:use_route => name, :only_path => false)
+ end
end
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
@@ -246,9 +296,7 @@ module ActionDispatch
@finalized = false
@set = Journey::Routes.new
- @router = Journey::Router.new(@set, {
- :parameters_key => PARAMETERS_KEY,
- :request_class => request_class})
+ @router = Journey::Router.new @set
@formatter = Journey::Formatter.new @set
end
@@ -299,7 +347,7 @@ module ActionDispatch
include UrlFor
end
- # Contains all the mounted helpers accross different
+ # Contains all the mounted helpers across different
# engines and the `main_app` helper for the application.
# You can include this in your classes if you want to
# access routes for other engines.
@@ -337,6 +385,8 @@ module ActionDispatch
@_routes = routes
class << self
delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
+ attr_reader :_routes
+ def url_options; {}; end
end
# Make named_routes available in the module singleton
@@ -368,11 +418,19 @@ module ActionDispatch
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
+ if name && named_routes[name]
+ raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \
+ "You may have defined two routes with the same name using the `:as` option, or " \
+ "you may be overriding a route already defined by a resource with the same naming. " \
+ "For the latter, you can restrict the routes created with `resources` as explained here: \n" \
+ "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
+ end
+
path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
route = @set.add_route(app, path, conditions, defaults, name)
- named_routes[name] = route if name && !named_routes[name]
+ named_routes[name] = route if name
route
end
@@ -444,11 +502,12 @@ module ActionDispatch
@recall = recall.dup
@set = set
+ normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- handle_nil_action!
+ normalize_action!
end
def controller
@@ -467,6 +526,11 @@ module ActionDispatch
end
end
+ # Set 'index' as default action for recall
+ def normalize_recall!
+ @recall[:action] ||= 'index'
+ end
+
def normalize_options!
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
@@ -482,8 +546,8 @@ module ActionDispatch
options[:controller] = options[:controller].to_s
end
- if options[:action]
- options[:action] = options[:action].to_s
+ if options.key?(:action)
+ options[:action] = (options[:action] || 'index').to_s
end
end
@@ -493,8 +557,6 @@ module ActionDispatch
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
- @recall[:action] ||= 'index' if current_controller
-
use_recall_for(:controller) or return
use_recall_for(:action) or return
use_recall_for(:id)
@@ -516,19 +578,17 @@ module ActionDispatch
@options[:controller] = controller.sub(%r{^/}, '') if controller
end
- # This handles the case of action: nil being explicitly passed.
- # It is identical to action: "index"
- def handle_nil_action!
- if options.has_key?(:action) && options[:action].nil?
- options[:action] = 'index'
+ # Move 'index' action from options to recall
+ def normalize_action!
+ if @options[:action] == 'index'
+ @recall[:action] = @options.delete(:action)
end
- recall[:action] = options.delete(:action) if options[:action] == 'index'
end
# Generates a path from routes, returns [path, params].
# If no route is generated the formatter will raise ActionController::UrlGenerationError
def generate
- @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
+ @set.formatter.generate(named_route, options, recall, PARAMETERIZE)
end
def different_controller?
@@ -573,41 +633,52 @@ module ActionDispatch
!mounted? && default_url_options.empty?
end
- def _generate_prefix(options = {})
- nil
+ def find_script_name(options)
+ options.delete :script_name
end
- # The +options+ argument must be +nil+ or a hash whose keys are *symbols*.
+ # The +options+ argument must be a hash whose keys are *symbols*.
def url_for(options)
- options = default_url_options.merge(options || {})
+ options = default_url_options.merge options
+
+ user = password = nil
+
+ if options[:user] && options[:password]
+ user = options.delete :user
+ password = options.delete :password
+ end
- user, password = extract_authentication(options)
- recall = options.delete(:_recall)
+ recall = options.delete(:_recall) { {} }
- original_script_name = options.delete(:original_script_name).presence
- script_name = options.delete(:script_name).presence || _generate_prefix(options)
+ original_script_name = options.delete(:original_script_name)
+ script_name = find_script_name options
if script_name && original_script_name
script_name = original_script_name + script_name
end
- path_options = options.except(*RESERVED_OPTIONS)
- path_options = yield(path_options) if block_given?
+ path_options = options.dup
+ RESERVED_OPTIONS.each { |ro| path_options.delete ro }
- path, params = generate(path_options, recall || {})
- params.merge!(options[:params] || {})
+ path, params = generate(path_options, recall)
- ActionDispatch::Http::URL.url_for(options.merge!({
- :path => path,
- :script_name => script_name,
- :params => params,
- :user => user,
- :password => password
- }))
+ if options.key? :params
+ params.merge! options[:params]
+ end
+
+ options[:path] = path
+ options[:script_name] = script_name
+ options[:params] = params
+ options[:user] = user
+ options[:password] = password
+
+ ActionDispatch::Http::URL.url_for(options)
end
def call(env)
- @router.call(env)
+ req = request_class.new(env)
+ req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
+ @router.serve(req)
end
def recognize_path(path, environment = {})
@@ -621,8 +692,8 @@ module ActionDispatch
raise ActionController::RoutingError, e.message
end
- req = @request_class.new(env)
- @router.recognize(req) do |route, matches, params|
+ req = request_class.new(env)
+ @router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
if value.is_a?(String)
@@ -630,8 +701,8 @@ module ActionDispatch
params[key] = URI.parser.unescape(value)
end
end
- old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
- env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params)
+ old_params = req.path_parameters
+ req.path_parameters = old_params.merge params
dispatcher = route.app
while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
dispatcher = dispatcher.app
@@ -649,17 +720,6 @@ module ActionDispatch
raise ActionController::RoutingError, "No route matches #{path.inspect}"
end
-
- private
-
- def extract_authentication(options)
- if options[:user] && options[:password]
- [options.delete(:user), options.delete(:password)]
- else
- nil
- end
- end
-
end
end
end